"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.TabServer = void 0;
const crypto = __importStar(require("node:crypto"));
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const kaspa_1 = require("../../vendor/kaspa-wasm/kaspa");
const types_1 = require("./types");
/**
 * Framework-agnostic payment-tab engine.
 *
 * Deposits are verified by querying the tab address's UTXOs at request time
 * (a deposit confirms in ~1 second on Kaspa, so the client's first retry
 * normally succeeds). Charges are an in-process ledger persisted to disk.
 */
class TabServer {
    config;
    tabsPath;
    tabs = new Map();
    /** Cache of confirmed deposits to limit RPC chatter (tabId -> [sompi, fetchedMs]). */
    depositCache = new Map();
    static DEPOSIT_CACHE_MS = 2_000;
    constructor(config) {
        this.config = config;
        fs.mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
        this.tabsPath = path.join(config.dataDir, "tabs.json");
        if (fs.existsSync(this.tabsPath)) {
            for (const t of JSON.parse(fs.readFileSync(this.tabsPath, "utf8"))) {
                this.tabs.set(t.tabId, t);
            }
        }
    }
    /** Total confirmed sompi deposited to the tab's address. */
    async depositedSompi(tab) {
        const cached = this.depositCache.get(tab.tabId);
        if (cached && Date.now() - cached[1] < TabServer.DEPOSIT_CACHE_MS)
            return cached[0];
        const rpc = await this.config.rpc();
        const { entries } = await rpc.getUtxosByAddresses([tab.depositAddress]);
        const total = entries.reduce((acc, e) => acc + BigInt(e?.amount ?? e?.entry?.amount ?? 0), 0n);
        this.depositCache.set(tab.tabId, [total, Date.now()]);
        return total;
    }
    newTab() {
        const keypair = kaspa_1.Keypair.random();
        const tab = {
            tabId: crypto.randomUUID(),
            depositPrivateKey: keypair.privateKey,
            depositAddress: keypair.toAddress(this.config.networkId).toString(),
            chargedSompi: "0",
            createdMs: Date.now(),
        };
        this.tabs.set(tab.tabId, tab);
        this.persist();
        return tab;
    }
    offer(tab) {
        const offer = {
            scheme: "kaspa-tab",
            network: this.config.networkId,
            payTo: tab.depositAddress,
            minDepositSompi: this.config.minDepositSompi.toString(),
            pricePerRequestSompi: this.config.pricePerRequestSompi.toString(),
            tabId: tab.tabId,
            description: this.config.description,
        };
        return { x402Version: 1, accepts: [offer] };
    }
    /**
     * Charge one request against the tab identified by the X-Payment header.
     * Returns a 402 offer when there is no header, an unknown tab, or
     * insufficient credit.
     */
    async charge(xPaymentHeader) {
        let tab;
        if (xPaymentHeader) {
            try {
                tab = this.tabs.get((0, types_1.decodePaymentHeader)(xPaymentHeader).tabId);
            }
            catch {
                tab = undefined;
            }
        }
        if (!tab) {
            return { ok: false, paymentRequired: this.offer(this.newTab()) };
        }
        const deposited = await this.depositedSompi(tab);
        const charged = BigInt(tab.chargedSompi);
        const price = this.config.pricePerRequestSompi;
        if (deposited === 0n || deposited - charged < price) {
            // Tab exists but has no usable credit: re-offer the same deposit address.
            return { ok: false, paymentRequired: this.offer(tab), tabId: tab.tabId };
        }
        tab.chargedSompi = (charged + price).toString();
        this.persist();
        return { ok: true, remainingSompi: deposited - charged - price, tabId: tab.tabId };
    }
    /** Node `http` adapter: returns true when the request was already answered with a 402. */
    async gate(req, res) {
        const header = req.headers[types_1.X_PAYMENT_HEADER];
        const result = await this.charge(typeof header === "string" ? header : undefined);
        if (result.ok) {
            res.setHeader("x-payment-remaining-sompi", result.remainingSompi.toString());
            return false;
        }
        res.statusCode = 402;
        res.setHeader("content-type", "application/json");
        res.end(JSON.stringify(result.paymentRequired));
        return true;
    }
    persist() {
        fs.writeFileSync(this.tabsPath, JSON.stringify([...this.tabs.values()]), { mode: 0o600 });
    }
    /**
     * Sweep tab deposits to `destination`.
     *
     * By default only exhausted tabs (remaining credit below one request price)
     * are swept — unspent credit belongs to the client. Pass
     * `includeUnspent: true` to take everything (e.g. when decommissioning).
     */
    async sweep(destination, includeUnspent = false) {
        const rpc = await this.config.rpc();
        const results = [];
        for (const tab of this.tabs.values()) {
            const { entries } = await rpc.getUtxosByAddresses([tab.depositAddress]);
            if (!entries.length)
                continue;
            const deposited = entries.reduce((acc, e) => acc + BigInt(e?.amount ?? e?.entry?.amount ?? 0), 0n);
            const remaining = deposited - BigInt(tab.chargedSompi);
            if (!includeUnspent && remaining >= this.config.pricePerRequestSompi)
                continue;
            const estimate = await rpc.getFeeEstimate();
            const feerate = estimate.estimate?.normalBuckets?.[0]?.feerate ?? 1;
            const { transactions } = await (0, kaspa_1.createTransactions)({
                entries,
                outputs: [],
                changeAddress: destination,
                feeRate: feerate,
                priorityFee: 0n,
                networkId: this.config.networkId,
            });
            const key = new kaspa_1.PrivateKey(tab.depositPrivateKey);
            let txid = "";
            for (const pending of transactions) {
                await pending.sign([key]);
                txid = await pending.submit(rpc);
            }
            this.depositCache.delete(tab.tabId);
            results.push({ tabId: tab.tabId, txid, sweptSompi: deposited.toString() });
        }
        return results;
    }
}
exports.TabServer = TabServer;
