UNPKG

@arkade-os/boltz-swap

Version:

A production-ready TypeScript package that brings Boltz submarine-swaps to Arkade.

1,179 lines (1,175 loc) 54.6 kB
import { Transaction, IWallet, ArkProvider, NetworkName, IndexerProvider } from '@arkade-os/sdk'; /** Configuration for BoltzSwapProvider. */ interface SwapProviderConfig { /** Custom API URL. If omitted, defaults based on `network` (e.g. bitcoin → https://api.ark.boltz.exchange). */ apiUrl?: string; /** The network to operate on (e.g. "mutinynet", "regtest", "bitcoin"). */ network: Network; /** Optional referral ID appended to swap requests. */ referralId?: string; } /** * All possible Boltz swap status values. * * Lifecycle (submarine): swap.created → invoice.set → invoice.pending → invoice.paid → transaction.claimed * Lifecycle (reverse): swap.created → transaction.mempool → transaction.confirmed → invoice.settled * Lifecycle (chain): swap.created → transaction.mempool → transaction.server.mempool → transaction.claimed */ type BoltzSwapStatus = "invoice.expired" | "invoice.failedToPay" | "invoice.paid" | "invoice.pending" | "invoice.set" | "invoice.settled" | "swap.created" | "swap.expired" | "transaction.claim.pending" | "transaction.claimed" | "transaction.confirmed" | "transaction.failed" | "transaction.lockupFailed" | "transaction.mempool" | "transaction.refunded" | "transaction.server.mempool" | "transaction.server.confirmed"; /** Returns true if the status indicates a failed submarine swap. */ declare const isSubmarineFailedStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the submarine swap has reached a terminal state. */ declare const isSubmarineFinalStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the submarine swap is still in progress. */ declare const isSubmarinePendingStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the submarine swap is eligible for refund. */ declare const isSubmarineRefundableStatus: (status: BoltzSwapStatus) => boolean; /** * Canonical progression of a successful submarine swap, in order. Failure * statuses are intentionally absent. This defines relative ordering, NOT a * guaranteed sequence: statuses can be skipped (e.g. "transaction.mempool" * jumps straight to "invoice.pending" when Boltz accepts 0-conf, and a * subscription only ever reports the current status). Consumers must treat * any later status as implying the earlier ones — which is exactly what * hasSubmarineStatusReached does. */ declare const SUBMARINE_STATUS_PROGRESSION: readonly ["swap.created", "invoice.set", "transaction.mempool", "transaction.confirmed", "invoice.pending", "invoice.paid", "transaction.claim.pending", "transaction.claimed"]; /** * A status in the successful submarine swap progression — the only valid * targets for optimistic settlement resolution. */ type SubmarineProgressionStatus = (typeof SUBMARINE_STATUS_PROGRESSION)[number]; /** * Returns true if `status` is at or beyond `target` in the successful * submarine swap progression. Returns false when the observed status is not * part of the progression (e.g. failure statuses like "swap.expired"). */ declare const hasSubmarineStatusReached: (status: BoltzSwapStatus, target: SubmarineProgressionStatus) => boolean; /** Returns true if the submarine swap completed successfully. */ declare const isSubmarineSuccessStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the reverse swap failed. */ declare const isReverseFailedStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the reverse swap has reached a terminal state. */ declare const isReverseFinalStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the reverse swap is still in progress. */ declare const isReversePendingStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the reverse swap VHTLC can be claimed. */ declare const isReverseClaimableStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the reverse swap completed successfully. */ declare const isReverseSuccessStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap failed. */ declare const isChainFailedStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap is claimable (server transaction in mempool or confirmed). */ declare const isChainClaimableStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap has reached a terminal state. */ declare const isChainFinalStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap is still in progress. */ declare const isChainPendingStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap is eligible for refund (swap.expired). */ declare const isChainRefundableStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap is ready for cooperative signing. */ declare const isChainSignableStatus: (status: BoltzSwapStatus) => boolean; /** Returns true if the chain swap completed successfully. */ declare const isChainSuccessStatus: (status: BoltzSwapStatus) => boolean; /** Type guard: narrows BoltzSwap to BoltzReverseSwap. */ declare const isPendingReverseSwap: (swap: BoltzSwap) => swap is BoltzReverseSwap; /** Type guard: narrows BoltzSwap to BoltzSubmarineSwap. */ declare const isPendingSubmarineSwap: (swap: BoltzSwap) => swap is BoltzSubmarineSwap; /** Type guard: narrows BoltzSwap to BoltzChainSwap. */ declare const isPendingChainSwap: (swap: BoltzSwap) => swap is BoltzChainSwap; /** Type guard: checks if swap is a refundable submarine swap (failed + not yet refunded). */ declare const isSubmarineSwapRefundable: (swap: BoltzSwap) => swap is BoltzSubmarineSwap; /** Type guard: checks if swap is a refundable chain swap (expired ARK → BTC). */ declare const isChainSwapRefundable: (swap: BoltzSwap) => swap is BoltzChainSwap; /** Type guard: checks if swap is a claimable reverse swap. */ declare const isReverseSwapClaimable: (swap: BoltzSwap) => swap is BoltzReverseSwap; /** Type guard: checks if swap is a claimable chain swap. */ declare const isChainSwapClaimable: (swap: BoltzSwap) => swap is BoltzChainSwap; type TimeoutBlockHeights = { refund: number; unilateralClaim: number; unilateralRefund: number; unilateralRefundWithoutReceiver: number; }; type GetReverseSwapTxIdResponse = { id: string; timeoutBlockHeight: number; }; type GetSwapStatusResponse = { status: BoltzSwapStatus; zeroConfRejected?: boolean; transaction?: { id: string; hex?: string; confirmed?: boolean; eta?: number; preimage?: string; }; }; /** Request to create a submarine swap (Arkade → Lightning). */ type CreateSubmarineSwapRequest = { /** BOLT11 Lightning invoice to pay. */ invoice: string; /** Compressed public key (33 bytes / 66 hex chars) for the refund path. */ refundPublicKey: string; }; /** Response from creating a submarine swap. */ type CreateSubmarineSwapResponse = { /** Unique swap ID. */ id: string; /** Amount in satoshis to send. */ expectedAmount: number; /** ARK lockup address to send funds to. */ address?: string; /** Boltz's public key for the claim path. */ claimPublicKey?: string; /** Whether zero-conf transactions are accepted. */ acceptZeroConf?: boolean; /** Block height for the onchain HTLC timeout. */ timeoutBlockHeight?: number; /** Block heights for various timeout/refund scenarios. */ timeoutBlockHeights?: TimeoutBlockHeights; }; type GetSwapPreimageResponse = { preimage: string; }; /** Request to create a reverse swap (Lightning → Arkade). */ type CreateReverseSwapRequest = { /** Compressed public key (33 bytes / 66 hex chars) for the claim path. */ claimPublicKey: string; /** Invoice amount in satoshis. */ invoiceAmount: number; /** SHA256 hash of the preimage (hex-encoded). */ preimageHash: string; /** Optional description for the BOLT11 invoice. */ description?: string; /** * Optional SHA256 description hash (hex, 32 bytes / 64 chars) to commit * into the invoice instead of a plaintext description. BOLT11 carries one * or the other, never both; when set, `description` is omitted. */ descriptionHash?: string; }; /** Response from creating a reverse swap. */ type CreateReverseSwapResponse = { /** Unique swap ID. */ id: string; /** BOLT11-encoded Lightning invoice to be paid. */ invoice: string; /** On-chain amount in satoshis (after Boltz fees). */ onchainAmount?: number; /** ARK lockup address where Boltz will lock funds. */ lockupAddress?: string; /** Boltz's public key for the refund path. */ refundPublicKey?: string; /** Block height for the onchain HTLC timeout. */ timeoutBlockHeight?: number; /** Block heights for various timeout/refund scenarios. */ timeoutBlockHeights?: TimeoutBlockHeights; }; type SwapTree = { claimLeaf: { version: number; output: string; }; refundLeaf: { version: number; output: string; }; }; type ChainSwapDetailsResponse = { amount: number; lockupAddress: string; timeoutBlockHeight: number; serverPublicKey: string; swapTree?: SwapTree; timeouts?: TimeoutBlockHeights; bip21?: string; }; /** Request to create a chain swap (ARK ↔ BTC). */ type CreateChainSwapRequest = { /** Destination chain. */ to: Chain; /** Source chain. */ from: Chain; /** SHA256 hash of the preimage (hex-encoded). */ preimageHash: string; /** Compressed public key for the claim path. */ claimPublicKey: string; /** Fee rate (sats/vbyte) for BTC transactions. */ feeSatsPerByte: number; /** Compressed public key for the refund path. */ refundPublicKey: string; /** Amount Boltz should lock (specify this OR userLockAmount). */ serverLockAmount?: number; /** Amount user will lock (specify this OR serverLockAmount). */ userLockAmount?: number; /** Optional referral ID. */ referralId?: string; }; /** Response from creating a chain swap. */ type CreateChainSwapResponse = { /** Unique swap ID. */ id: string; /** Details for claiming the received funds. */ claimDetails: ChainSwapDetailsResponse; /** Details for the lockup (user sends funds here). */ lockupDetails: ChainSwapDetailsResponse; }; type GetChainClaimDetailsResponse = { pubNonce: string; publicKey: string; transactionHash: string; }; type PostChainClaimDetailsRequest = { preimage?: string; toSign?: { index: number; transaction: string; pubNonce: string; }; signature?: { partialSignature: string; pubNonce: string; }; }; type PostChainClaimDetailsResponse = { pubNonce?: string; partialSignature?: string; }; type GetChainQuoteResponse = { amount: number; }; type PostChainQuoteRequest = { amount: number; }; type PostChainQuoteResponse = {}; type PostBtcTransactionResponse = { id: string; }; type Leaf = { version: number; output: string; }; type Tree = { claimLeaf: Leaf; refundLeaf: Leaf; covenantClaimLeaf?: Leaf; refundWithoutBoltzLeaf?: Leaf; unilateralClaimLeaf?: Leaf; unilateralRefundLeaf?: Leaf; unilateralRefundWithoutBoltzLeaf?: Leaf; }; type Details = { tree: Tree; amount?: number; keyIndex: number; transaction?: { id: string; vout: number; }; lockupAddress: string; serverPublicKey: string; timeoutBlockHeight?: number; timeoutBlockHeights?: TimeoutBlockHeights; preimageHash?: string; }; type RestoredChainSwap = { id: string; type: "chain"; status: BoltzSwapStatus; createdAt: number; from: "ARK" | "BTC"; to: "ARK" | "BTC"; preimageHash?: string; invoice?: string; refundDetails?: Details; claimDetails?: Details; }; type RestoredSubmarineSwap = { to: "BTC"; id: string; from: "ARK"; type: "submarine"; createdAt: number; preimageHash?: string; status: BoltzSwapStatus; refundDetails: Details; invoice?: string; }; type RestoredReverseSwap = { to: "ARK"; id: string; from: "BTC"; type: "reverse"; createdAt: number; preimageHash?: string; status: BoltzSwapStatus; claimDetails: Details; invoice?: string; }; type CreateSwapsRestoreResponse = (RestoredChainSwap | RestoredReverseSwap | RestoredSubmarineSwap)[]; /** * API client for the Boltz swap service. * Handles swap creation, status monitoring, fee/limit queries, and cooperative signing * for both Lightning and chain swaps. */ declare class BoltzSwapProvider { private readonly wsUrl; private readonly apiUrl; private readonly network; private readonly referralId?; private readonly inflightGets; /** @param config Provider configuration with network and optional API URL. */ constructor(config: SwapProviderConfig); /** Returns the Boltz API base URL. */ getApiUrl(): string; /** Returns the Boltz WebSocket URL (derived from apiUrl). */ getWsUrl(): string; /** Returns the configured network. */ getNetwork(): Network; /** Returns current Lightning swap fees (submarine + reverse) from Boltz. */ getFees(): Promise<FeesResponse>; /** Returns current Lightning swap min/max limits from Boltz. */ getLimits(): Promise<LimitsResponse>; /** Returns the current BTC chain tip height from Boltz. */ getChainHeight(): Promise<number>; /** Gets the lockup transaction ID for a reverse swap. */ getReverseSwapTxId(id: string): Promise<GetReverseSwapTxIdResponse>; /** * Queries the current status of a swap by ID. * * @throws {SwapNotFoundError} when Boltz responds with HTTP 404 and a body * matching the "could not find swap" pattern. Distinct from a generic 404 * so callers (e.g. SwapManager polling) can drive a per-swap unknown-to- * provider counter without tripping on transient route or proxy errors. */ getSwapStatus(id: string): Promise<GetSwapStatusResponse>; /** Gets the preimage for a settled submarine swap (proof of payment). */ getSwapPreimage(id: string): Promise<GetSwapPreimageResponse>; /** Creates a submarine swap (Arkade → Lightning) on Boltz. */ createSubmarineSwap({ invoice, refundPublicKey, }: CreateSubmarineSwapRequest): Promise<CreateSubmarineSwapResponse>; /** Creates a reverse swap (Lightning → Arkade) on Boltz. */ createReverseSwap({ invoiceAmount, claimPublicKey, preimageHash, description, descriptionHash, }: CreateReverseSwapRequest): Promise<CreateReverseSwapResponse>; /** Creates a chain swap (ARK ↔ BTC) on Boltz. */ createChainSwap({ to, from, preimageHash, feeSatsPerByte, claimPublicKey, refundPublicKey, serverLockAmount, userLockAmount, }: CreateChainSwapRequest): Promise<CreateChainSwapResponse>; /** Requests Boltz co-signature for a submarine swap refund. Returns signed transaction + checkpoint. */ refundSubmarineSwap(swapId: string, transaction: Transaction, checkpoint: Transaction): Promise<{ transaction: Transaction; checkpoint: Transaction; }>; /** Requests Boltz co-signature for a chain swap refund. Returns signed transaction + checkpoint. */ refundChainSwap(swapId: string, transaction: Transaction, checkpoint: Transaction): Promise<{ transaction: Transaction; checkpoint: Transaction; }>; /** * Monitors swap status updates and forwards them to the update callback. * Prefers a WebSocket subscription; on connection error or premature close * it falls back to REST polling so callers don't fail when the WS endpoint * is flaky (observed in CI). Resolves when the swap reaches a terminal * status. */ monitorSwap(swapId: string, update: (type: BoltzSwapStatus, data?: any) => void): Promise<void>; /** Returns current chain swap fees for a given pair (e.g. ARK→BTC). */ getChainFees(from: Chain, to: Chain): Promise<ChainFeesResponse>; /** Returns current chain swap min/max limits for a given pair. */ getChainLimits(from: Chain, to: Chain): Promise<LimitsResponse>; /** Gets claim details (pubNonce, publicKey, transactionHash) for cooperative chain swap claiming. */ getChainClaimDetails(swapId: string): Promise<GetChainClaimDetailsResponse>; /** Gets a renegotiated quote for a chain swap when lockup amount differs from expected. */ getChainQuote(swapId: string): Promise<GetChainQuoteResponse>; /** Accepts a renegotiated quote amount for a chain swap. */ postChainQuote(swapId: string, request: PostChainQuoteRequest): Promise<PostChainQuoteResponse>; /** Broadcasts a raw BTC transaction through Boltz. */ postBtcTransaction(hex: string): Promise<PostBtcTransactionResponse>; /** Posts claim details (preimage + signing data) or cooperative signature for a chain swap. */ postChainClaimDetails(swapId: string, request: PostChainClaimDetailsRequest): Promise<PostChainClaimDetailsResponse>; /** Restores swaps from Boltz API using the wallet's public key. */ restoreSwaps(publicKey: string): Promise<CreateSwapsRestoreResponse>; private request; private doRequest; } /** * Swap action types emitted by SwapManager. * * Lightning actions: * - `claim` — claim a reverse swap VHTLC (Lightning → Arkade) * - `refund` — refund a submarine swap VHTLC (Arkade → Lightning, failed) * * Chain swap actions: * - `claimArk` — claim ARK via VHTLC (BTC → ARK swap) * - `claimBtc` — claim BTC via HTLC (ARK → BTC swap) * - `refundArk` — refund ARK via VHTLC (ARK → BTC swap, failed) * * Note: there is no `refundBtc` because BTC lockup refunds are handled * on-chain by Boltz after the timelock expires. * * Cooperative signing: * - `signServerClaim` — sign a cooperative claim for the server (BTC → ARK, courtesy) */ type Actions = "claim" | "refund" | "claimArk" | "claimBtc" | "refundArk" | "signServerClaim"; interface SwapManagerConfig { /** Auto claim/refund swaps (default: true) */ enableAutoActions?: boolean; /** Polling interval in ms (default: 30000) */ pollInterval?: number; /** Initial reconnect delay (default: 1000) */ reconnectDelayMs?: number; /** Max reconnect delay (default: 60000) */ maxReconnectDelayMs?: number; /** Initial poll retry delay (default: 5000) */ pollRetryDelayMs?: number; /** Max poll retry delay (default: 300000) */ maxPollRetryDelayMs?: number; /** Absolute ceiling for any poll interval (default: 300000) */ maxPollIntervalMs?: number; /** Event callbacks for swap lifecycle events (optional, can use on/off methods instead) */ events?: SwapManagerEvents; } /** Event callbacks for swap lifecycle events. Can be provided in config or registered via on/off methods. */ interface SwapManagerEvents { onSwapUpdate?: (swap: BoltzSwap, oldStatus: BoltzSwapStatus) => void; onSwapCompleted?: (swap: BoltzSwap) => void; onSwapFailed?: (swap: BoltzSwap, error: Error) => void; onActionExecuted?: (swap: BoltzSwap, action: Actions) => void; onWebSocketConnected?: () => void; onWebSocketDisconnected?: (error?: Error) => void; } /** Callback for swap status changes. Receives the updated swap and its previous status. */ type SwapUpdateListener = (swap: BoltzSwap, oldStatus: BoltzSwapStatus) => void; /** Callback for swap completions (final success state reached). */ type SwapCompletedListener = (swap: BoltzSwap) => void; /** Callback for swap failures. Includes the error that caused the failure. */ type SwapFailedListener = (swap: BoltzSwap, error: Error) => void; /** Callback after a swap action (claim/refund) has been executed. */ type ActionExecutedListener = (swap: BoltzSwap, action: Actions) => void; /** Callback when the WebSocket connection is established. */ type WebSocketConnectedListener = () => void; /** Callback when the WebSocket disconnects. Includes the error if disconnection was not clean. */ type WebSocketDisconnectedListener = (error?: Error) => void; /** Per-swap update callback used with subscribeToSwapUpdates. */ type SwapUpdateCallback = (swap: BoltzSwap, oldStatus: BoltzSwapStatus) => void; /** Public interface for SwapManager consumers. Provides swap monitoring, event subscription, and lifecycle control. */ interface SwapManagerClient { /** Starts the manager, loading initial swaps and connecting WebSocket. */ start(pendingSwaps: BoltzSwap[]): Promise<void>; /** Stops the manager, closes WebSocket, and clears all timers. */ stop(): Promise<void>; /** Adds a new swap to be monitored. Immediately subscribes via WebSocket. */ addSwap(swap: BoltzSwap): Promise<void>; /** Removes a swap from monitoring. */ removeSwap(swapId: string): Promise<void>; /** Returns all currently monitored (non-final) swaps. */ getPendingSwaps(): Promise<BoltzSwap[]>; /** Subscribes to status updates for a specific swap. @returns Unsubscribe function. */ subscribeToSwapUpdates(swapId: string, callback: SwapUpdateCallback): Promise<() => void>; /** Returns a promise that resolves with { txid } when the swap completes, or rejects on failure. */ waitForSwapCompletion(swapId: string): Promise<{ txid: string; }>; /** Returns true if a claim/refund action is currently executing for this swap. */ isProcessing(swapId: string): Promise<boolean>; /** Returns true if the manager is monitoring this swap. */ hasSwap(swapId: string): Promise<boolean>; /** Returns operational statistics (running state, WebSocket status, monitored count, etc.). */ getStats(): Promise<{ isRunning: boolean; monitoredSwaps: number; websocketConnected: boolean; usePollingFallback: boolean; currentReconnectDelay: number; currentPollRetryDelay: number; }>; onSwapUpdate(listener: SwapUpdateListener): Promise<() => void>; onSwapCompleted(listener: SwapCompletedListener): Promise<() => void>; onSwapFailed(listener: SwapFailedListener): Promise<() => void>; onActionExecuted(listener: ActionExecutedListener): Promise<() => void>; onWebSocketConnected(listener: WebSocketConnectedListener): Promise<() => void>; onWebSocketDisconnected(listener: WebSocketDisconnectedListener): Promise<() => void>; offSwapUpdate(listener: SwapUpdateListener): void; offSwapCompleted(listener: SwapCompletedListener): void; offSwapFailed(listener: SwapFailedListener): void; offActionExecuted(listener: ActionExecutedListener): void; offWebSocketConnected(listener: WebSocketConnectedListener): void; offWebSocketDisconnected(listener: WebSocketDisconnectedListener): void; } /** Internal callbacks wired by ArkadeSwaps to perform claim/refund/save operations. */ interface SwapManagerCallbacks { claim: (swap: BoltzReverseSwap) => Promise<void>; refund: (swap: BoltzSubmarineSwap) => Promise<void>; /** * Claim the ARK side of a BTC→ARK chain swap. * * Returns the on-chain claim txid — the manager surfaces it from * {@link SwapManager.waitForSwapCompletion}, since Boltz does not report a * usable transaction id for chain swaps at `transaction.claimed`. */ claimArk: (swap: BoltzChainSwap) => Promise<{ txid: string; }>; /** * Claim the BTC side of an ARK→BTC chain swap. Returns the on-chain claim * txid; see {@link SwapManagerCallbacks.claimArk}. */ claimBtc: (swap: BoltzChainSwap) => Promise<{ txid: string; }>; refundArk: (swap: BoltzChainSwap) => Promise<ChainArkRefundOutcome>; signServerClaim?: (swap: BoltzChainSwap) => Promise<void>; saveSwap: (swap: BoltzSwap) => Promise<void>; } /** * Background swap monitor with WebSocket + polling fallback. * * Monitors all pending swaps via a single multiplexed WebSocket connection to Boltz. * Automatically claims reverse swaps, refunds failed submarine swaps, and handles * chain swap actions (claim/refund on both ARK and BTC sides). * * Falls back to HTTP polling when WebSocket is unavailable, with exponential backoff * for both reconnection and polling intervals. */ declare class SwapManager implements SwapManagerClient { /** * Number of consecutive Boltz 404s for a single swap ID before the * polling loop gives up and transitions the swap to a terminal state. * At the default 30s poll cadence this is roughly a 5-minute grace * window — long enough to ride out a transient Boltz blip, short * enough that a real "swap unknown to this provider" surfaces quickly. */ private static readonly NOT_FOUND_THRESHOLD; /** * Delay between re-attempts of a chain refund that left VTXOs deferred * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV * has elapsed). Boltz won't send another status update once the swap * is `swap.expired`, so the manager owns the local retry cadence. */ private static readonly REFUND_RETRY_DELAY_MS; private readonly swapProvider; private readonly config; private swapUpdateListeners; private swapCompletedListeners; private swapFailedListeners; private actionExecutedListeners; private wsConnectedListeners; private wsDisconnectedListeners; private websocket; private monitoredSwaps; private initialSwaps; private pollTimer; private reconnectTimer; private initialPollTimer; private pollRetryTimers; private refundRetryTimers; private notFoundCounts; private isRunning; private currentReconnectDelay; private currentPollRetryDelay; private usePollingFallback; private isReconnecting; private webSocketUnavailable; private swapsInProgress; private swapSubscriptions; private chainClaimPromises; private claimCallback; private refundCallback; private claimArkCallback; private claimBtcCallback; private refundArkCallback; private signServerClaimCallback; private saveSwapCallback; constructor(swapProvider: BoltzSwapProvider, config?: SwapManagerConfig); /** * Set callbacks for claim, refund, and save operations. * These are called by the manager when autonomous actions are needed. */ setCallbacks(callbacks: SwapManagerCallbacks): void; /** * Add an event listener for swap updates * @returns Unsubscribe function */ onSwapUpdate(listener: SwapUpdateListener): Promise<() => void>; /** * Add an event listener for swap completion * @returns Unsubscribe function */ onSwapCompleted(listener: SwapCompletedListener): Promise<() => void>; /** * Add an event listener for swap failures * @returns Unsubscribe function */ onSwapFailed(listener: SwapFailedListener): Promise<() => void>; /** * Add an event listener for executed actions (claim/refund) * @returns Unsubscribe function */ onActionExecuted(listener: ActionExecutedListener): Promise<() => void>; /** * Add an event listener for WebSocket connection * @returns Unsubscribe function */ onWebSocketConnected(listener: WebSocketConnectedListener): Promise<() => void>; /** * Add an event listener for WebSocket disconnection * @returns Unsubscribe function */ onWebSocketDisconnected(listener: WebSocketDisconnectedListener): Promise<() => void>; /** Remove a swap update listener */ offSwapUpdate(listener: SwapUpdateListener): void; /** Remove a swap completed listener */ offSwapCompleted(listener: SwapCompletedListener): void; /** Remove a swap failed listener */ offSwapFailed(listener: SwapFailedListener): void; /** Remove an action executed listener */ offActionExecuted(listener: ActionExecutedListener): void; /** Remove a WebSocket connected listener */ offWebSocketConnected(listener: WebSocketConnectedListener): void; /** Remove a WebSocket disconnected listener */ offWebSocketDisconnected(listener: WebSocketDisconnectedListener): void; /** * Start the swap manager * This will: * 1. Load pending swaps * 2. Connect WebSocket (with fallback to polling) * 3. Poll all swaps after connection * 4. Resume any actionable swaps */ start(pendingSwaps: BoltzSwap[]): Promise<void>; /** * Stop the swap manager * Cleanup: close WebSocket, stop all timers */ stop(): Promise<void>; /** * Set the polling interval (ms). * Restarts any running timer so the new interval takes effect immediately. */ setPollInterval(ms: number): void; /** * Add a new swap to monitoring. * If fallback polling is active, this triggers an immediate poll and resets * fallback delay so newly-added swaps are checked without waiting. */ addSwap(swap: BoltzSwap): Promise<void>; /** * Remove a swap from monitoring */ removeSwap(swapId: string): Promise<void>; /** * Get all currently monitored swaps */ getPendingSwaps(): Promise<BoltzSwap[]>; /** * Subscribe to updates for a specific swap * Returns an unsubscribe function * Useful for UI components that need to track specific swap progress */ subscribeToSwapUpdates(swapId: string, callback: SwapUpdateCallback): Promise<() => void>; /** * Wait for a specific swap to complete. * Blocks until the swap reaches a final status or fails. * Useful when you want blocking behavior even with SwapManager enabled. * * If the swap is already in a final status, resolves immediately with the * on-chain txid of the successfully claimed swap (reverse: from * getReverseSwapTxId; submarine/chain: from getSwapStatus) and throws for a * failed final status. * * @throws If the swap is already in a failed final status. * @throws If the swap is not found in the manager. * @throws If a completed swap has no transaction id available yet. */ waitForSwapCompletion(swapId: string): Promise<{ txid: string; }>; /** * Resolve the on-chain txid for a claimed submarine/chain swap. * * Chain swaps are claimed by the manager itself, so the claim txid captured * from the claimArk/claimBtc callback is the swap's on-chain completion and * is preferred — Boltz does not surface it via getSwapStatus at * transaction.claimed. Submarine swaps (claimed by Boltz) and chain swaps * we never claimed in this session (e.g. restored already-claimed) fall * back to the provider status. Rejects when no transaction id is available, * so callers never receive the Boltz swap id in place of a real txid. */ private resolveClaimedTxid; /** * Check if a swap is currently being processed * Useful for preventing race conditions */ isProcessing(swapId: string): Promise<boolean>; /** * Check if manager has a specific swap */ hasSwap(swapId: string): Promise<boolean>; /** * Try connecting to WebSocket for real-time swap updates. * If WebSocket is unavailable in the current runtime, switch directly to polling fallback. * If connection setup fails, also switches to polling fallback. */ private tryConnectWebSocket; /** * Runtime feature detection for WebSocket API. */ private hasWebSocketSupport; /** * Enter polling fallback mode and notify listeners about WebSocket unavailability/failure. * Resets fallback delay to its configured initial value (capped by max poll interval). */ private enterPollingFallback; /** * Schedule WebSocket reconnection with exponential backoff */ private scheduleReconnect; /** * Subscribe to a specific swap ID on the WebSocket */ private subscribeToSwap; /** * Handle incoming WebSocket message */ private handleWebSocketMessage; /** * Handle status update for a swap * This is the core logic that determines what actions to take */ private handleSwapStatusUpdate; /** * Drop a swap from monitoring and emit the terminal completion event. * Shared between the on-status-update finalization path and the * refund-retry finalization path (used when a previously-deferred * chain refund has finished its remaining work). */ private finalizeMonitoredSwap; /** * Schedule another `executeAutonomousAction` run for a chain swap whose * refund left VTXOs deferred. After the retry completes, if no further * deferral was reported, finalize monitoring cleanup. */ private scheduleRefundRetry; /** * Execute autonomous action based on swap status * Uses locking to prevent race conditions with manual operations */ private executeAutonomousAction; /** * Execute claim action for reverse swap */ private executeClaimAction; /** * Execute refund action for submarine swap */ private executeRefundAction; /** * Execute claim action for chain swap Btc to Ark */ private executeClaimArkAction; /** * Execute claim action for chain swap Ark to Btc */ private executeClaimBtcAction; /** * Store the in-flight claim promise returned by a claim callback so * {@link resolveClaimedTxid} can await it and surface the real on-chain * txid at transaction.claimed — even when that update races the still * in-flight claim. Storing the promise (not the resolved txid) closes the * window where the txid is not yet captured when transaction.claimed * arrives. Defensive against callbacks that don't return a promise (older * integrations, test doubles): those simply fall back to getSwapStatus. */ private rememberChainClaim; /** * Execute refund action for chain swap Ark to Btc */ private executeRefundArkAction; /** * Execute sign server claim action for chain swap. * Returns true on success, false if no callback is set. * Throws if the callback itself throws. */ private executeSignServerClaimAction; /** * Save swap to storage */ private saveSwap; /** * Resume actionable swaps on startup * This checks all pending swaps and executes actions if needed */ private resumeActionableSwaps; /** * Start regular polling * Polls all swaps at configured interval when WebSocket is active */ private startPolling; /** * Start polling fallback when WebSocket is unavailable * Uses exponential backoff for retry delay */ private startPollingFallback; /** * Poll all monitored swaps for status updates * This is called: * 1. After WebSocket connects * 2. After WebSocket reconnects * 3. Periodically while WebSocket is active * 4. As fallback when WebSocket is unavailable */ private pollAllSwaps; private pollSingleSwap; /** * Increment the consecutive-not-found counter and, once the threshold is * reached, transition the swap to a terminal state and stop polling it. * Driven from {@link pollSingleSwap} when `getSwapStatus` throws * {@link SwapNotFoundError}. The threshold rides out a transient blip * but ensures we stop hammering Boltz with requests for swap IDs the * server has no record of (e.g. after switching the configured * Boltz endpoint). */ private handleSwapNotFound; /** * Transition a swap to {@code swap.expired} (terminal for all swap types) * after Boltz has consistently reported it unknown for * {@link SwapManager.NOT_FOUND_THRESHOLD} consecutive polls. The swap is * persisted, removed from monitoring, and reported via `onSwapFailed`. * Bypasses {@link handleSwapStatusUpdate} on purpose: we don't want to * trigger autonomous claim/refund actions against a Boltz instance that * has no record of this swap — the requests would just generate more * 404s without recovering anything. */ private markSwapAsUnknownToProvider; /** * Check if a status is final (no more updates expected) */ private isFinalStatus; /** * Get current manager statistics (for debugging/monitoring) */ getStats(): Promise<{ isRunning: boolean; monitoredSwaps: number; websocketConnected: boolean; usePollingFallback: boolean; currentReconnectDelay: number; currentPollRetryDelay: number; }>; } type GetSwapsFilter = { id?: string | string[]; status?: BoltzSwapStatus | BoltzSwapStatus[]; type?: BoltzSwap["type"] | BoltzSwap["type"][]; orderBy?: "createdAt"; orderDirection?: "asc" | "desc"; }; interface SwapRepository extends AsyncDisposable { readonly version: 1; saveSwap<T extends BoltzSwap>(swap: T): Promise<void>; deleteSwap(id: string): Promise<void>; getAllSwaps<T extends BoltzSwap>(filter?: GetSwapsFilter): Promise<T[]>; clear(): Promise<void>; } /** A virtual transaction output (VTXO) — an off-chain UTXO in the Ark protocol. */ interface Vtxo { /** Transaction ID of the VTXO. */ txid: string; /** Output index in the transaction. */ vout: number; /** Amount in satoshis. */ sats: number; /** The output script (hex-encoded). */ script: string; /** Raw transaction data. */ tx: { /** Raw transaction hex. */ hex: string; /** Transaction version number. */ version: number; /** Transaction locktime. */ locktime: number; }; } /** Network name (e.g. "mutinynet", "regtest", "bitcoin"). */ type Network = NetworkName; /** The chain side of a swap: "ARK" (off-chain Ark protocol) or "BTC" (on-chain Bitcoin). */ type Chain = "ARK" | "BTC"; /** Response from creating an ARK → BTC chain swap. */ interface ArkToBtcResponse { /** The ARK lockup address to send funds to. */ arkAddress: string; /** Amount in satoshis to send to the lockup address. */ amountToPay: number; /** The pending chain swap object for monitoring. */ pendingSwap: BoltzChainSwap; } /** Response from creating a BTC → ARK chain swap. */ interface BtcToArkResponse { /** The BTC lockup address to send funds to. */ btcAddress: string; /** Amount in satoshis to send to the lockup address. */ amountToPay: number; /** The pending chain swap object for monitoring. */ pendingSwap: BoltzChainSwap; } /** Request to create a Lightning invoice (reverse swap: Lightning → Arkade). */ interface CreateLightningInvoiceRequest { /** Invoice amount in satoshis. */ amount: number; /** Optional description embedded in the BOLT11 invoice. */ description?: string; /** * Optional SHA256 description hash (hex, 32 bytes) to commit into the * BOLT11 invoice instead of a plaintext description. A BOLT11 invoice * carries either a description or a description hash, never both, so when * this is set `description` is ignored. Use this for flows that must bind * the invoice to an external document — e.g. NIP-57 zaps, where the hash * is SHA256 of the zap request and the receipt later proves the match. */ descriptionHash?: string; } /** Response containing the created Lightning invoice and swap details. */ interface CreateLightningInvoiceResponse { /** The on-chain amount in satoshis (after Boltz fees). */ amount: number; /** Invoice expiry: seconds from invoice creation (timestamp) until it expires. */ expiry: number; /** Invoice creation timestamp (Unix seconds). Always present in a valid BOLT11 invoice; `0` should not occur in practice. */ timestamp: number; /** The BOLT11-encoded Lightning invoice string. */ invoice: string; /** The payment hash (hex-encoded). */ paymentHash: string; /** The pending reverse swap for monitoring. */ pendingSwap: BoltzReverseSwap; /** The preimage (hex-encoded). Keep secret until claiming. */ preimage: string; } /** Request to send a Lightning payment (submarine swap: Arkade → Lightning). */ interface SendLightningPaymentRequest { /** BOLT11-encoded Lightning invoice to pay. */ invoice: string; /** * When the returned promise resolves (default "settled"). * * - "settled": at the terminal "transaction.claimed" status, once Boltz * has swept the HTLC. The preimage is included in the response. * - "funded": optimistically, as soon as the lockup transaction is * observed ("transaction.mempool" or any later status — statuses can be * skipped since subscriptions report only the current one). The sender's * funds are committed and the swap is refundable from this point, so * most wallets show the payment as "sent" here. No preimage is available * yet; monitoring continues in the background and keeps persisting * status updates until a terminal status, but a late failure no longer * rejects — keep the SwapManager enabled so refunds are handled * automatically. */ waitFor?: "settled" | "funded"; } /** Response after a successful Lightning payment. */ interface SendLightningPaymentResponse { /** Amount paid in satoshis. */ amount: number; /** Payment preimage proving payment was made (hex-encoded). */ preimage: string; /** Transaction ID of the Arkade payment. */ txid: string; } /** * Response after a Lightning payment sent with `waitFor: "funded"` — the * payment is in flight but may not be settled yet, so the preimage may not * be available. */ interface OptimisticSendLightningPaymentResponse { /** Amount paid in satoshis. */ amount: number; /** Payment preimage (hex-encoded). Undefined when the call resolved before the swap settled. */ preimage?: string; /** Transaction ID of the Arkade payment. */ txid: string; } /** Tracks an in-progress reverse swap (Lightning → Arkade). */ interface BoltzReverseSwap { /** Unique swap ID from Boltz. */ id: string; /** Discriminator — always "reverse". */ type: "reverse"; /** Unix timestamp (seconds) when the swap was created. */ createdAt: number; /** The swap preimage (hex-encoded). Required for claiming the VHTLC. */ preimage: string; /** Current Boltz swap status. */ status: BoltzSwapStatus; /** The original request sent to Boltz. */ request: CreateReverseSwapRequest; /** Boltz API response with lockup address, invoice, and timeout details. */ response: CreateReverseSwapResponse; } /** Tracks an in-progress submarine swap (Arkade → Lightning). */ interface BoltzSubmarineSwap { /** Unique swap ID from Boltz. */ id: string; /** Discriminator — always "submarine". */ type: "submarine"; /** Unix timestamp (seconds) when the swap was created. */ createdAt: number; /** Payment preimage (hex-encoded). Available after the Lightning payment settles. */ preimage?: string; /** Original preimage hash from Boltz (available for restored swaps). */ preimageHash?: string; /** Whether the swap has been refunded. */ refunded?: boolean; /** Whether the swap is eligible for refund. */ refundable?: boolean; /** Current Boltz swap status. */ status: BoltzSwapStatus; /** The original request sent to Boltz. */ request: CreateSubmarineSwapRequest; /** Boltz API response with payment address and expected amount. */ response: CreateSubmarineSwapResponse; } /** * Outcome of inspecting a submarine swap's lockup address for recoverable * funds. * * - `recoverable` — unspent VTXOs exist and can be swept now, either because * the refund CLTV has elapsed or because the Boltz 3-of-3 refund path is * immediately available. * - `pre_cltv` — unspent VTXOs exist but the refund locktime has not * passed yet and no immediate Boltz 3-of-3 refund path was detected. * - `none` — no unspent VTXOs at the address (never funded, fully * pruned, or only preconfirmed-only state). * - `already_spent` — the address has VTXOs but every one is spent * (typical for a healthy completed swap). * - `invalid_swap` — the swap is not a recovery candidate (pending * status), the swap data is incomplete, or the reconstructed VHTLC * address doesn't match the one Boltz returned. */ type SubmarineRecoveryStatus = "recoverable" | "pre_cltv" | "none" | "already_spent" | "invalid_swap"; /** Diagnostic snapshot of a submarine swap's recovery state. */ interface SubmarineRecoveryInfo { /** The submarine swap this snapshot describes. */ swap: BoltzSubmarineSwap; /** Classification of the lockup address state. */ status: SubmarineRecoveryStatus; /** Number of unspent VTXOs at the lockup address. */ vtxoCount: number; /** Total satoshis across the unspent VTXOs. */ amountSats: number; /** * Absolute Unix-timestamp CLTV from the swap's VHTLC, when available. * Compared against wall-clock seconds for refund readiness. */ refundLocktime?: number; /** Reason populated when `status === "invalid_swap"`. */ error?: string; } /** Outcome of a single `refundVHTLC` call: how many VTXOs were swept vs. deferred. */ interface SubmarineRefundOutcome { /** Number of VTXOs successfully refunded (joined a batch or via Boltz co-sign). */ swept: number; /** * Number of VTXOs that could not be refunded yet (e.g. recoverable VTXO * pre-CLTV, or Boltz rejected and CLTV still not satisfied). The caller * is expected to retry these later. */ skipped: number; } /** Outcome of a single `refundArk` call: how many VTXOs were swept vs. deferred. */ interface ChainArkRefundOutcome { /** Number of VTXOs successfully refunded. */ swept: number; /** * Number of VTXOs that could not be refunded yet (e.g. recoverable VTXO * pre-CLTV, or Boltz rejected and CLTV still not satisfied). The caller * is expected to retry these later. */ skipped: number; } /** Per-swap outcome of a bulk recovery call. */ interface SubmarineRecoveryResult { /** ID of the swap whose VHTLC we attempted to refund. */ swapId: string; /** True only when at least one VTXO was actually swept by `refundVHTLC`. */ recovered: boolean; /** * True when `refundVHTLC` returned without throwing but at least one * VTXO was deferred — distinguishes a real sweep from a no-op skip * path. Always false when `error` is set (the error supersedes). */ skipped: boolean; /** Failure message when `refundVHTLC` threw. */ error?: string; } /** Tracks an in-progress chain swap (ARK ↔ BTC). */ interface BoltzChainSwap { /** Unique swap ID from Boltz. */ id: string; /** Discriminator — always "chain". */ type: "chain"; /** The swap preimage (hex-encoded). Required for claiming. */ preimage: string; /** Unix timestamp (seconds) when the swap was created. */ createdAt: number; /** Ephemeral private key (hex) for BTC MuSig2 claim/refund signing. */ ephemeralKey: string; /** Fee rate (sats/vbyte) used for on-chain BTC transactions. */ feeSatsPerByte: number; /** Current Boltz swap status. */ status: BoltzSwapStatus; /** The original chain swap request sent to Boltz. */ request: CreateChainSwapRequest; /** Boltz API response with lockup and claim details. */ response: CreateChainSwapResponse; /** Destination address for the received funds. */ toAddress?: string; /** Swap amount in satoshis. */ amount: number; } /** Union type of all pending swap types. */ type BoltzSwap = BoltzReverseSwap | BoltzSubmarineSwap | BoltzChainSwap; /** Pending- swap type aliases for backwards compatibility */ interface PendingReverseSwap extends BoltzReverseSwap { } interface PendingSubmarineSwap extends BoltzSubmarineSwap { } interface PendingChainSwap extends BoltzChainSwap { } type PendingSwap = PendingReverseSwap | PendingSubmarineSwap | PendingChainSwap; /** Configuration for initializing ArkadeSwaps via the constructor (swapProvider is required). */ interface ArkadeSwapsConfig { /** An IWallet instance from @arkade-os/sdk (must expose arkProvider and indexerProvider). */ wallet: IWallet; /** Explicit ArkProvider. Falls back to wallet.arkProvider if omitted. */ arkProvider?: ArkProvider; /** BoltzSwapProvider instance for interacting with the Boltz API. */ swapProvider: BoltzSwapProvider; /** Explicit IndexerProvider. Falls back to wallet.indexerProvider if omitted. */ indexerProvider?: IndexerProvider; /** * Background swap monitoring and autonomous actions (enabled by default). * - `undefined` or `true`: SwapManager enabled with default configuration * - `false`: SwapManager disabled * - `SwapManagerConfig` object: SwapManager enabled with custom configuration */ swapManager?: boolean | (SwapManagerConfig & { autoStart?: boolean; }); /** * Optional swap repository to use for persisting swap data. * - `undefined`: fallback to default IndexedDbSwapRepository * - `SwapRepository` object: SwapRepository enabled with custom configuration */ swapRepository?: SwapRepository; } /** * Configuration for {@link ArkadeSwaps.create} — same as ArkadeSwapsConfig but * `swapProvider` is optional (auto-created from the wallet's network if omitted). */ type ArkadeSwapsCreateConfig = Omit<ArkadeSwapsConfig, "swapProvider"> & { swapProvider?: BoltzSwapProvider; }; /** A decoded BOLT11 Lightning invoice. */ interface DecodedInvoice { /** Invoice expiry timestamp (Unix seconds). */ expiry: number; /** Invoice creation timestamp (Unix seconds). Always present in a valid BOLT11 invoice;