import { W as WalletRpcClient } from './wallet-rpc-sY1qJ24S.cjs'; import { DaemonRpcClient } from './rpc/index.cjs'; /** * Escrow types for DeroPay smart contract-based payments. * * These types model the lifecycle of a DERO escrow transaction * from deployment through resolution. */ /** Status codes stored on-chain in the escrow smart contract */ declare const EscrowStatusCode: { readonly AWAITING_DEPOSIT: 0; readonly FUNDED: 1; readonly RELEASED: 2; readonly REFUNDED: 3; readonly EXPIRED_CLAIMED: 4; readonly DISPUTED: 5; readonly ARBITRATED: 6; readonly CANCELLED: 7; }; type EscrowStatusCodeValue = (typeof EscrowStatusCode)[keyof typeof EscrowStatusCode]; /** Human-readable escrow status */ type EscrowStatus = "quoted" | "awaiting_deposit" | "funded" | "released" | "refunded" | "expired_claimed" | "disputed" | "arbitrated" | "cancelled" | "deploying" /** * PREMINT: the mint (empty Initialize) or the Bind failed. Both are deterministic, * fungible failures — the empty box is retried and any orphaned unbound box is * reclaimable via CancelUnfunded, so this state is safely releasable + re-quotable. * (There is no broadcast-ambiguous quarantine: mint is off the terms path and Bind * is a normal invoke, so a failure never leaves a live, terms-bound contract with an * unknown SCID.) */ | "deploy_failed"; /** Map on-chain status code to SDK status string */ declare const statusCodeToString: Record; /** Parameters to QUOTE an escrow (phase 1 — no buyer, no on-chain deploy). */ type CreateEscrowQuoteParams = { /** Seller's DERO address */ sellerAddress: string; /** Arbitrator's DERO address. Must be set explicitly; self-arbitration * (arbitrator == owner/fee-recipient) is rejected by the invoice engine * unless the caller opts in via allowSelfArbitration. */ arbitratorAddress: string; /** Platform fee in basis points (100 = 1%, 250 = 2.5%). Defaults if omitted. */ feeBasisPoints?: number; /** Blocks after deposit before the seller can claim without buyer confirmation. * Must be within [4000, 10000000] (~20 hours to ~5.7 years at ~18s/block); * enforced on-chain and by the deploy() call. The 4000-block floor guarantees * a human buyer a realistic dispute window before ClaimAfterExpiry unlocks. * Defaults if omitted. */ blockExpiration?: number; /** Required: the exact price (atomic units). On-chain Deposit() rejects any * deposit below this — blocks dust locks and underpayment. */ expectedAmount: bigint; /** Arbitrary metadata */ metadata?: Record; }; /** Parameters to create + deploy an escrow in one call (merchant-known-buyer * fast path). Adds the buyer address bound at deploy. */ type CreateEscrowParams = CreateEscrowQuoteParams & { /** Buyer's DERO address — bound at deploy; only this address may Deposit(). * MUST be a wallet-connect / authenticated address, never unverified input. */ buyerAddress: string; }; /** The full escrow contract state as read from the blockchain */ type EscrowOnChainState = { /** Smart Contract ID */ scid: string; /** On-chain status code */ statusCode: EscrowStatusCodeValue; /** Human-readable status */ status: EscrowStatus; /** Owner (deployer) address */ owner: string; /** Bind flag written on-chain: 0 = minted but no terms yet (empty box), * 1 = order terms assigned. The keeper's pool-ready gate requires bound === 0 * (an empty, still-bindable box) alongside owner set and status 0. */ bound: number; /** Seller address */ seller: string; /** Buyer address (set after deposit) */ buyer: string | null; /** Arbitrator address */ arbitrator: string; /** Platform fee in basis points */ feeBasisPoints: number; /** Block expiration window */ blockExpiration: number; /** Expected deposit amount in atomic units (a deposit must be >= this) */ expectedAmount: number; /** Current escrow balance in atomic units */ escrowBalance: number; /** Block height of the deposit (set after deposit) */ depositHeight: number | null; /** Block height of the dispute (set after Dispute()); null until disputed. The * buyer's RefundAfterDisputeTimeout unlocks at disputeHeight + 14400 (~3 days). */ disputeHeight: number | null; /** Arbitration direction, written on-chain by Arbitrate() (1 = released to * seller, 0 = refunded to buyer). Null until the dispute is arbitrated. * Both arbitrate branches zero escrowBalance, so this flag — not the * balance — is the authoritative source of the resolution direction. */ arbitrateResult: number | null; /** DERO balance held by the SC */ scBalance: number; }; /** Local escrow record with both on-chain and off-chain data */ type EscrowRecord = { /** Unique local ID */ id: string; /** Smart Contract ID (set after successful deployment) */ scid: string | null; /** Deployment transaction ID */ deployTxid: string | null; /** Current status (includes SDK-only states like deploying) */ status: EscrowStatus; /** Seller address */ sellerAddress: string; /** Arbitrator address */ arbitratorAddress: string; /** Fee in basis points */ feeBasisPoints: number; /** Block expiration window */ blockExpiration: number; /** Expected deposit amount (atomic units) */ expectedAmount: bigint | null; /** Actual deposit amount (atomic units) */ depositAmount: bigint | null; /** * Proven buyer address (deto1/dero1 bech32) bound at claim time. This is the * authenticated, actionable address; it is NEVER overwritten from on-chain * state (the contract stores the RAW point, which GetSC returns as hex). */ buyerAddress: string | null; /** * The RAW (ADDRESS_RAW hex) form of the buyer as stored on-chain, surfaced by * reconcile() on the funded transition for verification only. Compare against * ADDRESS_RAW(buyerAddress) to confirm the depositor matches the bound buyer; * do NOT use it as an actionable address. Null until funded. */ onChainBuyerRaw?: string | null; /** When the escrow was created locally (ISO 8601) */ createdAt: string; /** When the deposit was made (ISO 8601) */ depositedAt: string | null; /** When the escrow was resolved (ISO 8601) */ resolvedAt: string | null; /** Resolution type */ resolution: EscrowResolution | null; /** Linked invoice ID (if created through the invoice engine) */ invoiceId: string | null; /** Arbitrary metadata */ metadata: Record; }; /** How the escrow was resolved */ type EscrowResolution = "buyer_confirmed" | "seller_refunded" | "owner_refunded" | "seller_claimed_expiry" | "arbitrator_released_seller" | "arbitrator_refunded_buyer"; /** Events emitted by the EscrowManager */ type EscrowManagerEvents = { /** Escrow deployed successfully */ escrowDeployed: (escrow: EscrowRecord) => void; /** Escrow mint or Bind failed DETERMINISTICALLY. Safe to release + re-quote * (the empty box is fungible; any orphaned unbound box is CancelUnfunded-able). */ escrowDeployFailed: (escrow: EscrowRecord, error: Error) => void; /** Buyer deposited into escrow */ escrowFunded: (escrow: EscrowRecord) => void; /** * O18 — an awaiting_deposit escrow flipped to on-chain status "funded" but the * funded amount FAILED independent verification: either escrowBalance != * expectedAmount (amount_mismatch) or the contract's real DERO holdings do not * cover escrowBalance (custody_shortfall). The escrow is deliberately NOT * settled and stays in awaiting_deposit; the invoice must never be driven to * escrow_funded/shippable off this. Requires human/out-of-band handling. */ escrowFundingMismatch: (escrow: EscrowRecord, detail: { expectedAmount: bigint | null; onChainBalance: bigint; scBalance: bigint; reason: "amount_mismatch" | "custody_shortfall"; }) => void; /** Escrow released to seller */ escrowReleased: (escrow: EscrowRecord) => void; /** Escrow refunded to buyer */ escrowRefunded: (escrow: EscrowRecord) => void; /** Dispute raised */ escrowDisputed: (escrow: EscrowRecord) => void; /** Escrow was cancelled while never funded (status 0 -> 7). Lets the app * re-quote a fresh contract onto a still-open invoice so a griefing * CancelUnfunded that races a buyer's deposit does not strand the buyer. */ escrowCancelled: (escrow: EscrowRecord) => void; /** Arbitrator resolved dispute */ escrowArbitrated: (escrow: EscrowRecord) => void; /** Status changed (generic) */ escrowStatusChanged: (escrow: EscrowRecord, previousStatus: EscrowStatus) => void; /** The PREMINT keeper pool was empty when this escrow claimed, so it fell back * to inline mint-on-demand (checkout still succeeds, just slower by ~1 block). * A recurring signal means the pool is undersized — raise targetReady/refillBelow * or investigate a stalled keeper. Only emitted when a keeper is configured. */ escrowInventoryEmpty: (escrow: EscrowRecord) => void; /** Error */ error: (error: Error) => void; }; /** EscrowManager configuration */ type EscrowManagerConfig = { /** Wallet RPC URL */ walletRpcUrl?: string; /** Daemon RPC URL */ daemonRpcUrl?: string; /** RPC auth */ rpcAuth?: { username: string; password: string; }; /** Polling interval for escrow status checks (ms, default: 10000) */ pollIntervalMs?: number; /** Default fee in basis points (default: 250 = 2.5%) */ defaultFeeBasisPoints?: number; /** Default block expiration (default: 9600 blocks ~= 2 days at ~18s/block). * Must be >= 4000 (the on-chain minimum dispute window). */ defaultBlockExpiration?: number; }; /** * Escrow smart contract wrapper. * * Provides a typed interface to deploy, invoke, and query * the DERO escrow smart contract via the RPC clients. */ /** * Typed wrapper around the escrow smart contract. * * All methods return transaction IDs or on-chain state. * The contract logic enforces access control on-chain. */ declare class EscrowContract { private walletRpc; private daemonRpc; constructor(walletRpc: WalletRpcClient, daemonRpc: DaemonRpcClient); /** * Get the escrow smart contract source code. */ getSource(): string; /** * Mint a NEW empty escrow box (PREMINT — MINT phase). * * Initialize() takes ZERO args and no DERO: it stores only owner=SIGNER and * status=0/bound=0. The deployer (signer) becomes the "owner" (platform). Order * terms are written later via bind(); the buyer is captured on-chain at * deposit() from SIGNER() (ring 2). An empty box is fungible — a failed mint is * trivially retried, and an unbound box can be reclaimed with cancelUnfunded(). * * @returns Deployment TXID (= the SCID of the empty box) */ deploy(): Promise; /** * Bind order terms into a minted empty box (PREMINT — ASSIGN phase). * * Owner-gated and one-shot on-chain: a box with bound!=0 rejects a re-bind, and * only the deployer (owner) may bind. The buyer is NOT set here — the contract * captures it at deposit() from SIGNER(). Party addresses and numeric ranges are * validated here so a bad value fails with a clear message before any bind gas is * spent (the contract enforces the same ranges on-chain: Bind lines 40–80). * * @param scid - the minted (empty) escrow box to bind * @returns Bind transaction ID */ bind(scid: string, params: { sellerAddress: string; arbitratorAddress: string; feeBasisPoints: number; blockExpiration: number; expectedAmount: bigint; }): Promise; /** * Buyer deposits DERO into the escrow contract. * * @param scid - Smart Contract ID * @param amount - Amount in atomic units to deposit * @returns Transaction ID */ deposit(scid: string, amount: bigint): Promise; /** * Buyer confirms delivery — releases funds to seller (minus fee). * * @param scid - Smart Contract ID * @returns Transaction ID */ confirmDelivery(scid: string): Promise; /** * Cancel a never-funded escrow (seller/owner action). Closes a status-0 * contract whose bound buyer never deposited (e.g. buyer proved wallet A at * claim but funds only from wallet B, so Deposit() perpetually reverts). * No funds move — escrowBalance is 0 in status 0. * * @param scid - Smart Contract ID * @returns Transaction ID */ cancelUnfunded(scid: string): Promise; /** * Seller or owner refunds the buyer. * * @param scid - Smart Contract ID * @returns Transaction ID */ refundBuyer(scid: string): Promise; /** * Seller claims funds after the expiration window. * * @param scid - Smart Contract ID * @returns Transaction ID */ claimAfterExpiry(scid: string): Promise; /** * Buyer raises a dispute, locking funds until arbitrator resolves. * * @param scid - Smart Contract ID * @returns Transaction ID */ dispute(scid: string): Promise; /** * Arbitrator resolves a dispute. * * @param scid - Smart Contract ID * @param releaseToSeller - true = pay seller, false = refund buyer * @returns Transaction ID */ arbitrate(scid: string, releaseToSeller: boolean): Promise; /** * Buyer's timeout escape hatch (buyer action). After the on-chain dispute * window (14400 blocks ~= 3 days) has passed since dispute(), the buyer may * reclaim their full deposit if the arbitrator never resolved. Deliberately * NOT blocked by pause() on-chain, so a frozen box can never permanently trap * the buyer's funds; it can only ever return the deposit to the bound buyer. * * @param scid - Smart Contract ID * @returns Transaction ID */ refundAfterDisputeTimeout(scid: string): Promise; /** * Owner circuit-breaker: freeze a box discovered mid-flight to be buggy. * Blocks deposit() and every discretionary settlement path on-chain, but NOT * the buyer's refundAfterDisputeTimeout() escape. Cannot claw back or drain. * * @param scid - Smart Contract ID * @returns Transaction ID */ pause(scid: string): Promise; /** * Owner lifts a pause() freeze. * * @param scid - Smart Contract ID * @returns Transaction ID */ unpause(scid: string): Promise; /** * Nominate a new owner (current-owner action). Two-step: the successor must * ClaimOwnership() to take over. Use this to move owner authority off the hot * deploy key onto a cold key, bounding a hot-key compromise. * * @param scid - Smart Contract ID * @param newOwner - DERO address of the nominated successor * @returns Transaction ID */ transferOwnership(scid: string, newOwner: string): Promise; /** * Accept a pending ownership nomination (successor action). Must be signed by * the exact address nominated via transferOwnership(). * * @param scid - Smart Contract ID * @returns Transaction ID */ claimOwnership(scid: string): Promise; /** * Query the full on-chain state of an escrow contract. * * @param scid - Smart Contract ID * @returns Parsed on-chain state */ getState(scid: string): Promise; /** * Check if an escrow contract exists on-chain by verifying * the SCID returns valid state data. */ exists(scid: string): Promise; } /** * Durable inventory of pre-minted empty escrow boxes for the {@link EscrowKeeper}. * * PREMINT moves the mint→confirm latency (~1 block) OFF the checkout path: the * keeper mints empty boxes ahead of demand and holds their SCIDs here; checkout * then only has to Bind an already-confirmed box. A box moves through three * states: * * minted — mint TX broadcast; SCID known but NOT yet on-chain-confirmed. * NEVER hand this out (bind() would hit "SC not found"). * confirmed — GetSC read back owner-set + bound=0 + status=0. POOL-READY. * claimed — atomically popped by a checkout; it is being bound and consumed. * * The claim MUST be atomic across processes: two concurrent checkouts must never * pop the same confirmed SCID (the loser's bind would revert on bound!=0). The * SQLite store does this with a single conditional UPDATE ... RETURNING; the * in-memory store relies on JS's single thread (no await between read and flip). */ /** Lifecycle state of an inventoried box. */ type EscrowInventoryState = "minted" | "confirmed" | "claimed"; interface EscrowInventoryStore { /** Whether pops are atomic ACROSS processes. A process-local store is `false` * and is unsafe under a multi-process server (two workers could pop the same * box); a shared-storage store is `true`. */ readonly durable: boolean; /** Record a freshly-minted box (state = minted). SCID = the mint TXID. */ add(scid: string): Promise; /** Promote a minted box to confirmed (pool-ready) after its GetSC gate passes. */ markConfirmed(scid: string): Promise; /** Atomically pop ONE confirmed box (confirmed → claimed) and return its SCID, * or null if the pool is empty. The single-winner primitive. */ claimOne(): Promise; /** * Return a claimed box to the 'minted' state after a FAILED bind (claimed → * minted). The keeper's next confirmMinted() re-reads it via GetSC: if the bind * never landed the box is still empty (bound=0) and gets re-pooled — reclaiming * the mint gas; if the bind actually landed (bound=1) the gate keeps it out, * correctly retiring it. Without this, a failed bind would strand the box in * 'claimed' forever (leaked inventory). No-op unless the box is 'claimed'. */ release(scid: string): Promise; /** Count confirmed (pool-ready) boxes. */ countReady(): Promise; /** SCIDs still in the minted (unconfirmed) state — the keeper polls these with * GetSC to promote or discard them. */ listMinted(): Promise; /** Drop a box from inventory entirely (e.g. a mint that never confirmed and was * reclaimed via CancelUnfunded). */ remove(scid: string): Promise; } /** * Single-process store backed by a Map. Atomic within one process only. Use with * the in-memory app store or in tests; use {@link SqliteEscrowInventoryStore} for * a real multi-process server. */ declare class MemoryEscrowInventoryStore implements EscrowInventoryStore { readonly durable = false; private readonly boxes; add(scid: string): Promise; markConfirmed(scid: string): Promise; claimOne(): Promise; release(scid: string): Promise; countReady(): Promise; listMinted(): Promise; remove(scid: string): Promise; } /** * Durable, multi-process store backed by a SQLite table. `claimOne` uses a single * conditional `UPDATE ... WHERE rowid = (SELECT ... LIMIT 1) RETURNING scid`: the * statement is atomic, so exactly one caller flips a given confirmed row to * claimed and reads its SCID; any concurrent caller either pops a DIFFERENT row or * gets undefined (empty pool). Atomic across processes/connections sharing the db * file. * * Pass the same `better-sqlite3` Database the app store / claim guard use so the * inventory lives in one file; the table is created on construction. */ declare class SqliteEscrowInventoryStore implements EscrowInventoryStore { readonly durable = true; private readonly db; constructor(db: any); add(scid: string): Promise; markConfirmed(scid: string): Promise; claimOne(): Promise; release(scid: string): Promise; countReady(): Promise; listMinted(): Promise; remove(scid: string): Promise; } /** * EscrowKeeper — keeps a pool of pre-minted, confirmed EMPTY escrow boxes stocked * so checkout only has to Bind (not mint + confirm + bind). * * The keeper runs a background loop that: * 1. confirms minted boxes — GetSC each 'minted' SCID; a box becomes pool-ready * ONLY once it reads back owner-set + bound=0 + status=0 (see THE TRAP below). * 2. refills when low — when ready count < refillBelow, mint empty boxes up * to targetReady (accounting for in-flight minted boxes so it never over-mints). * * THE TRAP — the keeper and the binder MUST be the SAME owner wallet. * The contract's Bind is owner-gated (`IF SIGNER() != LOAD("owner")`), and * Initialize sets owner = SIGNER() at mint. A box minted by wallet A can only be * bound by wallet A. This keeper is therefore constructed with the SAME * EscrowContract instance the EscrowManager binds with — one platform wallet mints * AND binds. Do NOT hand it a second signer. * * Second trap — a mint's SCID exists only after the mint confirms (~1 block). * Offering an unconfirmed SCID would make bind() fail with "SC not found". The * confirm gate below is the guard: a box is pool-ready ONLY after GetSC proves it. */ interface EscrowKeeperOptions { /** Desired number of confirmed, pool-ready boxes to keep on hand. */ targetReady: number; /** Refill is triggered when ready count drops BELOW this. */ refillBelow: number; /** Interval between keeper ticks (confirm + refill), in ms. */ pollMs: number; } interface EscrowKeeperEvents { /** A minted box passed its GetSC gate and entered the pool. */ boxConfirmed: (scid: string) => void; /** A box was minted (SCID broadcast, not yet confirmed). */ boxMinted: (scid: string) => void; /** A keeper tick threw (mint RPC error, GetSC error). Non-fatal; the loop * continues on the next tick. */ error: (error: Error) => void; } declare class EscrowKeeper { /** MUST mint with the SAME platform wallet the manager binds with (THE TRAP). */ private readonly contract; private readonly store; private timer; private ticking; private readonly opts; private listeners; constructor( /** MUST mint with the SAME platform wallet the manager binds with (THE TRAP). */ contract: EscrowContract, store: EscrowInventoryStore, opts?: Partial); on(event: K, cb: EscrowKeeperEvents[K]): () => void; private emit; /** Start the background loop. Runs one tick immediately, then every pollMs. */ start(): void; stop(): void; /** Whether the backing inventory store pops atomically ACROSS processes. The * engine asserts this is true for a multi-process deployment (two workers must * never pop the same box). */ get durable(): boolean; /** Pop a confirmed box for a checkout to bind. Returns null when the pool is * empty (caller falls back to inline mint-on-demand). */ take(): Promise; /** Return a taken box to the keeper after a FAILED bind so it is not leaked in * the 'claimed' state. The next tick re-verifies it via GetSC and either * re-pools it (bind never landed) or retires it (bind landed, bound!=0). */ release(scid: string): Promise; /** Confirmed (pool-ready) box count — surfaced so the app can alert on low stock. */ readyCount(): Promise; /** * One keeper cycle: confirm minted boxes, then refill if the pool is low. * Re-entrancy-guarded so a slow tick (many GetSC / mint RPCs) never overlaps * itself under the interval. Exposed for deterministic tests (drive it directly * instead of waiting on the timer). */ tick(): Promise; /** * Promote minted boxes that GetSC proves are empty and ours. THE TRAP gate: * pool-ready iff owner is set (minted by us), bound === 0 (no terms yet), and * status === 0 (never funded). Anything else stays 'minted' (still confirming) * and is retried next tick — never handed out early. */ private confirmMinted; /** * Mint empty boxes up to targetReady. The deficit subtracts BOTH confirmed and * still-minted (in-flight) boxes so a burst of ticks before the first mint * confirms does not over-mint the pool. */ private refill; } /** * EscrowManager — lifecycle orchestrator for escrow payments. * * Manages the full lifecycle of escrow transactions: * 1. Deploys escrow smart contracts * 2. Polls on-chain state for status changes * 3. Emits events on transitions (funded, released, disputed, etc.) * 4. Maintains local records for fast lookups * * Usage: * ```ts * const manager = new EscrowManager({ * walletRpcUrl: "http://127.0.0.1:30000/json_rpc", * daemonRpcUrl: "http://127.0.0.1:20000/json_rpc", * }); * * await manager.start(); * * // Merchant-known-buyer fast path (quote + deploy in one call): * const escrow = await manager.createEscrow({ * sellerAddress: "dero1q...", // base address (NOT integrated deto1…) * buyerAddress: "dero1q...", // base addr; only this addr may fund (must match SIGNER()) * arbitratorAddress: "dero1q...", * feeBasisPoints: 250, * blockExpiration: 9600, // ~2 days at ~18s/block; must be >= 4000 * expectedAmount: 5_000_000_000_000n, // 5 DERO — deposit must be >= this * }); * * // Open flow: createEscrowQuote(...) then claimEscrow(id, provenBuyerAddr) * // once the buyer connects their wallet. * ``` */ /** * EscrowManager orchestrates the lifecycle of escrow smart contracts. */ /** * Durable compare-and-set hook for the quote->claim single-claim guard. * * The in-memory guard in {@link EscrowManager.claimEscrow} is atomic ONLY * within a single process. The stated deployment target is a persistent, * multi-process server; there, two workers can both read a "quoted" record and * both proceed to deploy (a TOCTOU that re-opens the buyer-seat hijack at the * claim window). Injecting a durable CAS closes this: `tryClaim` must perform an * atomic conditional write (e.g. `UPDATE escrows SET status='deploying' WHERE * id=$1 AND status='quoted'`) and return true ONLY if THIS caller won the row. */ interface EscrowClaimGuard { /** * Whether this guard is atomic ACROSS processes. A process-local guard (e.g. * the in-memory Set) is `false` and provides NO protection in a clustered * deployment; a shared-storage guard (SQLite/DB) is `true`. The engine uses * this to fail LOUD at startup when a multi-process server is configured with * a process-local guard (O4) instead of silently failing open. */ readonly durable: boolean; /** Atomically flip id from 'quoted' to 'deploying'. Returns true iff this * caller won the transition (i.e. it was still 'quoted'). */ tryClaim(id: string): Promise; /** Roll the row back to 'quoted' if the subsequent deploy failed. */ releaseClaim(id: string): Promise; /** * Record the deploy TXID against a won claim as soon as the deploy is * broadcast, BEFORE the (separate) invoice-blob persist. This is the durable * breadcrumb a crash-recovery reconciler follows: a held row carrying a * deployTxid means "an on-chain contract for this quote exists" even if the * invoice blob never got its scid (O5). Optional so process-local guards may * no-op. */ recordDeployTxid?(id: string, txid: string): Promise; /** * Enumerate held claim rows for crash recovery (O5). Returns every claimed id * with the deployTxid recorded (or null if the crash happened before the * deploy was even broadcast) AND the claimedAt epoch-ms the row was won. * * claimedAt is load-bearing for O12: the reconciler MUST NOT release a row a * live peer worker is still mid-deploy against (it has won tryClaim but not yet * reached recordDeployTxid). Because a broadcast completes in seconds while a * crashed claim leaves an aged row, the reconciler only heals/releases rows * older than a deploy lease — never a fresh, actively-deploying peer's row. * Optional; a process-local guard returns nothing durable so it may omit this. */ listClaims?(): Promise>; /** * O18 — return held rows that are OLDER than `leaseMs`, with the age computed * against the SAME clock authority that stamped `claimed_at`. The reconciler * MUST use this instead of comparing `claimedAt` to its own `Date.now()`: * `claimed_at` is stamped by the DEPLOYING worker and the cutoff is evaluated * by the RECONCILING worker, which in a multi-host cluster is a different wall * clock. Cross-host NTP skew larger than (lease − broadcast latency) would let * a reconciler free a live, mid-broadcast peer row and re-open the double-deploy * A12 closed. Evaluating age inside the guard (SQLite `unixepoch`) makes the * lease a single-clock interval. Each returned row is eligible to heal/release. * Optional; a process-local guard has no cross-host concern so it may omit this * and the reconciler falls back to listClaims() (single-process = one clock). */ listExpiredClaims?(leaseMs: number): Promise>; } declare class EscrowManager { private walletRpc; private daemonRpc; private contract; private claimGuard; private keeper; private escrows; private scidToId; private pollTimer; private isStarted; private pollIntervalMs; private defaultFeeBasisPoints; private defaultBlockExpiration; private listeners; constructor(config?: EscrowManagerConfig & { /** Inject RPC clients (for testing); when set, walletRpcUrl/daemonRpcUrl are ignored */ walletRpc?: WalletRpcClient; daemonRpc?: DaemonRpcClient; /** * Durable single-claim guard. REQUIRED for any multi-process deployment: * without it the quote->claim transition is only process-atomic and a * claim-race attacker can re-open the buyer-seat hijack. When omitted the * manager falls back to the in-memory guard (safe ONLY for a single * process / tests). */ claimGuard?: EscrowClaimGuard; /** * PREMINT keeper inventory. When provided, the manager builds a background * {@link EscrowKeeper} that pre-mints empty boxes into this store; claimEscrow * then binds a pooled box instead of minting inline (the ~1-block mint-confirm * latency moves off the checkout path). The keeper mints through the manager's * OWN contract/wallet, so the minter and the binder are the same owner — the * hard requirement for the owner-gated Bind (THE TRAP). Omit to keep the * original mint-on-demand behavior. */ escrowInventory?: EscrowInventoryStore; /** Tuning for the keeper pool (targetReady / refillBelow / pollMs). Ignored * unless escrowInventory is set. */ keeperOptions?: Partial; }); /** Register an event listener. Returns an unsubscribe function. */ on(event: K, callback: EscrowManagerEvents[K]): () => void; private emit; /** * Start the escrow manager. * Begins polling tracked escrows for on-chain status changes. */ start(): Promise; /** * Stop the escrow manager. */ stop(): void; /** Whether the manager is running */ get running(): boolean; /** * Create and deploy a new escrow smart contract. * * Deploys the contract on-chain and returns a local EscrowRecord * that starts being polled for status updates. */ /** * Phase 1 — create a local QUOTE. No buyer, no on-chain deployment. * * The contract is NOT deployed until {@link claimEscrow} binds a proven * buyer address. This is what structurally closes the deposit front-run: * no unbound-buyer contract ever exists on-chain for an attacker to race. */ createEscrowQuote(params: CreateEscrowQuoteParams): Promise; /** * Phase 2 — bind a proven buyer and deploy the contract on-chain. * * IMPORTANT: `buyerAddress` MUST come from an authenticated / wallet-connect * source. Binding an unproven address would let refunds and dispute payouts * go to the wrong party. Guarded so a quote can be claimed only once. * * Note: the in-memory status guard is atomic within a single process. A * server that persists escrows across processes must back this with a * durable compare-and-set to prevent a double-deploy under a claim race. */ claimEscrow(id: string, buyerAddress: string): Promise; /** * Convenience for the merchant-known-buyer fast path: quote + immediately * claim (deploy) in one call. Requires the buyer address up front. */ createEscrow(params: CreateEscrowParams): Promise; /** * Get an escrow record by its local ID. */ getEscrow(id: string): EscrowRecord | null; /** The configured single-claim guard, or null if none was injected. Exposed * so the engine can assert a durable guard in a multi-process deployment (O4). */ getClaimGuard(): EscrowClaimGuard | null; /** * Get an escrow record by its SCID. */ getEscrowByScid(scid: string): EscrowRecord | null; /** * List all tracked escrows, optionally filtered by status. */ listEscrows(statusFilter?: EscrowStatus[]): EscrowRecord[]; /** * Get the number of active (non-terminal) escrows being tracked. */ get activeCount(): number; /** * Import an existing escrow (e.g. from persistent storage on restart). * * O17 — the import is DEFENSIVE, not unconditional. A rebuild-on-any-worker * path (claimEscrowInvoice / the reconciler) calls this whenever getEscrow() * returns falsy, but a near-simultaneous request in the SAME process can have * already advanced the record past 'quoted' (deploying/awaiting_deposit) and * bound its scid. An unconditional `set` would blow that live binding away with * a fresh scid=null 'quoted' record and desync scidToId. So: NEVER overwrite an * existing record that is already past 'quoted'. If a record already exists and * is in a live/deployed state, the import is a no-op (the caller's rebuild is * stale). We only accept the import when there is no record, or the existing one * is still a scid-less 'quoted'/'deploying' placeholder being (re)hydrated. */ importEscrow(record: EscrowRecord): void; /** * Stop tracking an escrow (removes from polling, keeps in memory). */ untrack(id: string): void; /** * Deposit DERO into an escrow contract (buyer action). * * Note: this uses the *current wallet* as the depositor. * For buyer-initiated deposits from their own wallet, use the * XSWD client-side flow instead. */ deposit(scidOrId: string, amount: bigint): Promise; /** * Confirm delivery (buyer action) — releases funds to seller. */ confirmDelivery(scidOrId: string): Promise; /** * Cancel a never-funded escrow (seller/owner action). Recovers a status-0 * contract whose bound buyer never deposited — e.g. the buyer proved wallet A * at claim but can only fund from wallet B, so Deposit() perpetually reverts. * No funds are at risk (escrowBalance is 0 in status 0). */ cancelUnfunded(scidOrId: string): Promise; /** * Nominate a new owner for an escrow (current-owner action). Two-step: the * successor must claimOwnership() to take over. Used to rotate owner authority * onto a cold key, bounding a hot-key compromise across the escrow book. */ transferOwnership(scidOrId: string, newOwner: string): Promise; /** * Accept a pending ownership nomination (successor action). */ claimOwnership(scidOrId: string): Promise; /** * Refund the buyer (seller/owner action). */ refundBuyer(scidOrId: string): Promise; /** * Claim funds after expiry (seller action). */ claimAfterExpiry(scidOrId: string): Promise; /** * Raise a dispute (buyer action). */ dispute(scidOrId: string): Promise; /** * Arbitrate a dispute (arbitrator action). */ arbitrate(scidOrId: string, releaseToSeller: boolean): Promise; /** * Buyer's timeout escape hatch (buyer action). Reclaims the full deposit if a * dispute went unresolved past the on-chain window (14400 blocks ~= 3 days). * Works even on a paused box (the contract exempts it from the freeze). */ refundAfterDisputeTimeout(scidOrId: string): Promise; /** * Freeze a box (owner circuit-breaker). Blocks deposit + discretionary * settlement, but not the buyer's refundAfterDisputeTimeout escape. */ pause(scidOrId: string): Promise; /** * Lift a pause() freeze (owner action). */ unpause(scidOrId: string): Promise; /** * Query the live on-chain state of an escrow contract. */ getOnChainState(scidOrId: string): Promise; /** * Get the underlying EscrowContract for advanced usage. */ getContract(): EscrowContract; /** * The PREMINT keeper, or null if no inventory store was injected. Exposed so the * app can read pool depth (readyCount) for a low-inventory alert or drive a tick * on demand. */ getKeeper(): EscrowKeeper | null; /** * O15b — the underlying wallet RPC client. Exposed so the crash reconciler can * enumerate the platform wallet's OWN outgoing SC-install TXs (listOwnScDeploys) * to recover a broadcast-indeterminate deploy whose SCID was never learned. */ getWalletRpc(): WalletRpcClient; /** * Poll all tracked escrows for on-chain status changes. */ private pollEscrows; /** * Reconcile local record with on-chain state, * emitting events for any status transitions. */ private reconcile; /** * Resolve a string to an SCID. * Accepts either a local escrow ID or a direct SCID. */ private resolveScid; } export { type CreateEscrowParams as C, type EscrowClaimGuard as E, MemoryEscrowInventoryStore as M, SqliteEscrowInventoryStore as S, type CreateEscrowQuoteParams as a, EscrowContract as b, type EscrowInventoryState as c, type EscrowInventoryStore as d, EscrowKeeper as e, type EscrowKeeperEvents as f, type EscrowKeeperOptions as g, EscrowManager as h, type EscrowManagerConfig as i, type EscrowManagerEvents as j, type EscrowOnChainState as k, type EscrowRecord as l, type EscrowResolution as m, type EscrowStatus as n, EscrowStatusCode as o, type EscrowStatusCodeValue as p, statusCodeToString as s };