// Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
// ══════════════════════════════════════════════════════════════════
// x402 payment rail — public reference adapter (Base + USDC)
// ══════════════════════════════════════════════════════════════════
// Implements PaymentRail over the x402 v1 protocol on Base mainnet
// with USDC as the asset. Settlement runs through a caller-supplied
// facilitator (Coinbase's public CDP facilitator at
// https://api.cdp.coinbase.com/x402 by default, or any compatible
// implementation).
//
// Flow:
//   1. createInvoice(opts) builds an X402PaymentRequirements suitable
//      for the resource server's 402 response body and caches it.
//   2. The resource server returns the requirements to the client.
//   3. The client signs an EIP-3009 transferWithAuthorization, sends
//      back the X-PAYMENT header carrying an X402PaymentPayload.
//   4. The resource server passes (invoice_id, payload) to
//      submitPayment(); the rail calls facilitator /verify, then
//      /settle, then resolves the X402SubmitOutcome.
//   5. Caller emits a signed APS PaymentReceipt with tx_proof set to
//      X402SubmitOutcome.transaction (the on-chain tx hash). Failure
//      paths emit a signed PaymentDenial with denial_reason 'rail_error'
//      and reason_detail carrying the facilitator's invalidReason or
//      errorReason.
//
// Rail does NOT:
//   - Sign EIP-3009 authorizations (sender does that off-chain)
//   - Submit on-chain transactions directly (facilitator does at /settle)
//   - Send outbound payments (sendPayment throws UnsupportedOperation —
//     x402 is a pull protocol from the resource server's perspective)
//   - Implement the HTTP transport (X-PAYMENT header encoding, base64
//     framing). Resource servers wire that themselves; the rail
//     produces and consumes the schema-typed bodies.
// ══════════════════════════════════════════════════════════════════
import { randomUUID } from 'node:crypto';
import { X402_VERSION, } from './types.js';
// ── Rail ──────────────────────────────────────────────────────────
export class X402PaymentRail {
    name;
    currency;
    config;
    invoices = new Map();
    /** Tx hash → invoice id, populated on settle. Lets verifyTransaction
     *  resolve a previously-settled invoice from a tx hash. */
    txIndex = new Map();
    revokedWallets = new Set();
    constructor(config) {
        this.config = config;
        this.currency = config.currency ?? 'USDC';
        this.name = config.name ?? `x402-${config.network}-${this.currency}`;
    }
    // ── PaymentRail surface ─────────────────────────────────────────
    async createInvoice(opts) {
        const scheme = this.config.scheme ?? 'exact';
        const decimals = this.config.assetDecimals ?? 6;
        const maxTimeoutSeconds = opts.expires_in_seconds ?? this.config.defaultMaxTimeoutSeconds ?? 60;
        const requirements = {
            scheme,
            network: this.config.network,
            maxAmountRequired: opts.amount_base_units,
            asset: this.config.asset,
            payTo: this.config.payTo,
            resource: this.config.resource,
            description: this.config.description ?? this.config.resource,
            maxTimeoutSeconds,
        };
        if (this.config.extra !== undefined) {
            requirements.extra = this.config.extra;
        }
        const invoiceId = randomUUID();
        const now = new Date();
        const expiresAt = new Date(now.getTime() + maxTimeoutSeconds * 1000).toISOString();
        const metadata = {
            x402_requirements: requirements,
            x402_version: X402_VERSION,
        };
        if (opts.settlement_id !== undefined)
            metadata.settlement_id = opts.settlement_id;
        if (opts.agent_id !== undefined)
            metadata.agent_id = opts.agent_id;
        const invoice = {
            invoice_id: invoiceId,
            rail_name: this.name,
            amount_base_units: opts.amount_base_units,
            amount_human: `${_formatAtomic(opts.amount_base_units, decimals)} ${this.currency}`,
            currency: this.currency,
            destination: this.config.payTo,
            status: 'pending',
            created_at: now.toISOString(),
            expires_at: expiresAt,
            metadata,
        };
        if (opts.memo !== undefined)
            invoice.memo = opts.memo;
        this.invoices.set(invoiceId, { ...invoice, requirements });
        return invoice;
    }
    async checkStatus(invoiceId) {
        const cached = this.invoices.get(invoiceId);
        if (cached === undefined) {
            throw new Error(`invoice ${invoiceId} not found in adapter cache`);
        }
        if (cached.status === 'pending' && cached.expires_at !== undefined) {
            if (new Date(cached.expires_at) < new Date()) {
                cached.status = 'expired';
                this.invoices.set(invoiceId, cached);
            }
        }
        return _stripInternal(cached);
    }
    async verifyTransaction(txProof, expectedAmountBaseUnits) {
        // x402's facilitator /verify takes a PaymentPayload, not a tx hash;
        // the on-chain transaction itself comes from /settle. Verification
        // by tx hash here resolves the cached settled invoice and confirms
        // the tx hash matches.
        const invoiceId = this.txIndex.get(txProof);
        if (invoiceId === undefined) {
            return {
                verified: false,
                amount_base_units: '0',
                error: `tx ${txProof} not associated with any settled invoice on this rail`,
            };
        }
        const cached = this.invoices.get(invoiceId);
        if (cached === undefined || cached.settled === undefined) {
            return {
                verified: false,
                amount_base_units: '0',
                error: `invoice ${invoiceId} no longer cached or never settled`,
            };
        }
        let amountMatches = true;
        if (expectedAmountBaseUnits !== undefined) {
            try {
                amountMatches =
                    BigInt(cached.amount_base_units) === BigInt(expectedAmountBaseUnits);
            }
            catch {
                amountMatches = false;
            }
        }
        return {
            verified: amountMatches && cached.settled.success,
            amount_base_units: cached.amount_base_units,
            sender: cached.settled.payer,
            receiver: cached.requirements.payTo,
            timestamp: cached.created_at,
        };
    }
    async revokeWallet(walletId) {
        this.revokedWallets.add(walletId);
        return true;
    }
    isWalletRevoked(walletId) {
        return this.revokedWallets.has(walletId);
    }
    // ── x402-specific extras ────────────────────────────────────────
    /** x402 is a pull protocol from the resource server's perspective:
     *  the server collects payment by serving 402 + requirements, then
     *  settling the client's signed authorization. There is no outbound
     *  send. Callers that try this on x402 are misusing the rail. */
    async sendPayment(_opts) {
        throw new Error('x402 rail does not support sendPayment: the protocol settles ' +
            'client-signed EIP-3009 authorizations through the facilitator ' +
            '(pull). Use submitPayment(invoiceId, payload) on the resource ' +
            'server when an X-PAYMENT header arrives.');
    }
    /**
     * Drive verify-then-settle for a PaymentPayload that arrived on
     * the resource server (decoded from X-PAYMENT). On success, the
     * cached invoice flips to 'confirmed' with the on-chain tx hash
     * recorded; the caller passes outcome.transaction to emitReceipt
     * as tx_proof. On verification failure, the invoice flips to
     * 'failed' and outcome.invalidReason carries the facilitator's
     * reason; caller passes that to emitDenial with denial_reason
     * 'rail_error'.
     *
     * The rail does not sign or emit APS receipts itself — that stays
     * the caller's responsibility, so the issuer key never leaves the
     * caller's process.
     */
    async submitPayment(invoiceId, payload) {
        const cached = this.invoices.get(invoiceId);
        if (cached === undefined) {
            throw new Error(`invoice ${invoiceId} not found in adapter cache`);
        }
        if (cached.status !== 'pending') {
            throw new Error(`invoice ${invoiceId} not pending (status=${cached.status}); ` +
                'submitPayment expects a fresh invoice');
        }
        // Verify
        const verifyReq = {
            x402Version: X402_VERSION,
            paymentPayload: payload,
            paymentRequirements: cached.requirements,
        };
        let verifyResp;
        try {
            verifyResp = await this.config.facilitatorVerify(verifyReq);
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            cached.status = 'failed';
            cached.receivedPayload = payload;
            this.invoices.set(invoiceId, cached);
            return {
                verified: false,
                invalidReason: `facilitator_verify_threw: ${msg}`,
            };
        }
        if (!verifyResp.isValid) {
            cached.status = 'failed';
            cached.receivedPayload = payload;
            this.invoices.set(invoiceId, cached);
            const out = {
                verified: false,
                invalidReason: verifyResp.invalidReason ?? 'invalid_payment_payload',
            };
            if (verifyResp.payer !== undefined)
                out.payer = verifyResp.payer;
            return out;
        }
        // Settle
        const settleReq = {
            x402Version: X402_VERSION,
            paymentPayload: payload,
            paymentRequirements: cached.requirements,
        };
        let settleResp;
        try {
            settleResp = await this.config.facilitatorSettle(settleReq);
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            cached.status = 'failed';
            cached.receivedPayload = payload;
            this.invoices.set(invoiceId, cached);
            const out = {
                verified: true,
                settled: false,
                settleErrorReason: `facilitator_settle_threw: ${msg}`,
            };
            if (verifyResp.payer !== undefined)
                out.payer = verifyResp.payer;
            return out;
        }
        if (!settleResp.success) {
            cached.status = 'failed';
            cached.receivedPayload = payload;
            this.invoices.set(invoiceId, cached);
            const out = {
                verified: true,
                settled: false,
                settleErrorReason: settleResp.errorReason ?? 'settlement_failed',
                payer: settleResp.payer,
            };
            return out;
        }
        // Confirmed
        cached.status = 'confirmed';
        cached.receivedPayload = payload;
        cached.settled = settleResp;
        cached.metadata = {
            ...cached.metadata,
            tx_hash: settleResp.transaction,
            payer: settleResp.payer,
            confirmed_at: new Date().toISOString(),
        };
        this.invoices.set(invoiceId, cached);
        if (settleResp.transaction !== '') {
            this.txIndex.set(settleResp.transaction, invoiceId);
        }
        return {
            verified: true,
            settled: true,
            transaction: settleResp.transaction,
            payer: settleResp.payer,
        };
    }
    /** Returns the X402PaymentRequirements cached for an invoice. Used
     *  by the resource server to assemble the 402 response body. */
    getRequirements(invoiceId) {
        return this.invoices.get(invoiceId)?.requirements;
    }
}
// ── Helpers ───────────────────────────────────────────────────────
function _stripInternal(c) {
    const { requirements: _r, receivedPayload: _p, settled: _s, ...rest } = c;
    return rest;
}
/** Format an atomic-units string as a decimal string with `decimals`
 *  fractional digits. Trailing zeros stripped. Used only for human
 *  display in PaymentInvoice.amount_human. */
function _formatAtomic(atomic, decimals) {
    if (decimals <= 0)
        return atomic;
    const big = BigInt(atomic);
    const div = BigInt(10) ** BigInt(decimals);
    const whole = big / div;
    const frac = big % div;
    if (frac === 0n)
        return whole.toString();
    const fracStr = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
    return `${whole}.${fracStr}`;
}
// ── Factory ───────────────────────────────────────────────────────
export function createX402Rail(config) {
    return new X402PaymentRail(config);
}
/** Default Coinbase CDP facilitator endpoint. The free public tier
 *  supports verification on the EIP-3009 USDC scheme without an API
 *  key (subject to Coinbase's published rate limits, currently
 *  ~1K tx/mo). For higher throughput or non-USDC assets a CDP API key
 *  is required and the caller should wire authorization headers
 *  inside their FacilitatorVerify / FacilitatorSettle implementations.
 *  The rail accepts any compatible facilitator URL — pin via the
 *  closure, do not embed in the rail. */
export const DEFAULT_FACILITATOR_URL = 'https://api.cdp.coinbase.com/x402';
/** USDC contract on Base mainnet (eip155:8453). */
export const USDC_BASE_MAINNET = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
/** USDC contract on Base Sepolia. */
export const USDC_BASE_SEPOLIA = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
//# sourceMappingURL=index.js.map