UNPKG

@arkade-os/boltz-swap

Version:

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

1,230 lines (1,223 loc) 265 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ArkadeLightningMessageHandler: () => ArkadeSwapsMessageHandler, ArkadeSwaps: () => ArkadeSwaps, ArkadeSwapsMessageHandler: () => ArkadeSwapsMessageHandler, BoltzRefundError: () => BoltzRefundError, BoltzSwapProvider: () => BoltzSwapProvider, InMemorySwapRepository: () => InMemorySwapRepository, IndexedDbSwapRepository: () => IndexedDbSwapRepository, InsufficientFundsError: () => InsufficientFundsError, InvoiceExpiredError: () => InvoiceExpiredError, InvoiceFailedToPayError: () => InvoiceFailedToPayError, NetworkError: () => NetworkError, PreimageFetchError: () => PreimageFetchError, QuoteRejectedError: () => QuoteRejectedError, SchemaError: () => SchemaError, ServiceWorkerArkadeLightning: () => ServiceWorkerArkadeSwaps, ServiceWorkerArkadeSwaps: () => ServiceWorkerArkadeSwaps, SwapError: () => SwapError, SwapExpiredError: () => SwapExpiredError, SwapManager: () => SwapManager, SwapNotFoundError: () => SwapNotFoundError, TransactionFailedError: () => TransactionFailedError, decodeInvoice: () => decodeInvoice, enrichReverseSwapPreimage: () => enrichReverseSwapPreimage, enrichSubmarineSwapInvoice: () => enrichSubmarineSwapInvoice, getInvoicePaymentHash: () => getInvoicePaymentHash, getInvoiceSatoshis: () => getInvoiceSatoshis, hasSubmarineStatusReached: () => hasSubmarineStatusReached, isChainClaimableStatus: () => isChainClaimableStatus, isChainFailedStatus: () => isChainFailedStatus, isChainFinalStatus: () => isChainFinalStatus, isChainPendingStatus: () => isChainPendingStatus, isChainRefundableStatus: () => isChainRefundableStatus, isChainSignableStatus: () => isChainSignableStatus, isChainSuccessStatus: () => isChainSuccessStatus, isChainSwapClaimable: () => isChainSwapClaimable, isChainSwapRefundable: () => isChainSwapRefundable, isPendingChainSwap: () => isPendingChainSwap, isPendingReverseSwap: () => isPendingReverseSwap, isPendingSubmarineSwap: () => isPendingSubmarineSwap, isReverseClaimableStatus: () => isReverseClaimableStatus, isReverseFailedStatus: () => isReverseFailedStatus, isReverseFinalStatus: () => isReverseFinalStatus, isReversePendingStatus: () => isReversePendingStatus, isReverseSuccessStatus: () => isReverseSuccessStatus, isReverseSwapClaimable: () => isReverseSwapClaimable, isSubmarineFailedStatus: () => isSubmarineFailedStatus, isSubmarineFinalStatus: () => isSubmarineFinalStatus, isSubmarinePendingStatus: () => isSubmarinePendingStatus, isSubmarineRefundableStatus: () => isSubmarineRefundableStatus, isSubmarineSuccessStatus: () => isSubmarineSuccessStatus, isSubmarineSwapRefundable: () => isSubmarineSwapRefundable, isValidArkAddress: () => isValidArkAddress, logger: () => logger, migrateToSwapRepository: () => migrateToSwapRepository, saveSwap: () => saveSwap, sdkVersion: () => sdkVersion, setLogger: () => setLogger, updateChainSwapStatus: () => updateChainSwapStatus, updateReverseSwapStatus: () => updateReverseSwapStatus, updateSubmarineSwapStatus: () => updateSubmarineSwapStatus, verifySignatures: () => verifySignatures }); module.exports = __toCommonJS(index_exports); // package.json var version = "0.3.50"; // src/errors.ts var SwapError = class extends Error { /** Whether the swap can still be claimed (default: false). */ isClaimable; /** Whether the swap can be refunded (default: false). */ isRefundable; /** The pending swap associated with this error, if available. */ pendingSwap; constructor(options = {}) { super( options.message ?? "Error during swap.", options.cause !== void 0 ? { cause: options.cause } : void 0 ); this.name = "SwapError"; this.isClaimable = options.isClaimable ?? false; this.isRefundable = options.isRefundable ?? false; this.pendingSwap = options.pendingSwap; } }; var InvoiceExpiredError = class extends SwapError { constructor(options = {}) { super({ message: "The invoice has expired.", ...options }); this.name = "InvoiceExpiredError"; } }; var InvoiceFailedToPayError = class extends SwapError { constructor(options = {}) { super({ message: "The provider failed to pay the invoice", ...options }); this.name = "InvoiceFailedToPayError"; } }; var InsufficientFundsError = class extends SwapError { constructor(options = {}) { super({ message: "Not enough funds available", ...options }); this.name = "InsufficientFundsError"; } }; var NetworkError = class extends Error { /** HTTP status code from the failed request, if available. */ statusCode; /** Raw error payload from the Boltz API, if available. */ errorData; constructor(message, statusCode, errorData) { super(message); this.name = "NetworkError"; this.statusCode = statusCode; this.errorData = errorData; } }; var SwapNotFoundError = class extends NetworkError { /** The swap ID Boltz did not recognise. */ swapId; constructor(swapId, errorData) { super( `Boltz returned 404 for swap '${swapId}': swap unknown to this Boltz instance`, 404, errorData ); this.name = "SwapNotFoundError"; this.swapId = swapId; } }; var SchemaError = class extends SwapError { constructor(options = {}) { super({ message: "Invalid API response", ...options }); this.name = "SchemaError"; } }; var SwapExpiredError = class extends SwapError { constructor(options = {}) { super({ message: "The swap has expired", ...options }); this.name = "SwapExpiredError"; } }; var TransactionFailedError = class extends SwapError { constructor(options = {}) { super({ message: "The transaction has failed.", ...options }); this.name = "TransactionFailedError"; } }; var PreimageFetchError = class extends SwapError { constructor(options = {}) { super({ message: "The payment settled, but fetching the preimage failed.", ...options }); this.name = "PreimageFetchError"; } }; var TransactionLockupFailedError = class extends SwapError { constructor(options = {}) { super({ message: "The transaction lockup has failed.", ...options }); this.name = "TransactionLockupFailedError"; } }; var TransactionRefundedError = class extends SwapError { constructor(options = {}) { super({ message: "The transaction has been refunded.", ...options }); this.name = "TransactionRefundedError"; } }; var QuoteRejectedError = class _QuoteRejectedError extends SwapError { reason; quotedAmount; floor; constructor(options) { super({ message: options.message ?? _QuoteRejectedError.defaultMessage(options), ...options }); this.name = "QuoteRejectedError"; this.reason = options.reason; this.quotedAmount = "quotedAmount" in options ? options.quotedAmount : void 0; this.floor = "floor" in options ? options.floor : void 0; } static defaultMessage(options) { switch (options.reason) { case "below_floor": return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`; case "non_positive": return `Boltz quote ${options.quotedAmount} is not positive`; case "non_safe_integer": return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`; case "no_baseline": return "Cannot accept quote: no minAcceptableAmount and no stored pending swap"; } } /** * Serialize into a plain `Error` whose `.message` carries the full * rejection payload as JSON behind a marker prefix. Structured clone * (used by `postMessage` between page and service worker) preserves * `Error.message` reliably but strips custom `.name` and own properties, * so we move the typed data into the message field for transport. */ toTransportError() { return new Error( QUOTE_REJECTION_TRANSPORT_PREFIX + JSON.stringify({ reason: this.reason, message: this.message, quotedAmount: this.quotedAmount, floor: this.floor }) ); } /** * Inverse of `toTransportError`. Returns a real `QuoteRejectedError` if * `error` carries the transport prefix, else `null`. */ static fromTransportError(error) { if (!(error instanceof Error) || !error.message.startsWith(QUOTE_REJECTION_TRANSPORT_PREFIX)) { return null; } const payload = error.message.slice(QUOTE_REJECTION_TRANSPORT_PREFIX.length); let data; try { data = JSON.parse(payload); } catch { return null; } if (typeof data.reason !== "string" || !QUOTE_REJECTION_REASONS.has(data.reason)) { return null; } const message = typeof data.message === "string" ? data.message : void 0; const reason = data.reason; const quotedAmount = typeof data.quotedAmount === "number" ? data.quotedAmount : null; const floor = typeof data.floor === "number" ? data.floor : null; switch (reason) { case "below_floor": if (quotedAmount === null || floor === null) return null; return new _QuoteRejectedError({ reason, quotedAmount, floor, message }); case "non_positive": case "non_safe_integer": if (quotedAmount === null) return null; return new _QuoteRejectedError({ reason, quotedAmount, message }); case "no_baseline": return new _QuoteRejectedError({ reason, message }); } } }; var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::"; var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([ "below_floor", "non_positive", "non_safe_integer", "no_baseline" ]); var BoltzRefundError = class extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = "BoltzRefundError"; } }; // src/arkade-swaps.ts var import_sdk8 = require("@arkade-os/sdk"); // src/boltz-swap-provider.ts var import_sdk = require("@arkade-os/sdk"); var import_base = require("@scure/base"); var isSubmarineFailedStatus = (status) => { return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status); }; var isSubmarineFinalStatus = (status) => { return ["invoice.failedToPay", "transaction.claimed", "swap.expired"].includes(status); }; var isSubmarinePendingStatus = (status) => { return [ "swap.created", "transaction.mempool", "transaction.confirmed", "invoice.set", "invoice.pending", "invoice.paid", "transaction.claim.pending" ].includes(status); }; var isSubmarineRefundableStatus = (status) => { return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status); }; var SUBMARINE_STATUS_PROGRESSION = [ "swap.created", "invoice.set", "transaction.mempool", "transaction.confirmed", "invoice.pending", "invoice.paid", "transaction.claim.pending", "transaction.claimed" ]; var hasSubmarineStatusReached = (status, target) => { const progression = SUBMARINE_STATUS_PROGRESSION; const statusIndex = progression.indexOf(status); const targetIndex = progression.indexOf(target); return statusIndex >= 0 && targetIndex >= 0 && statusIndex >= targetIndex; }; var isSubmarineSuccessStatus = (status) => { return status === "transaction.claimed"; }; var isReverseFailedStatus = (status) => { return [ "invoice.expired", "transaction.failed", "transaction.refunded", "swap.expired" ].includes(status); }; var isReverseFinalStatus = (status) => { return [ "transaction.refunded", "transaction.failed", "invoice.settled", // normal status for completed swaps "invoice.expired", "swap.expired" ].includes(status); }; var isReversePendingStatus = (status) => { return ["swap.created", "transaction.mempool", "transaction.confirmed"].includes(status); }; var isReverseClaimableStatus = (status) => { return ["transaction.mempool", "transaction.confirmed"].includes(status); }; var isReverseSuccessStatus = (status) => { return status === "invoice.settled"; }; var isChainFailedStatus = (status) => { return ["transaction.failed", "swap.expired"].includes(status); }; var isChainClaimableStatus = (status) => { return ["transaction.server.mempool", "transaction.server.confirmed"].includes(status); }; var isChainFinalStatus = (status) => { return [ "transaction.refunded", "transaction.failed", "transaction.claimed", // normal status for completed swaps "swap.expired" ].includes(status); }; var isChainPendingStatus = (status) => { return [ "swap.created", "transaction.mempool", "transaction.confirmed", "transaction.lockupFailed", "transaction.server.mempool", "transaction.server.confirmed" ].includes(status); }; var isChainRefundableStatus = (status) => { return ["swap.expired"].includes(status); }; var isChainSignableStatus = (status) => { return ["transaction.claim.pending"].includes(status); }; var isChainSuccessStatus = (status) => { return status === "transaction.claimed"; }; var isPendingReverseSwap = (swap) => { return swap.type === "reverse"; }; var isPendingSubmarineSwap = (swap) => { return swap.type === "submarine"; }; var isPendingChainSwap = (swap) => { return swap.type === "chain"; }; var isSubmarineSwapRefundable = (swap) => { return isSubmarineRefundableStatus(swap.status) && isPendingSubmarineSwap(swap) && swap.refundable !== false && swap.refunded !== true; }; var isChainSwapRefundable = (swap) => { return isChainRefundableStatus(swap.status) && isPendingChainSwap(swap) && swap.request.from === "ARK"; }; var isReverseSwapClaimable = (swap) => { return isReverseClaimableStatus(swap.status) && isPendingReverseSwap(swap); }; var isChainSwapClaimable = (swap) => { return isChainClaimableStatus(swap.status) && isPendingChainSwap(swap); }; var isTimeoutBlockHeights = (data) => { return data && typeof data === "object" && typeof data.refund === "number" && typeof data.unilateralClaim === "number" && typeof data.unilateralRefund === "number" && typeof data.unilateralRefundWithoutReceiver === "number"; }; var isGetReverseSwapTxIdResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && typeof data.timeoutBlockHeight === "number"; }; var isGetSwapStatusResponse = (data) => { return data && typeof data === "object" && typeof data.status === "string" && (data.zeroConfRejected === void 0 || typeof data.zeroConfRejected === "boolean") && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && (data.transaction.confirmed === void 0 || typeof data.transaction.confirmed === "boolean") && (data.transaction.eta === void 0 || typeof data.transaction.eta === "number") && (data.transaction.hex === void 0 || typeof data.transaction.hex === "string") && (data.transaction.preimage === void 0 || typeof data.transaction.preimage === "string")); }; var isGetSubmarinePairsResponse = (data) => { return data && typeof data === "object" && data.ARK && typeof data.ARK === "object" && data.ARK.BTC && typeof data.ARK.BTC === "object" && typeof data.ARK.BTC.hash === "string" && typeof data.ARK.BTC.rate === "number" && data.ARK.BTC.limits && typeof data.ARK.BTC.limits === "object" && typeof data.ARK.BTC.limits.maximal === "number" && typeof data.ARK.BTC.limits.minimal === "number" && typeof data.ARK.BTC.limits.maximalZeroConf === "number" && data.ARK.BTC.fees && typeof data.ARK.BTC.fees === "object" && typeof data.ARK.BTC.fees.percentage === "number" && typeof data.ARK.BTC.fees.minerFees === "number"; }; var isGetReversePairsResponse = (data) => { return data && typeof data === "object" && data.BTC && typeof data.BTC === "object" && data.BTC.ARK && typeof data.BTC.ARK === "object" && data.BTC.ARK.hash && typeof data.BTC.ARK.hash === "string" && typeof data.BTC.ARK.rate === "number" && data.BTC.ARK.limits && typeof data.BTC.ARK.limits === "object" && typeof data.BTC.ARK.limits.maximal === "number" && typeof data.BTC.ARK.limits.minimal === "number" && data.BTC.ARK.fees && typeof data.BTC.ARK.fees === "object" && typeof data.BTC.ARK.fees.percentage === "number" && typeof data.BTC.ARK.fees.minerFees === "object" && typeof data.BTC.ARK.fees.minerFees.claim === "number" && typeof data.BTC.ARK.fees.minerFees.lockup === "number"; }; var isCreateSubmarineSwapResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && typeof data.expectedAmount === "number" && (data.address === void 0 || typeof data.address === "string") && (data.claimPublicKey === void 0 || typeof data.claimPublicKey === "string") && (data.acceptZeroConf === void 0 || typeof data.acceptZeroConf === "boolean") && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights)); }; var isGetSwapPreimageResponse = (data) => { return data && typeof data === "object" && typeof data.preimage === "string"; }; var isCreateReverseSwapResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && typeof data.invoice === "string" && (data.onchainAmount === void 0 || typeof data.onchainAmount === "number") && (data.lockupAddress === void 0 || typeof data.lockupAddress === "string") && (data.refundPublicKey === void 0 || typeof data.refundPublicKey === "string") && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights)); }; var isRefundSubmarineSwapResponse = (data) => { return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string"; }; var isRefundChainSwapResponse = (data) => { return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string"; }; var isGetChainPairsResponse = (data) => { return data && typeof data === "object" && data.ARK && data.BTC && typeof data.ARK === "object" && typeof data.BTC === "object" && data.ARK.BTC && data.BTC.ARK && typeof data.ARK.BTC === "object" && typeof data.BTC.ARK === "object" && typeof data.ARK.BTC.hash === "string" && typeof data.BTC.ARK.hash === "string" && typeof data.ARK.BTC.rate === "number" && typeof data.BTC.ARK.rate === "number" && data.ARK.BTC.limits && data.BTC.ARK.limits && typeof data.ARK.BTC.limits === "object" && typeof data.BTC.ARK.limits === "object" && typeof data.ARK.BTC.limits.maximal === "number" && typeof data.BTC.ARK.limits.maximal === "number" && typeof data.ARK.BTC.limits.minimal === "number" && typeof data.BTC.ARK.limits.minimal === "number" && typeof data.ARK.BTC.limits.maximalZeroConf === "number" && typeof data.BTC.ARK.limits.maximalZeroConf === "number" && data.ARK.BTC.fees && data.BTC.ARK.fees && typeof data.ARK.BTC.fees === "object" && typeof data.BTC.ARK.fees === "object" && typeof data.ARK.BTC.fees.percentage === "number" && typeof data.BTC.ARK.fees.percentage === "number" && typeof data.ARK.BTC.fees.minerFees === "object" && typeof data.BTC.ARK.fees.minerFees === "object" && typeof data.ARK.BTC.fees.minerFees.server === "number" && typeof data.BTC.ARK.fees.minerFees.server === "number" && data.ARK.BTC.fees.minerFees.user && data.BTC.ARK.fees.minerFees.user && typeof data.ARK.BTC.fees.minerFees.user === "object" && typeof data.BTC.ARK.fees.minerFees.user === "object" && typeof data.ARK.BTC.fees.minerFees.user.claim === "number" && typeof data.BTC.ARK.fees.minerFees.user.claim === "number" && typeof data.ARK.BTC.fees.minerFees.user.lockup === "number" && typeof data.BTC.ARK.fees.minerFees.user.lockup === "number"; }; var isSwapTree = (data) => { return data && typeof data === "object" && typeof data.claimLeaf === "object" && typeof data.claimLeaf.version === "number" && typeof data.claimLeaf.output === "string" && typeof data.refundLeaf === "object" && typeof data.refundLeaf.version === "number" && typeof data.refundLeaf.output === "string"; }; var isChainSwapDetailsResponse = (data) => { return data && typeof data === "object" && typeof data.amount === "number" && typeof data.lockupAddress === "string" && typeof data.serverPublicKey === "string" && typeof data.timeoutBlockHeight === "number" && (data.swapTree === void 0 || isSwapTree(data.swapTree)) && (data.timeouts === void 0 || isTimeoutBlockHeights(data.timeouts)) && (data.bip21 === void 0 || typeof data.bip21 === "string"); }; var isCreateChainSwapResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && isChainSwapDetailsResponse(data.claimDetails) && isChainSwapDetailsResponse(data.lockupDetails); }; var isGetChainClaimDetailsResponse = (data) => { return data && typeof data === "object" && typeof data.pubNonce === "string" && typeof data.publicKey === "string" && typeof data.transactionHash === "string"; }; var isPostChainClaimDetailsResponse = (data) => { return data && typeof data === "object" && (typeof data.pubNonce === "string" && typeof data.partialSignature === "string" || typeof data.pubNonce === "undefined" && typeof data.partialSignature === "undefined"); }; var isGetChainQuoteResponse = (data) => { return data && typeof data === "object" && typeof data.amount === "number"; }; var isPostChainQuoteResponse = (data) => { return data && typeof data === "object" && Object.keys(data).length === 0 && data.constructor === Object; }; var isPostBtcTransactionResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string"; }; var isLeaf = (data) => { return data && typeof data === "object" && typeof data.version === "number" && typeof data.output === "string"; }; var isTree = (data) => { return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && (data.covenantClaimLeaf === void 0 || isLeaf(data.covenantClaimLeaf)) && (data.refundWithoutBoltzLeaf === void 0 || isLeaf(data.refundWithoutBoltzLeaf)) && (data.unilateralClaimLeaf === void 0 || isLeaf(data.unilateralClaimLeaf)) && (data.unilateralRefundLeaf === void 0 || isLeaf(data.unilateralRefundLeaf)) && (data.unilateralRefundWithoutBoltzLeaf === void 0 || isLeaf(data.unilateralRefundWithoutBoltzLeaf)); }; var isDetails = (data) => { return data && typeof data === "object" && isTree(data.tree) && (data.amount === void 0 || typeof data.amount === "number") && typeof data.keyIndex === "number" && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && typeof data.transaction.vout === "number") && typeof data.lockupAddress === "string" && typeof data.serverPublicKey === "string" && (data.timeoutBlockHeight === void 0 || typeof data.timeoutBlockHeight === "number") && (data.timeoutBlockHeights === void 0 || isTimeoutBlockHeights(data.timeoutBlockHeights)) && (data.preimageHash === void 0 || typeof data.preimageHash === "string"); }; var isRestoredChainSwap = (data) => { return data && typeof data === "object" && typeof data.id === "string" && data.type === "chain" && typeof data.status === "string" && typeof data.createdAt === "number" && (data.from === "ARK" || data.from === "BTC") && (data.to === "ARK" || data.to === "BTC") && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && (data.invoice === void 0 || typeof data.invoice === "string") && (data.refundDetails === void 0 || isDetails(data.refundDetails)) && (data.claimDetails === void 0 || isDetails(data.claimDetails)); }; var isRestoredSubmarineSwap = (data) => { return data && typeof data === "object" && data.to === "BTC" && typeof data.id === "string" && data.from === "ARK" && data.type === "submarine" && typeof data.createdAt === "number" && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && typeof data.status === "string" && isDetails(data.refundDetails) && (data.invoice === void 0 || typeof data.invoice === "string"); }; var isRestoredReverseSwap = (data) => { return data && typeof data === "object" && data.to === "ARK" && typeof data.id === "string" && data.from === "BTC" && data.type === "reverse" && typeof data.createdAt === "number" && (data.preimageHash === void 0 || typeof data.preimageHash === "string") && typeof data.status === "string" && isDetails(data.claimDetails) && (data.invoice === void 0 || typeof data.invoice === "string"); }; var isCreateSwapsRestoreResponse = (data) => { return Array.isArray(data) && data.every( (item) => isRestoredChainSwap(item) || isRestoredReverseSwap(item) || isRestoredSubmarineSwap(item) ); }; var BASE_URLS = { bitcoin: "https://api.boltz.exchange", mutinynet: "https://api.boltz.mutinynet.arkade.sh", regtest: "http://localhost:9069" }; var isSwapNotFoundBody = (error) => { const needle = "could not find swap"; const fromJson = error.errorData?.error; if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle)) return true; return error.message.toLowerCase().includes(needle); }; var BoltzSwapProvider = class { wsUrl; apiUrl; network; referralId; inflightGets = /* @__PURE__ */ new Map(); /** @param config Provider configuration with network and optional API URL. */ constructor(config) { this.network = config.network; this.referralId = config.referralId ?? "arkade-ts-sdk"; const apiUrl = config.apiUrl || BASE_URLS[config.network]; if (!apiUrl) throw new Error(`API URL is required for network: ${config.network}`); this.apiUrl = apiUrl; this.wsUrl = this.apiUrl.replace(/^http(s)?:\/\//, "ws$1://").replace("9069", "9004") + "/v2/ws"; } /** Returns the Boltz API base URL. */ getApiUrl() { return this.apiUrl; } /** Returns the Boltz WebSocket URL (derived from apiUrl). */ getWsUrl() { return this.wsUrl; } /** Returns the configured network. */ getNetwork() { return this.network; } /** Returns current Lightning swap fees (submarine + reverse) from Boltz. */ async getFees() { const [submarine, reverse] = await Promise.all([ this.request("/v2/swap/submarine", "GET"), this.request("/v2/swap/reverse", "GET") ]); if (!isGetSubmarinePairsResponse(submarine)) throw new SchemaError({ message: "error fetching submarine fees" }); if (!isGetReversePairsResponse(reverse)) throw new SchemaError({ message: "error fetching reverse fees" }); return { submarine: { percentage: submarine.ARK.BTC.fees.percentage, minerFees: submarine.ARK.BTC.fees.minerFees }, reverse: { percentage: reverse.BTC.ARK.fees.percentage, minerFees: reverse.BTC.ARK.fees.minerFees } }; } /** Returns current Lightning swap min/max limits from Boltz. */ async getLimits() { const response = await this.request("/v2/swap/submarine", "GET"); if (!isGetSubmarinePairsResponse(response)) throw new SchemaError({ message: "error fetching limits" }); return { min: response.ARK.BTC.limits.minimal, max: response.ARK.BTC.limits.maximal }; } /** Returns the current BTC chain tip height from Boltz. */ async getChainHeight() { const response = await this.request("/v2/chain/heights", "GET"); if (typeof response?.BTC !== "number") throw new SchemaError({ message: "error fetching chain heights" }); return response.BTC; } /** Gets the lockup transaction ID for a reverse swap. */ async getReverseSwapTxId(id) { const res = await this.request( `/v2/swap/reverse/${id}/transaction`, "GET" ); if (!isGetReverseSwapTxIdResponse(res)) throw new SchemaError({ message: `error fetching txid for swap: ${id}` }); return res; } /** * 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. */ async getSwapStatus(id) { let response; try { response = await this.request(`/v2/swap/${id}`, "GET"); } catch (error) { if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) { throw new SwapNotFoundError(id, error.errorData); } throw error; } if (!isGetSwapStatusResponse(response)) throw new SchemaError({ message: `error fetching status for swap: ${id}` }); return response; } /** Gets the preimage for a settled submarine swap (proof of payment). */ async getSwapPreimage(id) { const res = await this.request( `/v2/swap/submarine/${id}/preimage`, "GET" ); if (!isGetSwapPreimageResponse(res)) throw new SchemaError({ message: `error fetching preimage for swap: ${id}` }); return res; } /** Creates a submarine swap (Arkade → Lightning) on Boltz. */ async createSubmarineSwap({ invoice, refundPublicKey }) { if (refundPublicKey.length != 66) { throw new SwapError({ message: "refundPublicKey must be a compressed public key" }); } const requestBody = { from: "ARK", to: "BTC", invoice, refundPublicKey, ...this.referralId ? { referralId: this.referralId } : {} }; const response = await this.request( "/v2/swap/submarine", "POST", requestBody ); if (!isCreateSubmarineSwapResponse(response)) throw new SchemaError({ message: "Error creating submarine swap" }); return response; } /** Creates a reverse swap (Lightning → Arkade) on Boltz. */ async createReverseSwap({ invoiceAmount, claimPublicKey, preimageHash, description, descriptionHash }) { if (claimPublicKey.length != 66) { throw new SwapError({ message: "claimPublicKey must be a compressed public key" }); } if (descriptionHash !== void 0 && !/^[0-9a-fA-F]{64}$/.test(descriptionHash)) { throw new SwapError({ message: "descriptionHash must be a 32-byte SHA256 (64 hex chars)" }); } const requestBody = { from: "BTC", to: "ARK", invoiceAmount, claimPublicKey, preimageHash, ...descriptionHash ? { descriptionHash } : { description: description?.trim() || "Send to Arkade address" }, ...this.referralId ? { referralId: this.referralId } : {} }; const response = await this.request( "/v2/swap/reverse", "POST", requestBody ); if (!isCreateReverseSwapResponse(response)) throw new SchemaError({ message: "Error creating reverse swap" }); return response; } /** Creates a chain swap (ARK ↔ BTC) on Boltz. */ async createChainSwap({ to, from, preimageHash, feeSatsPerByte, claimPublicKey, refundPublicKey, serverLockAmount, userLockAmount }) { if (["BTC", "ARK"].indexOf(to) === -1) throw new SwapError({ message: "Invalid 'to' chain" }); if (["BTC", "ARK"].indexOf(from) === -1) throw new SwapError({ message: "Invalid 'from' chain" }); if (to === from) throw new SwapError({ message: "Invalid swap direction" }); if (!preimageHash || preimageHash.length != 64) throw new SwapError({ message: "Invalid preimageHash" }); if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" }); if (serverLockAmount !== void 0 && userLockAmount !== void 0 || serverLockAmount === void 0 && userLockAmount === void 0) throw new SwapError({ message: "Either serverLockAmount or userLockAmount must be provided" }); if (userLockAmount !== void 0 && userLockAmount <= 0) throw new SwapError({ message: "Invalid userLockAmount" }); if (serverLockAmount !== void 0 && serverLockAmount <= 0) throw new SwapError({ message: "Invalid serverLockAmount" }); if (claimPublicKey.length != 66) { throw new SwapError({ message: "claimPublicKey must be a compressed public key" }); } if (refundPublicKey.length != 66) { throw new SwapError({ message: "refundPublicKey must be a compressed public key" }); } const requestBody = { to, from, preimageHash, feeSatsPerByte, claimPublicKey, refundPublicKey, serverLockAmount, userLockAmount, ...this.referralId ? { referralId: this.referralId } : {} }; const response = await this.request( "/v2/swap/chain", "POST", requestBody ); if (!isCreateChainSwapResponse(response)) throw new SchemaError({ message: "Error creating chain swap" }); return response; } /** Requests Boltz co-signature for a submarine swap refund. Returns signed transaction + checkpoint. */ async refundSubmarineSwap(swapId, transaction, checkpoint) { const requestBody = { checkpoint: import_base.base64.encode(checkpoint.toPSBT()), transaction: import_base.base64.encode(transaction.toPSBT()) }; const response = await this.request( `/v2/swap/submarine/${swapId}/refund/ark`, "POST", requestBody ); if (!isRefundSubmarineSwapResponse(response)) throw new SchemaError({ message: "Error refunding submarine swap" }); return { transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)), checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint)) }; } /** Requests Boltz co-signature for a chain swap refund. Returns signed transaction + checkpoint. */ async refundChainSwap(swapId, transaction, checkpoint) { const requestBody = { checkpoint: import_base.base64.encode(checkpoint.toPSBT()), transaction: import_base.base64.encode(transaction.toPSBT()) }; const response = await this.request( `/v2/swap/chain/${swapId}/refund/ark`, "POST", requestBody ); if (!isRefundChainSwapResponse(response)) throw new SchemaError({ message: "Error refunding chain swap" }); return { transaction: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.transaction)), checkpoint: import_sdk.Transaction.fromPSBT(import_base.base64.decode(response.checkpoint)) }; } /** * 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. */ async monitorSwap(swapId, update) { return new Promise((resolve, reject) => { let settled = false; let lastStatus = null; let pollTimer = null; let webSocket = null; let connectionTimeout = null; const cleanup = () => { if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (webSocket) { try { webSocket.close(); } catch { } webSocket = null; } }; const finish = (err) => { if (settled) return; settled = true; cleanup(); if (err) reject(err); else resolve(); }; const handleStatus = (status, data) => { if (settled) return; if (status === lastStatus) return; lastStatus = status; const negotiable = status === "transaction.lockupFailed" && data?.failureDetails?.actual !== void 0 && data?.failureDetails?.expected !== void 0; switch (status) { case "invoice.settled": case "transaction.claimed": case "transaction.refunded": case "invoice.expired": case "invoice.failedToPay": case "transaction.failed": case "swap.expired": update(status, data); finish(); return; case "transaction.lockupFailed": update(status, data); if (!negotiable) finish(); return; case "invoice.paid": case "invoice.pending": case "invoice.set": case "swap.created": case "transaction.mempool": case "transaction.confirmed": case "transaction.claim.pending": case "transaction.server.mempool": case "transaction.server.confirmed": update(status, data); return; } }; const startPolling = () => { if (settled || pollTimer) return; const poll = async () => { if (settled) return; try { const result = await this.getSwapStatus(swapId); handleStatus(result.status, { ...result, id: swapId }); } catch { } }; pollTimer = setInterval(poll, 1e3); poll(); }; try { webSocket = new globalThis.WebSocket(this.wsUrl); connectionTimeout = setTimeout(() => { try { webSocket?.close(); } catch { } webSocket = null; startPolling(); }, 5e3); webSocket.onerror = () => { if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } webSocket = null; startPolling(); }; webSocket.onopen = () => { if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } webSocket?.send( JSON.stringify({ op: "subscribe", channel: "swap.update", args: [swapId] }) ); }; webSocket.onclose = () => { if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } if (!settled && !pollTimer) startPolling(); }; webSocket.onmessage = (rawMsg) => { try { const msg = JSON.parse(rawMsg.data); if (msg.event !== "update" || msg.args?.[0]?.id !== swapId) return; if (msg.args[0].error) { finish(new SwapError({ message: msg.args[0].error })); return; } handleStatus(msg.args[0].status, msg.args[0]); } catch { } }; } catch { webSocket = null; startPolling(); } }); } /** Returns current chain swap fees for a given pair (e.g. ARK→BTC). */ async getChainFees(from, to) { if (from === to) { throw new SwapError({ message: "Invalid chain pair" }); } const response = await this.request("/v2/swap/chain", "GET"); if (!isGetChainPairsResponse(response)) throw new SchemaError({ message: "error fetching fees" }); if (!response[from]?.[to]) { throw new SchemaError({ message: `unsupported chain pair: ${from} -> ${to}` }); } return response[from][to].fees; } /** Returns current chain swap min/max limits for a given pair. */ async getChainLimits(from, to) { if (from === to) { throw new SwapError({ message: "Invalid chain pair" }); } const response = await this.request("/v2/swap/chain", "GET"); if (!isGetChainPairsResponse(response)) throw new SchemaError({ message: "error fetching limits" }); if (!response[from]?.[to]) { throw new SchemaError({ message: `unsupported chain pair: ${from} -> ${to}` }); } return { min: response[from][to].limits.minimal, max: response[from][to].limits.maximal }; } /** Gets claim details (pubNonce, publicKey, transactionHash) for cooperative chain swap claiming. */ async getChainClaimDetails(swapId) { const response = await this.request( `/v2/swap/chain/${swapId}/claim`, "GET" ); if (!isGetChainClaimDetailsResponse(response)) throw new SchemaError({ message: `error fetching claim details for swap: ${swapId}` }); return response; } /** Gets a renegotiated quote for a chain swap when lockup amount differs from expected. */ async getChainQuote(swapId) { const response = await this.request( `/v2/swap/chain/${swapId}/quote`, "GET" ); if (!isGetChainQuoteResponse(response)) throw new SchemaError({ message: `error fetching quote for swap: ${swapId}` }); return response; } /** Accepts a renegotiated quote amount for a chain swap. */ async postChainQuote(swapId, request) { const response = await this.request( `/v2/swap/chain/${swapId}/quote`, "POST", request ); if (!isPostChainQuoteResponse(response)) throw new SchemaError({ message: `error posting quote for swap: ${swapId}` }); return response; } /** Broadcasts a raw BTC transaction through Boltz. */ async postBtcTransaction(hex9) { const requestBody = { hex: hex9 }; const response = await this.request( "/v2/chain/BTC/transaction", "POST", requestBody ); if (!isPostBtcTransactionResponse(response)) throw new SchemaError({ message: "error posting BTC transaction" }); return response; } /** Posts claim details (preimage + signing data) or cooperative signature for a chain swap. */ async postChainClaimDetails(swapId, request) { const response = await this.request( `/v2/swap/chain/${swapId}/claim`, "POST", request ); if (!isPostChainClaimDetailsResponse(response)) throw new SchemaError({ message: `error posting claim details for swap: ${swapId}` }); return response; } /** Restores swaps from Boltz API using the wallet's public key. */ async restoreSwaps(publicKey) { const requestBody = { publicKey }; const response = await this.request( "/v2/swap/restore", "POST", requestBody ); if (!isCreateSwapsRestoreResponse(response)) throw new SchemaError({ message: "Invalid schema in response for swap restoration" }); return response; } async request(path, method, body) { if (method === "GET") { const inflight = this.inflightGets.get(path); if (inflight) return inflight; const p = this.doRequest(path, method).finally(() => { this.inflightGets.delete(path); }); this.inflightGets.set(path, p); return p; } return this.doRequest(path, method, body); } async doRequest(path, method, body) { const url = `${this.apiUrl}${path}`; try { const response = await globalThis.fetch(url, { method, headers: { "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : void 0 }); if (!response.ok) { const errorBody = await response.text(); let errorData; try { errorData = JSON.parse(errorBody); } catch { } const message = `Boltz API error: ${response.status} ${errorBody}`; throw new NetworkError(message, response.status, errorData); } if (response.headers.get("content-length") === "0") { throw new NetworkError("Empty response from Boltz API"); } return await response.json(); } catch (error) { if (error instanceof NetworkError) throw error; throw new NetworkError(`Request to ${url} failed: ${error.message}`); } } }; // src/arkade-swaps.ts var import_sha23 = require("@noble/hashes/sha2.js"); var import_legacy2 = require("@noble/hashes/legacy.js"); var import_base9 = require("@scure/base"); var import_secp256k13 = require("@noble/curves/secp256k1.js"); var import_utils3 = require("@noble/hashes/utils.js"); var import_btc_signer5 = require("@scure/btc-signer"); var import_utils4 = require("@scure/btc-signer/utils.js"); // src/utils/musig.ts var import_secp256k1 = require("@noble/curves/secp256k1.js"); var import_base2 = require("@scure/base"); var import_musig2 = require("@scure/btc-signer/musig2.js"); var import_utils = require("@scure/btc-signer/utils.js"); var findKeyIndex = (keys, target) => keys.findIndex((k) => (0, import_utils.equalBytes)(target, k)); var assertPublicKeys = (keys) => { const seen = /* @__PURE__ */ new Set(); for (const key of keys) { if (key.length !== 33) throw new Error(`public key must be 33 bytes, got ${key.length}`); const enc = import_base2.hex.encode(key); if (seen.has(enc)) throw new Error(`duplicate public key ${enc}`); seen.add(enc); } }; var aggregateKeys = (publicKeys, tweak) => { assertPublicKeys([...publicKeys]); return (0, import_musig2.keyAggExport)((0, import_musig2.keyAggregate)([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : [])); }; var MusigKeyAgg = class _MusigKeyAgg { constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, internalKey, _tweak) { this.privateKey = privateKey; this.myPublicKey = myPublicKey; this.publicKeys = publicKeys; this.myIndex = myIndex; this.aggPubkey = aggPubkey; this.internalKey = internalKey; this._tweak = _tweak; } xonlyTweakAdd(tweak) { if (this._tweak) throw new Error("musig key already tweaked"); return new _MusigKeyAgg( this.privateKey, this.myPublicKey, this.publicKeys, this.myIndex, aggregateKeys(this.publicKeys, tweak), this.internalKey, tweak ); } message(msg) { return new MusigWithMessage( this.privateKey, this.myPublicKey, this.publicKeys, this.myIndex, this.aggPubkey, this._tweak, msg ); } }; var MusigWithMessage = class { constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, tweak, msg) { this.privateKey = privateKey; this.myPublicKey = myPublicKey; this.publicKeys = publicKeys; this.myIndex = myIndex; this.aggPubkey = aggPubkey; this.tweak = tweak; this.msg = msg; } generateNonce() { const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg); return new MusigWithNonce( this.privateKey, this.myPublicKey, this.publicKeys, this.myIndex, this.tweak, this.msg, nonce ); } }; var MusigWithNonce = class { constructor(privateKey, myPublicKey, publicKeys, myIndex, tweak, msg, nonce) { this.privateKey = privateKey; this.myPublicKey = myPublicKey; this.publicKeys = publicKeys; this.myIndex = myIndex; this.tweak = tweak; this.msg = msg; this.nonce = nonce; } get publicNonce() { return this.nonce.public; } aggregateNonces(nonces) { const pairs = Array.from(nonces); const ours = pairs.find(([k]) => (0, import_utils.equalBytes)(this.myPublicKey, k)); if (!ours) { pairs.push([this.myPublicKey, this.publicNonce]); } else if (!(0, import_utils.equalBytes)(ours[1], this.publicNonce)) { throw new Error("nonce for our public key does not match"); } if (this.publicKeys.length !== pairs.length) { throw new Error("number of nonces != number of public keys"); } const nonceByKey = /* @__PURE__ */ new Map(); for (const [key, nonce] of pairs) { nonceByKey.set(import_base2.hex.encode(key), nonce); } const ordered = []; for (const key of this.publicKeys) { const n = nonceByKey.get(import_base2.hex.encode(key)); if (!n) throw new Error("missing nonce for public key"); ordered.push(n); } const aggregatedNonce = (0, import_musig2.nonceAggregate)([...ordered]); return new MusigNoncesAggregated( this.privateKey, this.publicKeys, this.myIndex, this.tweak, this.msg, this.nonce, Object.freeze(ordered), aggregatedNonce ); } }; var MusigNoncesAggregated = class { constructor(privateKey, publicKeys, myIndex, tweak, msg, nonce, pubNonces, aggregatedNonce) { this.privateKey = privateKey; this.publicKeys = publicKeys; this.myIndex = myIndex; this.tweak = tweak; this.msg =