"use strict";
/**
 * TransactionSubmitter, server-side submission orchestrator for the
 * Midnight contract path (deploy + call).
 *
 * Phase 2b: the SDK invocation moved into a `worker_threads` worker (see
 * `srv/midnight/wallet-worker.ts`) so the SDK's Effect.ts microtask saturation
 * stays off the main thread. This class is now an orchestrator:
 *
 *   1. Insert a `pending` row into PendingSubmissions BEFORE invoking the
 *      worker. The row carries our internal UUID; txHash + contractAddress
 *      are filled in once the worker returns. This is the recovery hook for
 *      the "process crashed mid-submission" case.
 *
 *   2. Build a CapDbPrivateStateProvider on the main thread (where the CAP DB
 *      lives) and register it under an ephemeral `proxyId` with the worker
 *      client. The worker creates a thin proxy that round-trips PS CRUD back
 *      to this provider via `private-state-rpc` messages.
 *
 *   3. Invoke the worker (`walletDeployContract` / `walletSubmitContractCall`).
 *      The worker owns the SDK, the compiled contract artifact (cached by
 *      name), and the wallet facade. It returns only primitives
 *      (`txHash`, `contractAddress`, `onChainStatus`) — no SDK object crosses
 *      the thread boundary.
 *
 *   4. On success → UPDATE row with `{ txHash, contractAddress, status }`.
 *      The crawler's BlockProcessor flips it to 'finalized' once the tx hash
 *      appears in a block.
 *
 *   5. On failure → UPDATE row with `{ status='failed', errorCode, errorMessage }`.
 *      Errors are classified (see classifySubmissionError):
 *        - 1014  permanent (invalid tx), no retry
 *        - 1016  on mainnet: deterministic per forum thread 1190 (early May 2026);
 *                fail fast with a known-issue reference. On preprod: retryable.
 *        - TIMEOUT/NETWORK transient, caller may retry
 *        - TxFailedError from SDK, the on-chain status was not SucceedEntirely
 *
 * The submitter does not retry on its own. The OData action callers decide
 * retry policy based on the returned error classification.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionSubmitter = exports.SubmissionError = void 0;
exports.classifySubmissionError = classifySubmissionError;
exports.reconcilePendingSubmission = reconcilePendingSubmission;
const cds_1 = __importDefault(require("@sap/cds"));
const { INSERT, UPDATE } = cds_1.default.ql;
const midnight_1 = require("#cds-models/midnight");
const cds_model_1 = require("../utils/cds-model");
const CapDbPrivateStateProvider_1 = require("../midnight/CapDbPrivateStateProvider");
const wallet_worker_client_1 = require("../midnight/wallet-worker-client");
class SubmissionError extends Error {
    submissionId;
    classification;
    constructor(submissionId, classification, cause) {
        super(classification.message);
        this.submissionId = submissionId;
        this.classification = classification;
        this.name = 'SubmissionError';
        if (cause instanceof Error && cause.stack)
            this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
    }
}
exports.SubmissionError = SubmissionError;
class TransactionSubmitter {
    deps;
    db;
    constructor(deps) {
        this.deps = deps;
        if (deps.db)
            this.db = deps.db;
    }
    async deploy(args) {
        const submissionId = await this.insertPending('DEPLOY', null, null, args.sessionId);
        const deployFn = this.deps.walletDeployContractImpl ?? wallet_worker_client_1.walletDeployContract;
        let release = null;
        let workerResult;
        try {
            const proxy = await this.registerPrivateStateProxy();
            release = proxy.release;
            workerResult = await deployFn(this.makeDeployRpcArgs(args, proxy.proxyId));
        }
        catch (err) {
            release?.();
            const classification = classifySubmissionError(err, this.deps.network);
            await this.markFailed(submissionId, classification);
            throw new SubmissionError(submissionId, classification, err);
        }
        release?.();
        const { txHash, contractAddress, onChainStatus } = workerResult;
        if (!txHash || !contractAddress) {
            const classification = {
                code: 'MalformedResult',
                retryable: false,
                message: 'Worker deployContract returned without txHash/contractAddress'
            };
            await this.markFailed(submissionId, classification);
            throw new SubmissionError(submissionId, classification);
        }
        const newStatus = onChainStatus === 'SucceedEntirely' ? 'included' : 'failed';
        await this.updateAfterSdk(submissionId, {
            txHash,
            contractAddress,
            status: newStatus,
            errorCode: newStatus === 'failed' ? `OnChainStatus:${onChainStatus}` : undefined,
            errorMessage: newStatus === 'failed' ? `On-chain status was ${onChainStatus}, expected SucceedEntirely` : undefined
        });
        if (newStatus === 'failed') {
            throw new SubmissionError(submissionId, {
                code: `OnChainStatus:${onChainStatus}`,
                retryable: false,
                message: `Deploy on-chain status ${onChainStatus}`
            });
        }
        return { submissionId, txHash, contractAddress, status: newStatus };
    }
    async call(args) {
        const submissionId = await this.insertPending('CALL', args.contractAddress, args.circuit, args.sessionId);
        const callFn = this.deps.walletSubmitContractCallImpl ?? wallet_worker_client_1.walletSubmitContractCall;
        let release = null;
        let workerResult;
        try {
            const proxy = await this.registerPrivateStateProxy();
            release = proxy.release;
            workerResult = await callFn(this.makeCallRpcArgs(args, proxy.proxyId));
        }
        catch (err) {
            release?.();
            const classification = classifySubmissionError(err, this.deps.network);
            await this.markFailed(submissionId, classification);
            throw new SubmissionError(submissionId, classification, err);
        }
        release?.();
        const { txHash, onChainStatus } = workerResult;
        if (!txHash) {
            const classification = {
                code: 'MalformedResult',
                retryable: false,
                message: 'Worker submitContractCall returned without txHash'
            };
            await this.markFailed(submissionId, classification);
            throw new SubmissionError(submissionId, classification);
        }
        const newStatus = onChainStatus === 'SucceedEntirely' ? 'included' : 'failed';
        await this.updateAfterSdk(submissionId, {
            txHash,
            contractAddress: args.contractAddress,
            status: newStatus,
            errorCode: newStatus === 'failed' ? `OnChainStatus:${onChainStatus}` : undefined,
            errorMessage: newStatus === 'failed' ? `On-chain status was ${onChainStatus}, expected SucceedEntirely` : undefined
        });
        if (newStatus === 'failed') {
            throw new SubmissionError(submissionId, {
                code: `OnChainStatus:${onChainStatus}`,
                retryable: false,
                message: `Call on-chain status ${onChainStatus}`
            });
        }
        return { submissionId, txHash, contractAddress: args.contractAddress, status: newStatus };
    }
    // -- Internals -----------------------------------------------------------
    /**
     * Build a CapDbPrivateStateProvider for this submission and register it
     * under a fresh `proxyId`. The worker's PS proxy will round-trip CRUD
     * calls back to the matching main-side instance. `release()` unregisters
     * the provider; it's safe to call exactly once after the worker RPC
     * resolves or rejects.
     *
     * Throws if the configured private-state backend is anything other than
     * 'cap-db' — the legacy LevelDB path is incompatible with worker-routed
     * submissions (the SDK's LevelDB provider doesn't survive a thread
     * boundary, and its on-disk format is dev-only per the SDK docs).
     */
    async registerPrivateStateProxy() {
        const backend = this.deps.walletMaterial.privateStateBackend ?? 'cap-db';
        if (backend !== 'cap-db') {
            throw new Error(`privateStateBackend='${backend}' is not supported on the worker-routed submission path; ` +
                `use 'cap-db' (default).`);
        }
        const db = await this.getDb();
        const provider = new CapDbPrivateStateProvider_1.CapDbPrivateStateProvider({
            accountId: this.deps.walletMaterial.accountId,
            privateStoragePasswordProvider: this.deps.walletMaterial.privateStoragePasswordProvider,
            db
        });
        const proxyId = cds_1.default.utils.uuid();
        (0, wallet_worker_client_1.registerPrivateStateProvider)(proxyId, provider);
        let released = false;
        return {
            proxyId,
            release: () => {
                if (released)
                    return;
                released = true;
                (0, wallet_worker_client_1.unregisterPrivateStateProvider)(proxyId);
            }
        };
    }
    /**
     * The worker stores facades keyed on `accountId` (deterministic per
     * viewing key), set by the pre-warm in connectWalletForSigning. We must
     * use the same key when looking up the facade for deploy/call — the OData
     * user-session UUID would miss. `args.sessionId` is preserved on the
     * PendingSubmissions row for audit.
     */
    makeDeployRpcArgs(args, proxyId) {
        return {
            sessionId: this.deps.walletMaterial.accountId,
            proxyId,
            contractName: args.contractName,
            registration: args.registration,
            indexerHttpUrl: this.deps.contractProvidersConfig.indexerHttpUrl,
            indexerWsUrl: this.deps.contractProvidersConfig.indexerWsUrl,
            proofServerUrl: this.deps.contractProvidersConfig.proofServerUrl,
            networkId: this.deps.network,
            initialPrivateState: args.initialPrivateState
        };
    }
    makeCallRpcArgs(args, proxyId) {
        return {
            sessionId: this.deps.walletMaterial.accountId,
            proxyId,
            contractName: args.contractName,
            registration: args.registration,
            contractAddress: args.contractAddress,
            circuit: args.circuit,
            args: args.args,
            indexerHttpUrl: this.deps.contractProvidersConfig.indexerHttpUrl,
            indexerWsUrl: this.deps.contractProvidersConfig.indexerWsUrl,
            proofServerUrl: this.deps.contractProvidersConfig.proofServerUrl,
            networkId: this.deps.network,
            witnessValues: args.witnessValues
        };
    }
    async getDb() {
        if (this.db)
            return this.db;
        await (0, cds_model_1.ensureNightgateModelLoaded)();
        this.db = await cds_1.default.connect.to('db');
        return this.db;
    }
    async insertPending(actionType, contractAddress, circuitName, sessionId) {
        const db = await this.getDb();
        const submissionId = cds_1.default.utils.uuid();
        await db.run(INSERT.into(midnight_1.PendingSubmissions).entries({
            ID: submissionId,
            txHash: null,
            contractAddress,
            circuitName,
            actionType,
            submittedAt: new Date().toISOString(),
            status: 'pending',
            sessionId
        }));
        return submissionId;
    }
    async updateAfterSdk(submissionId, patch) {
        const db = await this.getDb();
        await db.run(UPDATE.entity(midnight_1.PendingSubmissions).set(patch).where({ ID: submissionId }));
    }
    async markFailed(submissionId, classification) {
        const db = await this.getDb();
        await db.run(UPDATE.entity(midnight_1.PendingSubmissions).set({
            status: 'failed',
            errorCode: classification.code,
            errorMessage: classification.message.slice(0, 500)
        }).where({ ID: submissionId }));
    }
}
exports.TransactionSubmitter = TransactionSubmitter;
// ---- Error classification --------------------------------------------------
const KNOWN_ISSUE_1016_MAINNET = 'https://forum.midnight.network/t/.../1190 (mainnet 1016 Immediately Dropped deterministic rejection, early May 2026)';
/**
 * Classifies a thrown error into a stable code + retryability decision.
 * Used by the OData callers and internally to populate
 * PendingSubmissions.errorCode/errorMessage.
 */
function classifySubmissionError(err, network) {
    const message = err instanceof Error ? err.message : String(err);
    const name = err instanceof Error ? err.name : 'Error';
    // SDK TxFailedError, on-chain status was not success
    if (name === 'TxFailedError' || message.includes('TxFailedError')) {
        return { code: 'TxFailed', retryable: false, message };
    }
    // Substrate pool errors
    if (/\b1014\b|invalid transaction/i.test(message)) {
        return { code: '1014', retryable: false, message: `Invalid transaction (Substrate 1014): ${message}` };
    }
    if (/\b1016\b|Immediately\s*Dropped/i.test(message)) {
        if (network === 'mainnet') {
            return {
                code: '1016',
                retryable: false,
                knownIssueRef: KNOWN_ISSUE_1016_MAINNET,
                message: `Mainnet deterministic rejection (1016 Immediately Dropped). Known issue; see ${KNOWN_ISSUE_1016_MAINNET}`
            };
        }
        return { code: '1016', retryable: true, message: `Transaction pool full or immediately dropped: ${message}` };
    }
    // Network / timeout patterns
    if (/ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|socket hang up|timeout/i.test(message)) {
        return { code: 'NetworkOrTimeout', retryable: true, message };
    }
    // SDK contract-type / config errors are not retryable
    if (/ContractTypeError|IncompleteCallTxPrivateStateConfig|IncompleteFindContractPrivateStateConfig/.test(name)) {
        return { code: name, retryable: false, message };
    }
    // Session lacks signing material; stable code so consumers can recognize it.
    if (name === 'WalletSigningNotAvailable' || /WalletSigningNotAvailable/.test(message)) {
        return {
            code: 'WalletSigningNotAvailable',
            retryable: false,
            message: `${message} (session needs encryptedSeedKey to sign/balance transactions)`
        };
    }
    // Default: unknown but assume non-retryable to avoid hammering
    return { code: name || 'UnknownError', retryable: false, message };
}
// ---- Reconciliation helper (called by crawler's BlockProcessor) ------------
/**
 * Called by BlockProcessor when a transaction is persisted. If a
 * PendingSubmissions row exists with the same txHash, mark it 'finalized'
 * and attach a JSON snapshot of the indexed tx.
 *
 * No-op if no pending row matches (most txs are not ours).
 */
async function reconcilePendingSubmission(db, txHash, indexedTxSnapshot) {
    if (!txHash)
        return;
    await db.run(UPDATE.entity(midnight_1.PendingSubmissions).set({
        status: 'finalized',
        finalizedAt: new Date().toISOString(),
        finalizedTxData: JSON.stringify(indexedTxSnapshot)
    }).where({ txHash, status: { in: ['pending', 'included'] } }));
}
