import { W as WalletRpcClient } from './wallet-rpc-sY1qJ24S.js'; import { DaemonRpcClient } from './rpc/index.js'; import { E as EscrowClaimGuard, d as EscrowInventoryStore, g as EscrowKeeperOptions, h as EscrowManager } from './manager-BG_G7s9X.js'; import { i as WebhookEventType, P as Payment, I as Invoice, f as InvoiceStatus, a as DeroPayConfig, b as CreateInvoiceParams } from './types-Bsy2LUTI.js'; /** * Durable webhook outbox types. * * The outbox is the at-least-once, restart-survivable spine of the DeroPay * Bridge: every payment/invoice event is committed to a SQLite row IN THE SAME * TRANSACTION as the invoice state change, then delivered by a separate worker. * A crash between "state changed" and "merchant notified" can therefore never * lose a notification — the row is already on disk. */ type OutboxStatus = "pending" | "delivering" | "delivered" | "dead"; /** * One durable outbox row. * * `id` is the deterministic delivery id (see {@link deriveDeliveryId}); it is * BOTH the primary key (so a replayed logical event collapses) AND the * `X-DeroPay-Delivery` header the merchant dedupes on. * * `payload` is the FROZEN signed JSON of the event, serialized (bigint->string) * at enqueue time INSIDE the enqueue transaction. Re-signing these exact bytes * yields an identical signature on every replay, so a redelivery after restart * is byte-identical to the first attempt. */ type OutboxRecord = { id: string; eventType: WebhookEventType; invoiceId: string; /** Frozen signed JSON (bigint already stringified). */ payload: string; status: OutboxStatus; attempts: number; /** Epoch ms; when this row is next eligible to be claimed. Durable backoff. */ nextAttemptAt: number; /** Epoch ms; a delivering row's claim lease. A stale lease is reclaimable. */ leaseUntil: number; lastError: string | null; createdAt: number; deliveredAt: number | null; }; /** What the engine hands the store to enqueue, before durable fields are set. */ type OutboxEvent = { id: string; eventType: WebhookEventType; invoiceId: string; payload: string; }; /** * The seam the InvoiceEngine writes through when a sink is configured. * * The engine NEVER builds the durable row itself — it describes the state * transition, and the sink (a) derives the deterministic id, (b) freezes + * signs the payload, and (c) commits the invoice mutation AND the outbox row in * ONE transaction via the store's combined methods. This keeps the single * writer of `amount_received` and the single point of id derivation in one * place. When no sink is configured the engine's default path is byte-identical * to today (the 223-test regression gate). */ type WebhookSink = { /** * A payment arrived (new txid). The sink OWNS the write: it re-sums in-tx * (single writer of amount_received), decides the resulting status from the * committed total, and enqueues the matching event(s). The engine must not * separately store.addPayment when a sink is present. */ onPaymentDetected(invoiceId: string, payment: Payment): Promise; /** * A confirmation-depth crossing. The sink re-reads store-authoritative * totals and enqueues completion/confirming on the confirmation edge. */ onPaymentConfirmed(invoiceId: string, txid: string): Promise; /** A terminal expiry decided by the engine/sweep (payment-aware). */ onInvoiceExpired(invoiceId: string): Promise; }; /** * Pluggable storage interface for DeroPay. * * Implement this interface to use your own storage backend. * In-memory and SQLite implementations are provided. */ /** Filter options for querying invoices */ type InvoiceFilter = { /** Filter by status */ status?: InvoiceStatus | InvoiceStatus[]; /** Filter invoices created after this date */ createdAfter?: Date; /** Filter invoices created before this date */ createdBefore?: Date; /** Limit results */ limit?: number; /** Offset for pagination */ offset?: number; }; /** * O10 — optional compare-and-set precondition for {@link InvoiceStore.updateInvoice}. * When present the escrow write applies atomically ONLY if the row's current * escrow blob still matches these fields, guarding the arbiter blob against a * concurrent whole-blob lost update. */ type UpdateInvoiceOpts = { expectedEscrow?: { /** The escrowId the caller read before mutating. `null` matches an absent escrow. */ escrowId: string | null; /** The escrowStatus the caller read before mutating. */ escrowStatus: string; }; }; /** Invoice store summary stats */ type InvoiceStats = { total: number; created: number; pending: number; confirming: number; completed: number; expired: number; partial: number; misrouted_to_base: number; escrow_funded: number; /** O19 — dispute in flight (escrow funded, awaiting arbitration). */ disputed: number; /** O19 — buyer refunded (chargeback-equivalent); distinct from expired. */ refunded: number; totalAmountReceived: bigint; }; type X402UsageReservation = { resource: string; windowKey: string; windowStart: string; windowEnd: string; amountAtomic: bigint; maxReceipts?: number; maxAmountAtomic?: bigint; }; type X402UsageReservationResult = { allowed: boolean; receiptCount: number; totalAmountAtomic: bigint; }; type X402UsageBatchReservationResult = { allowed: boolean; results: X402UsageReservationResult[]; }; /** Public shareable link that creates invoices on demand. */ type PaymentLink = { id: string; slug: string; productId?: string | null; name: string; description?: string | null; amountAtomic?: string | null; currency?: "DERO" | null; ttlSeconds: number; usedCount?: number; usesCount: number; usageLimit?: number | null; maxUses?: number | null; invoiceTemplateId?: string | null; expiresAt?: number | null; redirectUrl?: string | null; revokedAt?: number | null; createdAt: number; archivedAt?: number | null; metadata?: Record; }; type PaymentLinkStats = { linkId: string; views: number; invoiceStarts: number; paidInvoices: number; conversionRate: number; }; type CreatePaymentLinkArgs = { slug?: string; name: string; description?: string; productId?: string; amountAtomic?: bigint; currency?: "DERO"; ttlSeconds?: number; usageLimit?: number; maxUses?: number; invoiceTemplateId?: string; expiresAt?: number; redirectUrl?: string; metadata?: Record; }; /** * Storage interface for invoices and payments. * * All methods that write data should be atomic where possible. */ type InvoiceStore = { /** * Save a new invoice. */ createInvoice(invoice: Invoice): Promise; /** * Get an invoice by its ID. * @returns The invoice or null if not found */ getInvoice(id: string): Promise; /** * Get an invoice by its payment ID. * @returns The invoice or null if not found */ getInvoiceByPaymentId(paymentId: bigint): Promise; /** * O20 — get the invoice whose escrow binding currently references `scid`. Same * motivation as getInvoiceByEscrowId: the escrow lifecycle handlers * (requoteCancelledEscrow, handleEscrowFundingMismatch) fire per on-chain event * and previously did a full-table scan + `.find()` per event. A targeted lookup * removes that O(N)-per-event cost. Optional; callers fall back to a scan. */ getInvoiceByScid?(scid: string): Promise; /** * Update an invoice's status and related fields. * * O10 — the escrow blob is a whole-column read-modify-write, so two concurrent * writers (claim-success vs a lifecycle/requote handler) can clobber each * other's escrow transition. For writes where the invoice blob is the ARBITER * of the claim outcome, pass `opts.expectedEscrow` to make the escrow write a * compare-and-set: the store applies it atomically ONLY if the row's current * escrow blob still matches the expected `escrowId`/`escrowStatus`, and returns * `false` on a precondition miss so the caller can abort/re-read instead of * silently winning a lost-update race. Without `opts` the call is an * unconditional write (backward compatible; return value is `true`). */ updateInvoice(id: string, updates: Partial>, opts?: UpdateInvoiceOpts): Promise; /** * Add a payment to an invoice. */ addPayment(invoiceId: string, payment: Payment): Promise; /** * Update a payment's confirmation count and status. */ updatePayment(invoiceId: string, txid: string, updates: Partial>): Promise; /** * List invoices with optional filters. */ listInvoices(filter?: InvoiceFilter): Promise; /** * Get all active (non-terminal) invoices. * Returns invoices in created, pending, confirming, or partial states. */ getActiveInvoices(): Promise; /** * Get summary statistics. */ getStats(): Promise; /** * Mark a receipt JTI as used for replay protection. * Returns true when this is the first use, false when already used. * * Implementations should expire entries at or after `expiresAt`. */ markReceiptJtiUsed?(jti: string, expiresAt: string): Promise; /** * Atomically check and reserve x402 route usage against a quota window. * Returns the resulting counters whether or not the reservation was allowed. */ reserveX402Usage?(reservation: X402UsageReservation): Promise; /** * Atomically check and reserve multiple x402 quota windows together. */ reserveX402UsageBatch?(reservations: X402UsageReservation[]): Promise; createPaymentLink?(args: CreatePaymentLinkArgs): PaymentLink; listPaymentLinks?(filter?: { includeArchived?: boolean; includeRevoked?: boolean; limit?: number; }): PaymentLink[]; getPaymentLink?(id: string): PaymentLink | null; getPaymentLinkBySlug?(slug: string): PaymentLink | null; updatePaymentLink?(id: string, patch: { name?: string; description?: string | null; amountAtomic?: bigint | null; usageLimit?: number | null; expiresAt?: number | null; redirectUrl?: string | null; metadata?: Record; invoiceTemplateId?: string | null; }): PaymentLink; revokePaymentLink?(id: string): PaymentLink; incrementPaymentLinkUses?(id: string): PaymentLink; recordPaymentLinkView?(idOrSlug: string): PaymentLinkStats | null; getPaymentLinkStats?(id: string): PaymentLinkStats; applyPaymentWithOutbox?(invoiceId: string, payment: Payment, buildEvent: (committedTotal: bigint, invoice: Invoice) => OutboxEvent | null): { invoice: Invoice; total: bigint; }; applyInvoiceUpdateWithOutbox?(invoiceId: string, updates: Partial>, buildEvent: (invoice: Invoice) => OutboxEvent | null): { invoice: Invoice; }; claimDueOutbox?(now: number, leaseMs: number, limit: number): Promise; markOutboxDelivered?(id: string, deliveredAt: number): Promise; rescheduleOutbox?(id: string, nextAttemptAt: number, lastError: string): Promise; markOutboxDead?(id: string, lastError: string): Promise; pruneDeliveredOutbox?(olderThan: number): Promise; countOutboxByStatus?(): Promise>; getOutboxRecord?(id: string): Promise; /** * Create a durable claim guard for the escrow quote->claim transition, if the * backend supports one. The engine injects it into the EscrowManager so a * multi-process server cannot double-claim (and double-deploy) a quote. */ createClaimGuard?(): EscrowClaimGuard; /** * Create a durable inventory store for the PREMINT keeper pool, if the backend * supports one. The engine injects it into the EscrowManager so pre-minted empty * boxes survive restarts and a multi-process server cannot hand the same pooled * box to two checkouts. Must share the same storage (db file) as createClaimGuard * so pool pops are atomic across workers. */ createInventoryStore?(): EscrowInventoryStore; /** * Close the store and release any resources. */ close(): Promise; }; /** * Invoice engine — the central orchestrator for DeroPay. * * Ties together invoice creation, payment monitoring, storage, * and webhook notifications into a single cohesive service. * * Usage: * ```ts * const engine = new InvoiceEngine({ * walletRpcUrl: "http://127.0.0.1:10103/json_rpc", * daemonRpcUrl: "http://127.0.0.1:10102/json_rpc", * webhookUrl: "https://mystore.com/webhooks/dero", * webhookSecret: process.env.WEBHOOK_SECRET!, * }); * * await engine.start(); * * const invoice = await engine.createInvoice({ * name: "Widget", * amount: deroToAtomic("5.0"), * }); * * // Invoice is now being monitored for payments * ``` */ type X402AuditEventType = "x402.challenge_issued" | "x402.receipt_issued" | "x402.receipt_used" | "x402.receipt_rejected"; type X402AuditEvent = { type: X402AuditEventType; timestamp: string; resource?: string; invoiceId?: string; jti?: string; reason?: string; metadata?: Record; }; /** Events emitted by the invoice engine */ type InvoiceEngineEvents = { /** Invoice status changed */ invoiceStatusChanged: (invoice: Invoice, previousStatus: InvoiceStatus) => void; /** New payment detected */ paymentDetected: (invoice: Invoice, payment: Payment) => void; /** Payment confirmed */ paymentConfirmed: (invoice: Invoice, payment: Payment) => void; /** x402 payment lifecycle audit event */ x402Audit: (event: X402AuditEvent) => void; /** Error occurred */ error: (error: Error) => void; }; /** * The main DeroPay invoice engine. * * Manages the full lifecycle of payment invoices: * 1. Creates invoices with unique integrated addresses * 2. Monitors the blockchain for matching payments * 3. Tracks confirmation depth * 4. Fires webhooks on state changes * 5. Persists everything to the configured store */ declare class InvoiceEngine { private walletRpc; private daemonRpc; private monitor; private store; private webhook; private webhookSink; private escrowManager; private config; private baseAddress; private isStarted; /** O4 — set when the caller declares a multi-process deployment; drives the * durable-claim-guard startup assertion. */ private multiProcess; private expiryTimer; /** O15c — re-entrancy guard so a slow reconcile pass never stacks on the next * tick of the engine's expiry timer (the reconciler is idempotent via its CAS * gates, but overlapping passes would waste RPC and could interleave releases). */ private reconcileInFlight; private listeners; constructor(options: DeroPayConfig & { /** Custom store implementation (default: MemoryInvoiceStore) */ store?: InvoiceStore; /** Default escrow fee in basis points (default: 250 = 2.5%) */ escrowFeeBasisPoints?: number; /** Default block expiration for escrow (default: 9600 ~= 2 days at ~18s/block; must be >= 4000) */ escrowBlockExpiration?: number; /** Enable escrow support (default: false — must be explicitly opted into) */ enableEscrow?: boolean; /** * Enable the PREMINT keeper (default: false). When true (and escrow is * enabled and the store provides createInventoryStore), a background loop * pre-mints a small pool of empty escrow boxes so checkout only has to Bind, * moving the ~1-block mint-confirm latency off the payment path. Opt-in * because pre-minting spends deploy gas ahead of demand; checkout still works * without it (mint-on-demand fallback). */ enableEscrowKeeper?: boolean; /** Keeper pool tuning: how many confirmed boxes to keep ready, the low-water * refill trigger, and the poll interval. Defaults: 5 / 2 / 10000ms. */ escrowKeeperOptions?: Partial; /** * O4 — declare that this engine runs in a MULTI-PROCESS deployment * (cluster / PM2 / multiple pods sharing one store). When true and escrow * is enabled, start() HARD-FAILS unless the store provides a DURABLE * (cross-process) claim guard. This turns the silent fail-open — a * clustered server on the memory store, or any store whose createClaimGuard * yields a process-local guard, gets N independent guards and N deploys — * into a loud startup error. Default false (single-process); a single * process is safe with the in-memory guard. */ multiProcess?: boolean; /** * Max number of AUTOMATIC re-quotes a single invoice may receive after * CancelUnfunded races (O13 amplifier guard). Once hit, the invoice is * parked in a terminal alert state instead of looping. Default: 3. */ escrowMaxAutoRequotes?: number; /** * Minimum wall-clock gap between automatic re-quotes of the same invoice * (O13 amplifier guard). Requote events arriving sooner are dropped. * Default: 60_000 ms. */ escrowRequoteCooldownMs?: number; /** Inject RPC clients (for testing); when set, walletRpcUrl/daemonRpcUrl are ignored */ walletRpc?: WalletRpcClient; daemonRpc?: DaemonRpcClient; /** * Opt-in durable webhook sink (the DeroPay Bridge). When provided, every * state-changing payment/invoice transition is routed through the sink's * transactional, deterministic-id, durable-outbox path instead of the * in-memory WebhookDispatcher. When omitted, the engine behaves exactly as * before (the default no-sink path is byte-identical — the regression gate). */ webhookSink?: WebhookSink; }); /** Register an event listener */ on(event: K, callback: InvoiceEngineEvents[K]): () => void; private emit; /** * Start the invoice engine. * * Verifies wallet/daemon connectivity, retrieves the wallet address, * resumes tracking active invoices, and starts the payment monitor. */ start(): Promise; /** * Stop the invoice engine. */ stop(): Promise; /** * Shut down the engine and close the store. */ shutdown(): Promise; /** * Create a new payment invoice. * * Generates a unique payment ID, creates an integrated address, * saves the invoice to the store, and starts monitoring for payments. */ createInvoice(params: CreateInvoiceParams): Promise; /** * Get an invoice by ID. */ getInvoice(id: string): Promise; /** * Get an invoice by payment ID. */ getInvoiceByPaymentId(paymentId: bigint): Promise; /** * List invoices with optional filters. */ listInvoices(filter?: InvoiceFilter): Promise; /** * Get invoice statistics. */ getStats(): Promise; /** * Get the underlying invoice store. */ getStore(): InvoiceStore; /** * Emit a structured x402 audit event for observability and monitoring. */ emitX402AuditEvent(event: Omit & { timestamp?: string; }): void; /** * Get the wallet's base address. */ getBaseAddress(): string | null; /** * Get the wallet balance. */ getBalance(): Promise<{ balance: bigint; unlockedBalance: bigint; }>; /** * Check if the engine is running. */ get running(): boolean; /** * Wire up payment monitor events to update store and fire webhooks. */ private setupMonitorEvents; /** * Calculate the new invoice status based on a payment. */ private calculateInvoiceStatus; /** * True once an invoice's escrow has left the pre-deploy stage — i.e. a * contract exists on-chain (awaiting_deposit) or funds are locked/settling. * Such invoices must NOT be expired on the integrated-address TTL clock; they * settle on the escrow contract's own on-chain window. A still-quoted (or * deploy_failed / cancelled) escrow has no on-chain funds and may expire * normally on the base rail. */ private isEscrowSettling; /** * Get the escrow manager (if escrow is enabled). */ getEscrowManager(): EscrowManager | null; /** * Perform escrow operations on an invoice. * * @param invoiceId - Invoice ID * @param action - Escrow action to perform * @returns Transaction ID */ /** * Bind a proven buyer to a QUOTED escrow invoice and deploy the contract. * * `buyerAddress` MUST come from an authenticated / wallet-connect source — * binding an unproven address would let refunds and dispute payouts go to * the wrong party. On success the invoice's escrow gains its scid/deployTxid * and moves to "awaiting_deposit". */ claimEscrowInvoice(invoiceId: string, buyerAddress: string): Promise; /** * O19 — recover a deploy_failed escrow into a fresh QUOTE so a proven buyer can * re-claim. Mints a NEW escrow quote (new escrowId, no scid) and persists it * under a compare-and-set against the observed deploy_failed binding, so a * concurrent writer (a peer also retrying, a reconciler) cannot be clobbered. * Bounded by the same O13/O21 auto-requote budget as the cancel path to keep a * flapping deploy from becoming an unbounded platform-gas amplifier. Returns the * updated invoice (escrow reset to 'quoted') on success, or null if the retry * budget is exhausted / the CAS was lost (caller should re-read + retry or park). */ private resetDeployFailedEscrow; escrowAction(invoiceId: string, action: "confirmDelivery" | "refundBuyer" | "dispute" | "claimAfterExpiry" | "arbitrateRelease" | "arbitrateRefund"): Promise; /** * Wire up escrow manager events to update invoices and fire webhooks. */ private setupEscrowEvents; /** * Re-quote a fresh escrow onto a still-open invoice whose escrow was cancelled * while never funded. A hostile/regretful seller (or a griefer) can land * CancelUnfunded ahead of a buyer's Deposit (both target status 0; SCDATA is * plaintext in the mempool). That never risks funds — a status-0 contract * holds 0 — but it strands the buyer against a dead SCID. Here we detect the * dead binding and reset the invoice's escrow to a fresh QUOTE so the buyer * can re-claim + deposit. No auto re-deploy: a new buyer proof is required at * the next claim, preserving the two-phase front-run protection. * * Guarded to no-op if the invoice is already paid, completed, expired, or if * its current escrow binding is not the cancelled SCID (so a stale event can't * clobber a newer binding). */ private requoteCancelledEscrow; /** * O20 — tear down a still-open escrow binding when its invoice receives a * misrouted base-rail payment. Prevents a later correct Deposit() from funding * the escrow and silently overwriting the misrouted_to_base alert (which would * settle the invoice while the merchant keeps the orphaned base funds = silent * double-charge). * * - quoted / deploy_failed / no scid: just drop the local quote (no on-chain * contract exists). * - awaiting_deposit (deployed, status-0): CancelUnfunded the contract so no * Deposit can land, then untrack. The cancel reverts iff a Deposit raced in * first; in that RARE case the escrow legitimately funds and we deliberately * let the escrow rail win (buyer's real escrow deposit is honored), but the * invoice keeps a metadata audit marker so the earlier misroute is never * invisible. * - funded / disputed / terminal: do nothing — real funds are already in * escrow; the escrow rail is authoritative and the misroute is a separate * base-wallet reconciliation item. */ private teardownEscrowOnMisroute; /** * O18 — an escrow reported on-chain "funded" but its amount failed independent * verification (escrowBalance != expectedAmount or the contract's real DERO * holdings do not cover it). The escrow manager already declined to settle it * (left it in awaiting_deposit and did NOT emit escrowFunded). Here we make * that visible at the invoice layer WITHOUT settling: the invoice stays off the * shippable path (never escrow_funded/completed) and a funding_mismatch webhook * fires for out-of-band handling. We do not expire it either — the buyer's real * deposit may still be on the contract and recoverable via the normal paths. */ private handleEscrowFundingMismatch; /** * O15 — reload in-flight escrows from the invoice store into the escrow * manager's poller after a restart. * * The escrow manager is in-memory only, so without this its poll loop runs * over an empty map and every not-yet-terminal on-chain escrow (real locked * DERO) goes permanently un-reconciled: no funded/released/arbitrated event * fires again and the invoice never reaches its terminal state. * * We reconstruct the EscrowRecord from the persisted InvoiceEscrow binding and * import it. Only escrows that have actually deployed (have an scid) and are in * a non-terminal state need re-polling; a still-`quoted` / `deploy_failed` * escrow has no on-chain contract to watch. The record we import is a * best-effort local view — the very next poll overwrites status/depositAmount * from authoritative on-chain state via reconcile(). */ private rehydrateEscrows; /** * Check for expired invoices and update their status. */ private checkExpiredInvoices; } export { type CreatePaymentLinkArgs as C, InvoiceEngine as I, type OutboxEvent as O, type PaymentLink as P, type UpdateInvoiceOpts as U, type WebhookSink as W, type X402UsageReservation as X, type InvoiceStore as a, type OutboxRecord as b, type OutboxStatus as c, type InvoiceFilter as d, type InvoiceStats as e, type X402UsageReservationResult as f, type X402UsageBatchReservationResult as g, type PaymentLinkStats as h, type InvoiceEngineEvents as i, type X402AuditEvent as j, type X402AuditEventType as k };