"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.bytesToHex = exports.VaultManager = void 0;
exports.estimateVaultSpendFeeSompi = estimateVaultSpendFeeSompi;
exports.spendVault = spendVault;
exports.generateOwnerKey = generateOwnerKey;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const kaspa_1 = require("../vendor/kaspa-wasm/kaspa");
const template_1 = require("./vault/template");
Object.defineProperty(exports, "bytesToHex", { enumerable: true, get: function () { return template_1.bytesToHex; } });
const SUBNETWORK_NATIVE = "00".repeat(20);
const STORAGE_MASS_PARAMETER = 10n ** 12n; // KIP-9 C
const MASS_PER_SPK_BYTE = 10n;
const GRAMS_PER_SIGOP = 1000n;
/**
 * Deterministic fee estimate for a 1-input vault spend.
 *
 * Compute mass: serialized size + scriptPubKey bytes ×10 + one sigop;
 * storage mass (KIP-9): C·(Σ 1/out − 1/in). Priced at the node's normal
 * feerate with a small margin so cap-max sends don't bounce on rounding.
 */
function estimateVaultSpendFeeSompi(inputAmount, outputs, redeemScriptLen, feerate) {
    // serialized size (see consensus transaction_estimated_serialized_size)
    const sigScriptLen = BigInt(1 + 65 + 1 + 2 + redeemScriptLen); // push(sig65) + selector + OpPushData1 + redeem
    const inputSize = 32n + 4n + 8n + sigScriptLen + 8n;
    const outputsSize = outputs.reduce((acc, o) => acc + 8n + 2n + 8n + BigInt(o.spkScriptLen), 0n);
    const size = 2n + 8n + inputSize + 8n + outputsSize + 8n + 20n + 8n + 32n + 8n;
    const spkMass = outputs.reduce((acc, o) => acc + (2n + BigInt(o.spkScriptLen)) * MASS_PER_SPK_BYTE, 0n);
    const computeGrams = size + spkMass + GRAMS_PER_SIGOP;
    const harmonicOuts = outputs.reduce((acc, o) => acc + (o.value > 0n ? STORAGE_MASS_PARAMETER / o.value : 0n), 0n);
    const harmonicIn = STORAGE_MASS_PARAMETER / inputAmount;
    const storageGrams = harmonicOuts > harmonicIn ? harmonicOuts - harmonicIn : 0n;
    const transientGrams = size * 2n; // normalized post-Toccata transient component
    const grams = (computeGrams > transientGrams ? computeGrams : transientGrams) + storageGrams;
    const rate = BigInt(Math.max(Math.ceil(feerate), 100));
    return (grams * rate * 110n) / 100n; // +10% margin
}
class VaultManager {
    vaultDir;
    networkId;
    constructor(dataDir, networkId) {
        this.vaultDir = path.join(dataDir, "vault");
        this.networkId = networkId;
    }
    get configured() {
        return fs.existsSync(path.join(this.vaultDir, "config.json"));
    }
    config() {
        return JSON.parse(fs.readFileSync(path.join(this.vaultDir, "config.json"), "utf8"));
    }
    /** Derive the vault address from the template and persist config. One vault per data dir. */
    create(maxOutflowSompi, ownerPublicKey) {
        if (this.configured) {
            throw new Error(`vault already exists at ${this.vaultDir}; use it or move it aside first`);
        }
        const ownerPublic = ownerPublicKey.trim().toLowerCase();
        if (!/^[0-9a-f]{64}$/.test(ownerPublic)) {
            throw new Error("ownerPublicKey must be a 32-byte x-only public key in hex (64 hex chars). " +
                "Ask your human operator to run `npx @elldeeone/sompi gen-owner-key` on THEIR machine and " +
                "give you the `public:` line; the private half must stay with them.");
        }
        const agent = kaspa_1.Keypair.random();
        const agentPublic = String(agent.xOnlyPublicKey);
        const address = this.deriveAddress(agentPublic, ownerPublic, maxOutflowSompi);
        fs.mkdirSync(this.vaultDir, { recursive: true, mode: 0o700 });
        fs.writeFileSync(path.join(this.vaultDir, "agent-key"), agent.privateKey, { mode: 0o600 });
        const config = {
            template: template_1.VAULT_TEMPLATE_VERSION,
            agentPublic,
            ownerPublic,
            maxOutflowSompi: maxOutflowSompi.toString(),
            address,
        };
        fs.writeFileSync(path.join(this.vaultDir, "config.json"), JSON.stringify(config, null, 2), { mode: 0o600 });
        return config;
    }
    deriveAddress(agentPublic, ownerPublic, maxOutflowSompi) {
        const redeem = (0, template_1.buildRedeemScript)(agentPublic, ownerPublic, maxOutflowSompi);
        const spk = (0, kaspa_1.payToScriptHashScript)(redeem);
        const address = (0, kaspa_1.addressFromScriptPublicKey)(spk, this.networkId);
        if (!address)
            throw new Error("could not derive vault address");
        return address.toString();
    }
    async balanceSompi(wallet) {
        return wallet.balanceSompi(this.config().address);
    }
    /**
     * Withdraw via the consensus-capped agent path.
     *
     * `amount` may be the literal "max": the largest amount the covenant cap
     * (and UTXO) allows once the estimated fee is accounted for. `authorize`
     * runs against the resolved amount before anything is broadcast.
     */
    async send(wallet, destination, amount, authorize) {
        const config = this.config();
        const agentKey = fs.readFileSync(path.join(this.vaultDir, "agent-key"), "utf8").trim();
        return spendVault({
            wallet,
            config,
            fn: "withdraw",
            privateKey: agentKey,
            destination,
            amount,
            authorize,
        });
    }
    /** The owner key is not stored here by design; recovery runs on the operator's machine. */
    async recover() {
        throw new Error("the owner key is not stored on this host (by design). Recover from the operator's machine: " +
            "node scripts/vault-recover.js <ownerPrivateKey> <agentPublic> <maxOutflowSompi> <destination>");
    }
}
exports.VaultManager = VaultManager;
/** Minimum change kept in the vault on a withdrawal (avoids dust and KIP-9 storage-mass blowups). */
const MIN_VAULT_CHANGE_SOMPI = 100000000n;
/** Build, sign, and broadcast a vault spend. Shared by agent send and operator recovery. */
async function spendVault(params) {
    const { wallet, config, fn, destination } = params;
    const max = BigInt(config.maxOutflowSompi);
    const redeem = (0, template_1.buildRedeemScript)(config.agentPublic, config.ownerPublic, max);
    const vaultSpk = (0, kaspa_1.payToScriptHashScript)(redeem);
    const rpc = await wallet.client();
    const { entries } = await rpc.getUtxosByAddresses([config.address]);
    if (!entries.length)
        throw new Error(`vault ${config.address} has no UTXOs; fund it first`);
    const utxo = 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];
    const destSpk = (0, kaspa_1.payToAddressScript)(destination);
    const destSpkLen = String(destSpk.script).length / 2;
    const vaultSpkLen = String(vaultSpk.script).length / 2;
    const estimate = await rpc.getFeeEstimate();
    const feerate = estimate.estimate?.normalBuckets?.[0]?.feerate ?? 100;
    const estimateFee = (amountSompi, changeSompi) => {
        const shape = fn === "withdraw"
            ? [
                { value: amountSompi, spkScriptLen: destSpkLen },
                { value: changeSompi, spkScriptLen: vaultSpkLen },
            ]
            : [{ value: amountSompi, spkScriptLen: destSpkLen }];
        return estimateVaultSpendFeeSompi(utxo.amount, shape, redeem.length, feerate);
    };
    let amountSompi;
    let feeSompi;
    if (fn === "recover") {
        feeSompi = params.feeSompi ?? estimateFee(utxo.amount, 0n);
        amountSompi = utxo.amount - feeSompi;
    }
    else if (params.amount === "max") {
        // Largest withdrawal the cap and UTXO allow: iterate the fee estimate
        // twice since the fee depends on the output amounts.
        const outflowCap = utxo.amount - MIN_VAULT_CHANGE_SOMPI < max ? utxo.amount - MIN_VAULT_CHANGE_SOMPI : max;
        if (outflowCap <= 0n)
            throw new Error(`vault UTXO of ${utxo.amount} sompi is too small to withdraw from`);
        feeSompi = params.feeSompi ?? estimateFee(outflowCap, utxo.amount - outflowCap);
        amountSompi = outflowCap - feeSompi;
        feeSompi = params.feeSompi ?? estimateFee(amountSompi, utxo.amount - outflowCap);
        amountSompi = outflowCap - feeSompi;
    }
    else {
        amountSompi = params.amount;
        feeSompi = params.feeSompi ?? estimateFee(amountSompi, utxo.amount - amountSompi);
    }
    if (fn === "withdraw" && amountSompi + feeSompi > max) {
        throw new Error(`outflow ${amountSompi + feeSompi} sompi (amount + estimated fee ${feeSompi}) exceeds the vault's ` +
            `consensus cap of ${max} sompi — the network would reject it. ` +
            `Largest sendable amount right now: pass amountSompi "max" (≈ ${max - feeSompi} sompi).`);
    }
    params.authorize?.(amountSompi);
    const outputs = fn === "withdraw"
        ? [
            { value: amountSompi, scriptPublicKey: destSpk },
            { value: utxo.amount - amountSompi - feeSompi, scriptPublicKey: vaultSpk },
        ]
        : [{ value: amountSompi, scriptPublicKey: destSpk }];
    if (outputs.some((o) => o.value <= 0n)) {
        throw new Error(`vault UTXO of ${utxo.amount} sompi is too small for this spend`);
    }
    const inputBase = {
        previousOutpoint: { transactionId: utxo.txid, index: utxo.index },
        sequence: 0n,
        sigOpCount: 1, // each vault path executes exactly one OpCheckSig (runtime-counted post-Toccata)
        utxo: {
            outpoint: { transactionId: utxo.txid, index: utxo.index },
            amount: utxo.amount,
            scriptPublicKey: vaultSpk,
            blockDaaScore: 0n,
            isCoinbase: false,
        },
    };
    const txShape = {
        version: 0,
        outputs,
        lockTime: 0n,
        subnetworkId: SUBNETWORK_NATIVE,
        gas: 0n,
        payload: "",
    };
    // Pass 1: signature over the populated transaction (sighash excludes signature scripts).
    const unsigned = new kaspa_1.Transaction({ ...txShape, inputs: [{ ...inputBase, signatureScript: "" }] });
    const pushedSig = (0, kaspa_1.createInputSignature)(unsigned, 0, new kaspa_1.PrivateKey(params.privateKey), kaspa_1.SighashType.All);
    const rawSig = (0, template_1.hexToBytes)(pushedSig).slice(1); // strip the push-65 opcode prefix
    // Pass 2: assemble the signature script and broadcast.
    const signatureScript = (0, kaspa_1.payToScriptHashSignatureScript)(redeem, (0, template_1.buildSigArgs)(rawSig, fn));
    const transaction = { ...txShape, inputs: [{ ...inputBase, signatureScript }] };
    const { transactionId } = await rpc.submitTransaction({ transaction, allowOrphan: false });
    return { txid: String(transactionId), amountSompi, feeSompi };
}
function generateOwnerKey() {
    const kp = kaspa_1.Keypair.random();
    return { privateKey: kp.privateKey, publicKey: String(kp.xOnlyPublicKey) };
}
