"use strict";
/**
 * OData handlers for NightgateService submission actions.
 *
 * - `deployContract`        → TransactionSubmitter.deploy()
 * - `submitContractCall`    → TransactionSubmitter.call()
 *
 * Responsibilities here (the submitter itself does NOT do any of these):
 *   1. Parse the JSON-encoded `args` / `initialPrivateState` payloads.
 *   2. Rate-limit per sessionId (deploys are stricter than calls).
 *   3. Resolve `compiledArtifactRef` → compiled contract + zkConfigPath +
 *      privateStateId via the contract registry.
 *   4. Look up the wallet session and build WalletMaterial (real signing-capable
 *      material when the session has a seed; `WalletMaterialUnavailable` otherwise).
 *   5. Catch SubmissionError / SessionNotFoundError / ContractNotRegisteredError /
 *      WalletMaterialUnavailable and translate to OData status codes.
 *
 * The submitter (`srv/submission/TransactionSubmitter.ts`) handles the actual
 * SDK call, error classification, and PendingSubmissions row lifecycle.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerSubmissionHandlers = registerSubmissionHandlers;
const cds_1 = __importDefault(require("@sap/cds"));
const sha256_1 = require("@noble/hashes/sha256");
const utils_1 = require("@noble/hashes/utils");
const TransactionSubmitter_1 = require("./TransactionSubmitter");
const contract_registry_1 = require("./contract-registry");
const wallet_material_factory_1 = require("./wallet-material-factory");
const arg_coercion_1 = require("./arg-coercion");
const nightgate_config_1 = require("../utils/nightgate-config");
const rate_limiter_1 = require("../utils/rate-limiter");
const providers_1 = require("../midnight/providers");
const background_jobs_1 = require("./background-jobs");
const disclosure_indexer_1 = require("./disclosure-indexer");
const grantee_identity_1 = require("./grantee-identity");
const nightgate_config_2 = require("../utils/nightgate-config");
const midnight_1 = require("#cds-models/midnight");
const { INSERT, UPDATE, SELECT } = cds_1.default.ql;
// 5 deploys / hour / session, deploys are heavyweight; tight bound.
const deployRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60 * 60 * 1000, maxRequests: 5 });
// 30 calls / minute / session.
const callRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60 * 1000, maxRequests: 30 });
// 10 doc anchors / hour / session, contract-call heavyweight + extra DB writes.
const anchorRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60 * 60 * 1000, maxRequests: 10 });
// 10 predicate proofs / hour / session — each is TWO heavyweight circuit calls
// (commitValue + provePredicate), so bound it like anchors.
const predicateRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60 * 60 * 1000, maxRequests: 10 });
// 30 disclosure grant/revoke ops / hour / session — single heavyweight circuit
// call each, attester-gated; looser than predicate but tighter than plain calls.
const disclosureRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60 * 60 * 1000, maxRequests: 30 });
const SHA256_HEX_RE = /^[0-9a-fA-F]{64}$/;
const DEFAULT_ATTESTATION_VAULT_REF = 'attestation-vault';
function hexToBytes(hex) {
    const out = new Uint8Array(hex.length / 2);
    for (let i = 0; i < out.length; i++) {
        out[i] = parseInt(hex.substr(i * 2, 2), 16);
    }
    return out;
}
function registerSubmissionHandlers(srv, 
// `any` (not cds.DatabaseService) on purpose: tests inject a minimal
// `{ run }` mock; the handlers only use db.run.
db, options = {}) {
    const walletFactory = options.walletMaterialFactory ?? wallet_material_factory_1.buildWalletMaterialForSession;
    const contractResolver = options.resolveContractImpl ?? contract_registry_1.resolveContract;
    const submitterFactory = options.submitterFactory ?? ((deps) => new TransactionSubmitter_1.TransactionSubmitter(deps));
    const argTypesLoader = options.circuitArgTypesLoader ?? arg_coercion_1.loadCircuitArgTypes;
    const disclosureReindexer = options.disclosureReindexer ?? disclosure_indexer_1.reindexDisclosuresForContract;
    srv.on('deployContract', async (req) => {
        const { compiledArtifactRef, sessionId, initialPrivateState, idempotencyKey } = req.data;
        if (!compiledArtifactRef)
            return req.reject(400, 'compiledArtifactRef is required');
        if (!sessionId)
            return req.reject(400, 'sessionId is required');
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(deployRateLimiter, sessionId, req))
            return;
        let parsedInitialState = {};
        if (initialPrivateState) {
            try {
                parsedInitialState = JSON.parse(initialPrivateState);
            }
            catch {
                return req.reject(400, 'initialPrivateState must be valid JSON');
            }
        }
        // Sync setup phase (on req.tx, translates the well-known setup errors
        // 404/401/501 into status codes via runSubmission). The actual SDK
        // round-trip is deferred to startJob's `work` and surfaces failures
        // via BackgroundJobs.errorCode/errorMessage instead of OData status.
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledArtifactRef);
            const wallet = await walletFactory({ sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            return (0, background_jobs_1.startJob)({
                kind: 'deployContract',
                sessionId,
                idempotencyKey,
                request: { compiledArtifactRef, sessionId, hasInitialState: !!initialPrivateState },
                work: async () => {
                    const result = await submitter.deploy({
                        contractName: compiledArtifactRef,
                        registration: {
                            artifactPath: resolved.artifactPath,
                            privateStateId: resolved.privateStateId,
                            zkConfigPath: resolved.zkConfigPath
                        },
                        initialPrivateState: parsedInitialState,
                        sessionId
                    });
                    return {
                        submissionId: result.submissionId,
                        txHash: result.txHash,
                        contractAddress: result.contractAddress,
                        status: result.status
                    };
                }
            });
        });
    });
    srv.on('submitContractCall', async (req) => {
        const { contractAddress, circuit, compiledArtifactRef, sessionId, args, idempotencyKey } = req.data;
        if (!contractAddress)
            return req.reject(400, 'contractAddress is required');
        if (!circuit)
            return req.reject(400, 'circuit is required');
        if (!compiledArtifactRef)
            return req.reject(400, 'compiledArtifactRef is required');
        if (!sessionId)
            return req.reject(400, 'sessionId is required');
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(callRateLimiter, sessionId, req))
            return;
        let parsedArgs = [];
        if (args) {
            try {
                const v = JSON.parse(args);
                if (!Array.isArray(v))
                    return req.reject(400, 'args must be a JSON array');
                parsedArgs = v;
            }
            catch {
                return req.reject(400, 'args must be valid JSON');
            }
        }
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledArtifactRef);
            // Coerce JSON-parsed args into the value shapes the compiled circuit
            // requires (Bytes<N> → Uint8Array, Uint<N> → BigInt) before the
            // worker spreads them into fn(...callArgs). CoercionError → 400 via
            // runSubmission. See srv/submission/arg-coercion.ts.
            const argTypes = argTypesLoader(resolved.zkConfigPath, circuit);
            const coercedArgs = (0, arg_coercion_1.coerceCircuitArgs)(parsedArgs, argTypes);
            const wallet = await walletFactory({ sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            return (0, background_jobs_1.startJob)({
                kind: 'submitContractCall',
                sessionId,
                idempotencyKey,
                request: { contractAddress, circuit, compiledArtifactRef, sessionId, argCount: coercedArgs.length },
                work: async () => {
                    const result = await submitter.call({
                        contractAddress,
                        circuit,
                        args: coercedArgs,
                        contractName: compiledArtifactRef,
                        registration: {
                            artifactPath: resolved.artifactPath,
                            privateStateId: resolved.privateStateId,
                            zkConfigPath: resolved.zkConfigPath
                        },
                        sessionId
                    });
                    return {
                        submissionId: result.submissionId,
                        txHash: result.txHash,
                        contractAddress: result.contractAddress,
                        status: result.status
                    };
                }
            });
        });
    });
    srv.on('anchorDocument', async (req) => {
        const data = req.data;
        if (!data.sha256)
            return req.reject(400, 'sha256 is required');
        if (!data.storageRef)
            return req.reject(400, 'storageRef is required');
        if (!data.sessionId)
            return req.reject(400, 'sessionId is required');
        if (!data.contractAddress)
            return req.reject(400, 'contractAddress is required');
        if (!SHA256_HEX_RE.test(data.sha256)) {
            return req.reject(400, 'sha256 must be 64 hex chars (32 bytes)');
        }
        const metadataStr = data.metadata ?? '';
        const compiledRef = data.compiledArtifactRef && data.compiledArtifactRef.length > 0
            ? data.compiledArtifactRef
            : DEFAULT_ATTESTATION_VAULT_REF;
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(anchorRateLimiter, data.sessionId, req))
            return;
        // Compute the on-chain inputs: payload_hash from the caller's sha256
        // and metadata_hash from the public metadata blob. Both are 32-byte
        // commitments — the actual bytes live off-chain at `storageRef`.
        const payloadHashBytes = hexToBytes(data.sha256);
        const metadataHashBytes = (0, sha256_1.sha256)(new TextEncoder().encode(metadataStr));
        // Insert the Documents row up-front on req.tx so the row ID is stable
        // and immediately queryable, even though the on-chain anchoring is
        // deferred to the background job. Mirrors the PendingSubmissions
        // pattern: clients have a handle to retry against without polling.
        const documentId = cds_1.default.utils.uuid();
        const insertedAt = new Date().toISOString();
        await db.run(INSERT.into(midnight_1.Documents).entries({
            ID: documentId,
            sha256: data.sha256.toLowerCase(),
            contentType: data.contentType ?? null,
            size: data.size ?? null,
            storageRef: data.storageRef,
            anchoredTxHash: null,
            anchoredAt: null,
            createdAt: insertedAt,
            modifiedAt: insertedAt
        }));
        // Sync setup (errors here become 404/401/501 via runSubmission); the
        // SDK round-trip + Documents UPDATE move into the background job.
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledRef);
            const wallet = await walletFactory({ sessionId: data.sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            const job = await (0, background_jobs_1.startJob)({
                kind: 'anchorDocument',
                sessionId: data.sessionId,
                idempotencyKey: data.idempotencyKey,
                request: {
                    sha256: data.sha256.toLowerCase(),
                    contractAddress: data.contractAddress,
                    compiledRef,
                    documentId
                },
                work: async () => {
                    const result = await submitter.call({
                        contractAddress: data.contractAddress,
                        circuit: 'attest',
                        args: [payloadHashBytes, metadataHashBytes],
                        contractName: compiledRef,
                        registration: {
                            artifactPath: resolved.artifactPath,
                            privateStateId: resolved.privateStateId,
                            zkConfigPath: resolved.zkConfigPath
                        },
                        sessionId: data.sessionId
                    });
                    const anchoredAt = new Date().toISOString();
                    await db.run(UPDATE.entity(midnight_1.Documents)
                        .set({ anchoredTxHash: result.txHash, anchoredAt, modifiedAt: anchoredAt })
                        .where({ ID: documentId }));
                    return {
                        documentId,
                        attestationId: data.sha256.toLowerCase(),
                        txHash: result.txHash,
                        anchoredAt
                    };
                }
            });
            return { jobId: job.jobId, status: job.status, documentId };
        });
    });
    srv.on('verifyDocument', async (req) => {
        const { documentId, providedSha256 } = req.data;
        if (!documentId)
            return req.reject(400, 'documentId is required');
        if (!providedSha256)
            return req.reject(400, 'providedSha256 is required');
        if (!SHA256_HEX_RE.test(providedSha256)) {
            return req.reject(400, 'providedSha256 must be 64 hex chars (32 bytes)');
        }
        const doc = await db.run(SELECT.one.from(midnight_1.Documents).where({ ID: documentId }));
        if (!doc)
            return req.reject(404, `Document ${documentId} not found`);
        const hashMatches = doc.sha256?.toLowerCase() === providedSha256.toLowerCase();
        const anchoredOk = Boolean(doc.anchoredTxHash);
        // Only resolve the on-chain status if we have a txHash and the hash
        // matched. Skipping the SELECT in the mismatch path saves one DB
        // round-trip on what is the "tampered" answer most of the time.
        let chainSuccess = false;
        if (anchoredOk && hashMatches) {
            const txRow = await db.run(SELECT.one.from(midnight_1.Transactions)
                .columns('ID', 'hash')
                .where({ hash: doc.anchoredTxHash }));
            if (txRow?.ID) {
                const result = await db.run(SELECT.one.from(midnight_1.TransactionResults)
                    .columns('status')
                    .where({ transaction_ID: txRow.ID }));
                chainSuccess = result?.status === 'SUCCESS';
            }
        }
        return {
            verified: hashMatches && anchoredOk && chainSuccess,
            anchoredTxHash: doc.anchoredTxHash ?? '',
            anchoredAt: doc.anchoredAt ?? null,
            originalSha256: doc.sha256 ?? ''
        };
    });
    srv.on('issuePredicateAttestation', async (req) => {
        const data = req.data;
        if (!data.payloadHash)
            return req.reject(400, 'payloadHash is required');
        if (!SHA256_HEX_RE.test(data.payloadHash))
            return req.reject(400, 'payloadHash must be 64 hex chars (32 bytes)');
        if (data.value === undefined || data.value === null || data.value === '') {
            return req.reject(400, 'value is required');
        }
        let valueBig;
        try {
            valueBig = BigInt(data.value);
        }
        catch {
            return req.reject(400, 'value must be an integer (decimal string)');
        }
        if (valueBig < 0n)
            return req.reject(400, 'value must be a non-negative integer');
        if (data.threshold === undefined || data.threshold === null)
            return req.reject(400, 'threshold is required');
        let thresholdBig;
        try {
            thresholdBig = BigInt(data.threshold);
        }
        catch {
            return req.reject(400, 'threshold must be an integer');
        }
        if (thresholdBig < 0n)
            return req.reject(400, 'threshold must be a non-negative integer');
        let op;
        if (data.predicate === 'lessOrEqual')
            op = 0;
        else if (data.predicate === 'greaterOrEqual')
            op = 1;
        else
            return req.reject(400, "predicate must be 'lessOrEqual' or 'greaterOrEqual'");
        if (!data.sessionId)
            return req.reject(400, 'sessionId is required');
        if (!data.contractAddress)
            return req.reject(400, 'contractAddress is required');
        let saltHex;
        if (data.salt) {
            if (!SHA256_HEX_RE.test(data.salt))
                return req.reject(400, 'salt must be 64 hex chars (32 bytes)');
            saltHex = data.salt.toLowerCase();
        }
        else {
            saltHex = (0, utils_1.bytesToHex)((0, utils_1.randomBytes)(32));
        }
        if (data.valueCommitment && !SHA256_HEX_RE.test(data.valueCommitment)) {
            return req.reject(400, 'valueCommitment must be 64 hex chars (32 bytes)');
        }
        const compiledRef = data.compiledArtifactRef && data.compiledArtifactRef.length > 0
            ? data.compiledArtifactRef
            : DEFAULT_ATTESTATION_VAULT_REF;
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(predicateRateLimiter, data.sessionId, req))
            return;
        const payloadHashBytes = hexToBytes(data.payloadHash);
        // The hidden value + salt travel ONLY as circuit witnesses — never as a
        // circuit arg, never persisted. This is what keeps the value private.
        const witnessValues = { attestedValue: valueBig.toString(), valueSalt: saltHex };
        // Insert the row up-front (mirrors anchorDocument): stable handle the
        // caller can poll independently of getJobStatus. Note: `value`/`salt`
        // are intentionally NOT stored.
        const predicateAttestationId = cds_1.default.utils.uuid();
        const insertedAt = new Date().toISOString();
        await db.run(INSERT.into(midnight_1.PredicateAttestations).entries({
            ID: predicateAttestationId,
            payloadHash: data.payloadHash.toLowerCase(),
            contractAddress: data.contractAddress,
            predicate: data.predicate,
            op,
            // Integer64 column; caller may pass the scaled integer as a string to
            // preserve precision past Number.MAX_SAFE_INTEGER. cds-models types it
            // as `number`, but the DB layer accepts the string at runtime.
            threshold: data.threshold,
            unit: data.unit ?? null,
            valueCommitment: data.valueCommitment ? data.valueCommitment.toLowerCase() : null,
            provenTxHash: null,
            provenAt: null,
            createdAt: insertedAt,
            modifiedAt: insertedAt
        }));
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledRef);
            const wallet = await walletFactory({ sessionId: data.sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            const registration = {
                artifactPath: resolved.artifactPath,
                privateStateId: resolved.privateStateId,
                zkConfigPath: resolved.zkConfigPath
            };
            const job = await (0, background_jobs_1.startJob)({
                kind: 'issuePredicateAttestation',
                sessionId: data.sessionId,
                idempotencyKey: data.idempotencyKey,
                request: {
                    payloadHash: data.payloadHash.toLowerCase(),
                    contractAddress: data.contractAddress,
                    predicate: data.predicate,
                    threshold: String(data.threshold),
                    predicateAttestationId
                },
                work: async () => {
                    // 1. Bind the numeric commitment to the attestation. The
                    //    commitment is computed in-circuit from the witnesses.
                    await submitter.call({
                        contractAddress: data.contractAddress,
                        circuit: 'commitValue',
                        args: [payloadHashBytes],
                        contractName: compiledRef,
                        registration,
                        sessionId: data.sessionId,
                        witnessValues
                    });
                    // 2. Prove the predicate. The ledger only accepts this tx if
                    //    the in-circuit asserts (commitment match + predicate)
                    //    held — so a successful tx IS the verified proof.
                    const proof = await submitter.call({
                        contractAddress: data.contractAddress,
                        circuit: 'provePredicate',
                        args: [payloadHashBytes, thresholdBig, BigInt(op)],
                        contractName: compiledRef,
                        registration,
                        sessionId: data.sessionId,
                        witnessValues
                    });
                    const provenAt = new Date().toISOString();
                    await db.run(UPDATE.entity(midnight_1.PredicateAttestations)
                        .set({ provenTxHash: proof.txHash, provenAt, modifiedAt: provenAt })
                        .where({ ID: predicateAttestationId }));
                    return {
                        predicateAttestationId,
                        payloadHash: data.payloadHash.toLowerCase(),
                        claim: {
                            predicate: data.predicate,
                            threshold: String(data.threshold),
                            unit: data.unit ?? null
                        },
                        proof: {
                            system: 'midnight-compact',
                            circuit: 'provePredicate',
                            verificationMethod: data.contractAddress,
                            proofValue: proof.txHash
                        }
                    };
                }
            });
            return { jobId: job.jobId, status: job.status, predicateAttestationId };
        });
    });
    srv.on('verifyPredicateAttestation', async (req) => {
        const { predicateAttestationId } = req.data;
        if (!predicateAttestationId)
            return req.reject(400, 'predicateAttestationId is required');
        const row = await db.run(SELECT.one.from(midnight_1.PredicateAttestations).where({ ID: predicateAttestationId }));
        if (!row)
            return req.reject(404, `PredicateAttestation ${predicateAttestationId} not found`);
        const provenOk = Boolean(row.provenTxHash);
        // Same chain-success check as verifyDocument: the proof tx must resolve
        // to an indexed SUCCESS result. Only then is the on-chain predicate
        // verification trustworthy.
        let chainSuccess = false;
        if (provenOk) {
            const txRow = await db.run(SELECT.one.from(midnight_1.Transactions).columns('ID', 'hash').where({ hash: row.provenTxHash }));
            if (txRow?.ID) {
                const result = await db.run(SELECT.one.from(midnight_1.TransactionResults).columns('status').where({ transaction_ID: txRow.ID }));
                chainSuccess = result?.status === 'SUCCESS';
            }
        }
        return {
            verified: provenOk && chainSuccess,
            predicate: row.predicate ?? '',
            threshold: row.threshold ?? 0,
            unit: row.unit ?? '',
            valueCommitment: row.valueCommitment ?? '',
            provenTxHash: row.provenTxHash ?? '',
            provenAt: row.provenAt ?? null
        };
    });
    srv.on('grantDisclosure', async (req) => {
        const data = req.data;
        if (!data.payloadHash)
            return req.reject(400, 'payloadHash is required');
        if (!SHA256_HEX_RE.test(data.payloadHash))
            return req.reject(400, 'payloadHash must be 64 hex chars (32 bytes)');
        if (!data.grantee)
            return req.reject(400, 'grantee is required');
        if (!SHA256_HEX_RE.test(data.grantee))
            return req.reject(400, 'grantee must be 64 hex chars (32 bytes)');
        if (data.level === undefined || data.level === null)
            return req.reject(400, 'level is required');
        let levelNum;
        try {
            levelNum = Number(data.level);
        }
        catch {
            return req.reject(400, 'level must be 0, 1, or 2');
        }
        if (!Number.isInteger(levelNum) || levelNum < 0 || levelNum > 2) {
            return req.reject(400, 'level must be 0 (public), 1 (legitimate-interest), or 2 (authority)');
        }
        if (!data.sessionId)
            return req.reject(400, 'sessionId is required');
        if (!data.contractAddress)
            return req.reject(400, 'contractAddress is required');
        const compiledRef = data.compiledArtifactRef && data.compiledArtifactRef.length > 0
            ? data.compiledArtifactRef
            : DEFAULT_ATTESTATION_VAULT_REF;
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(disclosureRateLimiter, data.sessionId, req))
            return;
        const payloadHashBytes = hexToBytes(data.payloadHash);
        const granteeBytes = hexToBytes(data.grantee);
        const payloadHashLc = data.payloadHash.toLowerCase();
        const granteeLc = data.grantee.toLowerCase();
        // Insert the row up-front (mirrors anchorDocument / issuePredicateAttestation):
        // a stable handle the caller can poll. active=false until the chain indexer
        // (Phase 2) confirms the grant is present in ledger state — the handler insert
        // is an optimistic placeholder, the chain is the source of truth.
        const disclosureGrantId = cds_1.default.utils.uuid();
        const insertedAt = new Date().toISOString();
        await db.run(INSERT.into(midnight_1.DisclosureGrants).entries({
            ID: disclosureGrantId,
            payloadHash: payloadHashLc,
            grantee: granteeLc,
            level: levelNum,
            contractAddress: data.contractAddress,
            grantedTxHash: null,
            revokedTxHash: null,
            active: false,
            createdAt: insertedAt,
            modifiedAt: insertedAt
        }));
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledRef);
            const wallet = await walletFactory({ sessionId: data.sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            const registration = {
                artifactPath: resolved.artifactPath,
                privateStateId: resolved.privateStateId,
                zkConfigPath: resolved.zkConfigPath
            };
            const job = await (0, background_jobs_1.startJob)({
                kind: 'grantDisclosure',
                sessionId: data.sessionId,
                idempotencyKey: data.idempotencyKey,
                request: {
                    payloadHash: payloadHashLc,
                    grantee: granteeLc,
                    level: levelNum,
                    contractAddress: data.contractAddress,
                    disclosureGrantId
                },
                work: async () => {
                    const result = await submitter.call({
                        contractAddress: data.contractAddress,
                        circuit: 'grantDisclosure',
                        args: [payloadHashBytes, granteeBytes, BigInt(levelNum)],
                        contractName: compiledRef,
                        registration,
                        sessionId: data.sessionId
                    });
                    const grantedAt = new Date().toISOString();
                    await db.run(UPDATE.entity(midnight_1.DisclosureGrants)
                        .set({ grantedTxHash: result.txHash, modifiedAt: grantedAt })
                        .where({ ID: disclosureGrantId }));
                    // Best-effort: pull the grant back out of on-chain state so the
                    // row's `active` flag becomes chain-confirmed. Never fail the
                    // submit on an indexing error — the row already records intent.
                    await reindexAfterSubmit(data.contractAddress, resolved);
                    return {
                        disclosureGrantId,
                        payloadHash: payloadHashLc,
                        grantee: granteeLc,
                        level: levelNum,
                        txHash: result.txHash
                    };
                }
            });
            return { jobId: job.jobId, status: job.status, disclosureGrantId };
        });
    });
    srv.on('revokeDisclosure', async (req) => {
        const data = req.data;
        if (!data.payloadHash)
            return req.reject(400, 'payloadHash is required');
        if (!SHA256_HEX_RE.test(data.payloadHash))
            return req.reject(400, 'payloadHash must be 64 hex chars (32 bytes)');
        if (!data.grantee)
            return req.reject(400, 'grantee is required');
        if (!SHA256_HEX_RE.test(data.grantee))
            return req.reject(400, 'grantee must be 64 hex chars (32 bytes)');
        if (!data.sessionId)
            return req.reject(400, 'sessionId is required');
        if (!data.contractAddress)
            return req.reject(400, 'contractAddress is required');
        const compiledRef = data.compiledArtifactRef && data.compiledArtifactRef.length > 0
            ? data.compiledArtifactRef
            : DEFAULT_ATTESTATION_VAULT_REF;
        if (rejectIfMainnetBlocked(req))
            return;
        if (!checkRate(disclosureRateLimiter, data.sessionId, req))
            return;
        const payloadHashBytes = hexToBytes(data.payloadHash);
        const granteeBytes = hexToBytes(data.grantee);
        const payloadHashLc = data.payloadHash.toLowerCase();
        const granteeLc = data.grantee.toLowerCase();
        return runSubmission(req, async () => {
            const facadeCfg = facadeConfigFromEnv();
            await (0, providers_1.ensureNetworkId)(facadeCfg.networkId);
            const resolved = await contractResolver(compiledRef);
            const wallet = await walletFactory({ sessionId: data.sessionId, db, facadeConfig: facadeCfg });
            const submitter = submitterFactory(buildSubmitterDeps(db, resolved, wallet));
            const registration = {
                artifactPath: resolved.artifactPath,
                privateStateId: resolved.privateStateId,
                zkConfigPath: resolved.zkConfigPath
            };
            const job = await (0, background_jobs_1.startJob)({
                kind: 'revokeDisclosure',
                sessionId: data.sessionId,
                idempotencyKey: data.idempotencyKey,
                request: {
                    payloadHash: payloadHashLc,
                    grantee: granteeLc,
                    contractAddress: data.contractAddress
                },
                work: async () => {
                    const result = await submitter.call({
                        contractAddress: data.contractAddress,
                        circuit: 'revokeDisclosure',
                        args: [payloadHashBytes, granteeBytes],
                        contractName: compiledRef,
                        registration,
                        sessionId: data.sessionId
                    });
                    // Mark any matching optimistic/active grant row as revoked. The
                    // chain indexer (Phase 2) is authoritative on `active`; this is a
                    // best-effort flip so the row reflects intent before reindex.
                    const revokedAt = new Date().toISOString();
                    await db.run(UPDATE.entity(midnight_1.DisclosureGrants)
                        .set({ revokedTxHash: result.txHash, active: false, modifiedAt: revokedAt })
                        .where({
                        contractAddress: data.contractAddress,
                        payloadHash: payloadHashLc,
                        grantee: granteeLc
                    }));
                    // Best-effort: reconcile against on-chain state (the chain is
                    // authoritative on `active`). Never fail the submit on error.
                    await reindexAfterSubmit(data.contractAddress, resolved);
                    return {
                        payloadHash: payloadHashLc,
                        grantee: granteeLc,
                        txHash: result.txHash
                    };
                }
            });
            return { jobId: job.jobId, status: job.status };
        });
    });
    srv.on('registerGranteeIdentity', async (req) => {
        const userId = req.user?.id;
        if (!userId)
            return req.reject(401, 'authentication required');
        const { bindingInput, scope } = req.data;
        if (!bindingInput)
            return req.reject(400, 'bindingInput is required');
        const bindingKind = (0, nightgate_config_2.getConfiguredGranteeBinding)((0, nightgate_config_1.getNightgatePluginConfig)());
        let granteeId;
        try {
            granteeId = (0, grantee_identity_1.deriveGranteeId)(bindingKind, bindingInput);
        }
        catch (err) {
            return req.reject(400, err instanceof Error ? err.message : String(err));
        }
        const scopeNorm = scope && scope.length > 0 ? scope : null;
        const now = new Date().toISOString();
        // Idempotent on (userId, scope): re-registering updates in place.
        const existing = await db.run(SELECT.one.from(midnight_1.GranteeIdentities).where({ userId, scope: scopeNorm }));
        if (existing) {
            await db.run(UPDATE.entity(midnight_1.GranteeIdentities)
                .set({ granteeId, bindingKind, modifiedAt: now })
                .where({ ID: existing.ID }));
            return { ID: existing.ID, granteeId, bindingKind };
        }
        const ID = cds_1.default.utils.uuid();
        await db.run(INSERT.into(midnight_1.GranteeIdentities).entries({
            ID, userId, granteeId, bindingKind, scope: scopeNorm,
            createdAt: now, modifiedAt: now
        }));
        return { ID, granteeId, bindingKind };
    });
    /**
     * Best-effort chain reindex after a disclosure grant/revoke submit. Swallows
     * all errors: an indexing failure must never fail the submission (the row
     * already records intent; a later reindex reconciles).
     */
    async function reindexAfterSubmit(contractAddress, resolved) {
        try {
            await disclosureReindexer({
                db,
                contractAddress,
                artifactPath: resolved.artifactPath,
                contractProvidersConfig: contractProvidersConfigFromEnv(resolved.zkConfigPath)
            });
        }
        catch {
            /* best-effort; intentionally ignored */
        }
    }
    function buildSubmitterDeps(db, resolved, wallet) {
        const nightgateConfig = (0, nightgate_config_1.getNightgatePluginConfig)();
        const { network, submissionEndpoints } = (0, nightgate_config_1.resolveNightgateRuntimeConfig)(nightgateConfig);
        const privateStateBackend = (0, nightgate_config_1.getConfiguredPrivateStateBackend)(nightgateConfig);
        const contractProvidersConfig = {
            indexerHttpUrl: submissionEndpoints.indexerHttpUrl,
            indexerWsUrl: submissionEndpoints.indexerWsUrl,
            proofServerUrl: submissionEndpoints.proofServerUrl,
            zkConfigPath: resolved.zkConfigPath
        };
        return {
            contractProvidersConfig,
            walletMaterial: { ...wallet, privateStateBackend: wallet.privateStateBackend ?? privateStateBackend },
            db,
            network: network
        };
    }
}
/** Resolves the WalletFacade build config from cds.requires.nightgate + env vars. */
function facadeConfigFromEnv() {
    const nightgateConfig = (0, nightgate_config_1.getNightgatePluginConfig)();
    const { network, nodeUrl, submissionEndpoints } = (0, nightgate_config_1.resolveNightgateRuntimeConfig)(nightgateConfig);
    return {
        networkId: network,
        indexerHttpUrl: submissionEndpoints.indexerHttpUrl,
        indexerWsUrl: submissionEndpoints.indexerWsUrl,
        proofServerUrl: submissionEndpoints.proofServerUrl,
        relayUrl: nodeUrl
    };
}
/** Contract-only provider config (no wallet) for read-side reindexing. */
function contractProvidersConfigFromEnv(zkConfigPath) {
    const { submissionEndpoints } = (0, nightgate_config_1.resolveNightgateRuntimeConfig)((0, nightgate_config_1.getNightgatePluginConfig)());
    return {
        indexerHttpUrl: submissionEndpoints.indexerHttpUrl,
        indexerWsUrl: submissionEndpoints.indexerWsUrl,
        proofServerUrl: submissionEndpoints.proofServerUrl,
        zkConfigPath
    };
}
/**
 * Mainnet submission gate. Returns true (and rejects with 403) when the resolved
 * network is mainnet and allowMainnetSubmission is not enabled. Call at the top
 * of every on-chain submission handler before doing any work.
 */
function rejectIfMainnetBlocked(req) {
    const reason = (0, nightgate_config_1.mainnetSubmissionBlockReason)((0, nightgate_config_1.getNightgatePluginConfig)());
    if (reason) {
        req.reject?.(403, reason);
        return true;
    }
    return false;
}
function checkRate(limiter, sessionId, req) {
    const r = limiter.check(sessionId);
    if (!r.allowed) {
        req.reject?.(429, `Rate limited. Retry after ${Math.ceil(r.retryAfterMs / 1000)}s`);
        return false;
    }
    return true;
}
/** Catch the known error classes and translate to OData status codes. */
async function runSubmission(req, op) {
    try {
        return await op();
    }
    catch (err) {
        if (err instanceof arg_coercion_1.CoercionError) {
            // Bad arg encoding (invalid hex, wrong byte length, non-integer
            // Uint, …) — a clean 400, not a deep circuit type error.
            return req.reject(400, err.message);
        }
        if (err instanceof contract_registry_1.ContractNotRegisteredError) {
            return req.reject(404, err.message);
        }
        if (err instanceof wallet_material_factory_1.SessionNotFoundError) {
            return req.reject(401, err.message);
        }
        if (err instanceof wallet_material_factory_1.WalletMaterialUnavailable) {
            // 501 = the session lacks signing material (no seed). The caller must
            // run connectWalletForSigning before deploy/call/submit actions.
            return req.reject(501, err.message);
        }
        if (err instanceof TransactionSubmitter_1.SubmissionError) {
            const c = err.classification;
            return req.reject(c.retryable ? 503 : 400, JSON.stringify({
                code: c.code,
                retryable: c.retryable,
                knownIssueRef: c.knownIssueRef,
                message: c.message,
                submissionId: err.submissionId
            }));
        }
        const msg = err instanceof Error ? err.message : String(err);
        return req.reject(500, msg);
    }
}
