"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateChannelKey = generateChannelKey;
exports.deriveEscrowAddress = deriveEscrowAddress;
exports.makeVoucher = makeVoucher;
exports.verifyVoucher = verifyVoucher;
exports.claimEscrow = claimEscrow;
exports.refundEscrow = refundEscrow;
const secp256k1_1 = require("@noble/curves/secp256k1");
const sha256_1 = require("@noble/hashes/sha256");
const kaspa_1 = require("../../vendor/kaspa-wasm/kaspa");
const escrow_template_1 = require("./escrow-template");
const vault_1 = require("../vault");
/**
 * Trust-minimized x402 payment channel (SompiEscrow covenant), pure JS.
 *
 * The client funds an escrow address once. As it consumes requests it issues
 * off-chain vouchers — BIP340 schnorr signatures over sha256(amount_le8) —
 * authorizing a running claim total. The server closes with the latest voucher
 * (taking at most the authorized amount, change back to escrow); the client
 * reclaims everything after the timeout if the server goes silent. Neither
 * party has to trust the other.
 */
const SUBNETWORK_NATIVE = "00".repeat(20);
/** Channel keypair (x-only public, raw private) for one escrow party. */
function generateChannelKey() {
    const priv = secp256k1_1.schnorr.utils.randomPrivateKey();
    return { privateKey: (0, escrow_template_1.bytesToHex)(priv), publicKey: (0, escrow_template_1.bytesToHex)(secp256k1_1.schnorr.getPublicKey(priv)) };
}
function deriveEscrowAddress(params, networkId) {
    const redeem = (0, escrow_template_1.buildEscrowRedeemScript)(params.clientPublic, params.serverPublic, params.timeout);
    const address = (0, kaspa_1.addressFromScriptPublicKey)((0, kaspa_1.payToScriptHashScript)(redeem), networkId);
    if (!address)
        throw new Error("could not derive escrow address");
    return address.toString();
}
/**
 * Client-side: produce a voucher authorizing the server to claim up to
 * `amountSompi`. Vouchers are cumulative — issue a fresh one for the new
 * running total each time the client consumes more service.
 */
function makeVoucher(clientPrivateHex, amountSompi) {
    const amountLe8 = (0, escrow_template_1.amountToLe8)(amountSompi);
    const sig = secp256k1_1.schnorr.sign((0, sha256_1.sha256)(amountLe8), (0, escrow_template_1.hexToBytes)(clientPrivateHex));
    return { amountSompi: amountSompi.toString(), voucherHex: (0, escrow_template_1.bytesToHex)(sig) };
}
/** Verify a voucher off-chain (server checks before serving). */
function verifyVoucher(clientPublicHex, amountSompi, voucherHex) {
    try {
        return secp256k1_1.schnorr.verify((0, escrow_template_1.hexToBytes)(voucherHex), (0, sha256_1.sha256)((0, escrow_template_1.amountToLe8)(amountSompi)), (0, escrow_template_1.hexToBytes)(clientPublicHex));
    }
    catch {
        return false;
    }
}
async function escrowUtxo(wallet, address) {
    const rpc = await wallet.client();
    const { entries } = await rpc.getUtxosByAddresses([address]);
    if (!entries.length)
        throw new Error(`escrow ${address} has no UTXOs; fund it first`);
    return entries
        .map((e) => ({
        txid: String(e?.outpoint?.transactionId ?? e?.entry?.outpoint?.transactionId),
        index: Number(e?.outpoint?.index ?? e?.entry?.outpoint?.index),
        amount: BigInt(e?.amount ?? e?.entry?.amount ?? 0),
    }))
        .sort((a, b) => (a.amount > b.amount ? -1 : 1))[0];
}
// Post-Toccata the live node meters runtime script units at ~100_000 per
// committed sigop. The claim path runs two signature ops (checkSig +
// checkSigFromStack) plus OpSHA256/introspection (~200_544 units), so it must
// commit 3; the refund path runs a single checkSig and commits 1.
function inputBase(utxo, escrowSpk, sequence, sigOpCount) {
    return {
        previousOutpoint: { transactionId: utxo.txid, index: utxo.index },
        sequence,
        sigOpCount,
        utxo: {
            outpoint: { transactionId: utxo.txid, index: utxo.index },
            amount: utxo.amount,
            scriptPublicKey: escrowSpk,
            blockDaaScore: 0n,
            isCoinbase: false,
        },
    };
}
/**
 * Server-side: close the channel by claiming `claimSompi` (≤ the voucher
 * amount) to `destination`, with the remainder returning to escrow. Fee comes
 * out of the server's own output.
 */
async function claimEscrow(wallet, params, serverPrivateHex, voucher, claimSompi, destination, feeSompi) {
    if (claimSompi > voucher.amountSompi) {
        throw new Error(`claim ${claimSompi} exceeds voucher authorization ${voucher.amountSompi}`);
    }
    if (!verifyVoucher(params.clientPublic, voucher.amountSompi, voucher.voucherHex)) {
        throw new Error("voucher signature does not verify against the client public key");
    }
    const redeem = (0, escrow_template_1.buildEscrowRedeemScript)(params.clientPublic, params.serverPublic, params.timeout);
    const escrowSpk = (0, kaspa_1.payToScriptHashScript)(redeem);
    const utxo = await escrowUtxo(wallet, deriveEscrowAddress(params, wallet.networkId));
    const rpc = await wallet.client();
    const destSpk = (0, kaspa_1.payToAddressScript)(destination);
    // Estimate the claim fee from the node (the demo's per-request price can be
    // smaller than a flat fee, so a realistic estimate matters).
    let resolvedFee = feeSompi;
    if (resolvedFee === undefined) {
        const est = await rpc.getFeeEstimate();
        const feerate = est.estimate?.normalBuckets?.[0]?.feerate ?? 100;
        resolvedFee = (0, vault_1.estimateVaultSpendFeeSompi)(utxo.amount, [
            { value: claimSompi, spkScriptLen: String(destSpk.script).length / 2 },
            { value: utxo.amount - claimSompi, spkScriptLen: String(escrowSpk.script).length / 2 },
        ], redeem.length, feerate);
    }
    if (claimSompi <= resolvedFee) {
        throw new Error(`claim ${claimSompi} sompi does not cover the estimated fee ${resolvedFee} sompi — ` +
            `accumulate more before claiming`);
    }
    const outputs = [
        { value: claimSompi - resolvedFee, scriptPublicKey: destSpk },
        { value: utxo.amount - claimSompi, scriptPublicKey: escrowSpk },
    ];
    if (outputs.some((o) => o.value <= 0n))
        throw new Error("escrow UTXO too small for this claim");
    const base = inputBase(utxo, escrowSpk, 0n, 3); // claim: 2 sig ops + sha256/introspection
    const txShape = { version: 0, outputs, lockTime: 0n, subnetworkId: SUBNETWORK_NATIVE, gas: 0n, payload: "" };
    const unsigned = new kaspa_1.Transaction({ ...txShape, inputs: [{ ...base, signatureScript: "" }] });
    const pushedSig = (0, kaspa_1.createInputSignature)(unsigned, 0, new kaspa_1.PrivateKey(serverPrivateHex), kaspa_1.SighashType.All);
    const serverSig = (0, escrow_template_1.hexToBytes)(pushedSig).slice(1); // strip push-65 opcode
    const args = (0, escrow_template_1.buildClaimArgs)(serverSig, (0, escrow_template_1.hexToBytes)(voucher.voucherHex), (0, escrow_template_1.amountToLe8)(voucher.amountSompi));
    const signatureScript = (0, kaspa_1.payToScriptHashSignatureScript)(redeem, args);
    const transaction = { ...txShape, inputs: [{ ...base, signatureScript }] };
    const { transactionId } = await rpc.submitTransaction({ transaction, allowOrphan: false });
    return String(transactionId);
}
/**
 * Client-side: reclaim the full escrow balance after the timeout. The spending
 * transaction's lockTime must be >= the escrow timeout and the input must not
 * be finalized (sequence < max), per OP_CHECKLOCKTIMEVERIFY.
 */
async function refundEscrow(wallet, params, clientPrivateHex, destination, feeSompi) {
    const redeem = (0, escrow_template_1.buildEscrowRedeemScript)(params.clientPublic, params.serverPublic, params.timeout);
    const escrowSpk = (0, kaspa_1.payToScriptHashScript)(redeem);
    const utxo = await escrowUtxo(wallet, deriveEscrowAddress(params, wallet.networkId));
    const rpc = await wallet.client();
    const info = await rpc.getServerInfo();
    const daa = BigInt(info.virtualDaaScore);
    if (daa < params.timeout) {
        throw new Error(`refund unavailable until DAA ${params.timeout}; current ${daa}`);
    }
    const destSpk = (0, kaspa_1.payToAddressScript)(destination);
    let resolvedFee = feeSompi;
    if (resolvedFee === undefined) {
        const est = await rpc.getFeeEstimate();
        const feerate = est.estimate?.normalBuckets?.[0]?.feerate ?? 100;
        resolvedFee = (0, vault_1.estimateVaultSpendFeeSompi)(utxo.amount, [{ value: utxo.amount, spkScriptLen: String(destSpk.script).length / 2 }], redeem.length, feerate);
    }
    const outputs = [{ value: utxo.amount - resolvedFee, scriptPublicKey: destSpk }];
    if (outputs[0].value <= 0n)
        throw new Error("escrow UTXO too small to refund");
    // lockTime = timeout satisfies `tx.time >= timeout`; sequence 0 keeps the input non-final.
    const base = inputBase(utxo, escrowSpk, 0n, 1); // refund: single checkSig
    const txShape = { version: 0, outputs, lockTime: params.timeout, subnetworkId: SUBNETWORK_NATIVE, gas: 0n, payload: "" };
    const unsigned = new kaspa_1.Transaction({ ...txShape, inputs: [{ ...base, signatureScript: "" }] });
    const pushedSig = (0, kaspa_1.createInputSignature)(unsigned, 0, new kaspa_1.PrivateKey(clientPrivateHex), kaspa_1.SighashType.All);
    const clientSig = (0, escrow_template_1.hexToBytes)(pushedSig).slice(1);
    const signatureScript = (0, kaspa_1.payToScriptHashSignatureScript)(redeem, (0, escrow_template_1.buildRefundArgs)(clientSig));
    const transaction = { ...txShape, inputs: [{ ...base, signatureScript }] };
    const { transactionId } = await rpc.submitTransaction({ transaction, allowOrphan: false });
    return String(transactionId);
}
