@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
TypeScript
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;