UNPKG

@arkade-os/boltz-swap

Version:

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

1,014 lines (1,005 loc) 39.3 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, { ArkadeLightning: () => ArkadeLightning, BoltzSwapProvider: () => BoltzSwapProvider, InsufficientFundsError: () => InsufficientFundsError, InvoiceExpiredError: () => InvoiceExpiredError, NetworkError: () => NetworkError, StorageProvider: () => StorageProvider, SwapError: () => SwapError, decodeInvoice: () => decodeInvoice, getInvoicePaymentHash: () => getInvoicePaymentHash, getInvoiceSatoshis: () => getInvoiceSatoshis }); module.exports = __toCommonJS(index_exports); // src/utils/polling.ts function poll(fn, condition, delayMs, maxAttempts) { let attempts = 0; return new Promise((resolve, reject) => { const executePoll = async () => { attempts++; try { const result = await fn(); if (condition(result)) { return resolve(result); } else if (attempts >= maxAttempts) { return reject(new Error("Polling timed out.")); } else { setTimeout(executePoll, delayMs); } } catch (error) { if (attempts >= maxAttempts) { return reject(error); } setTimeout(executePoll, delayMs); } }; executePoll(); }); } // src/errors.ts var SwapError = class extends Error { isClaimable; isRefundable; pendingSwap; constructor(options = {}) { super(options.message ?? "Error during swap."); 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 { constructor(message) { super(message); this.name = "NetworkError"; } }; 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 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"; } }; // src/arkade-lightning.ts var import_sdk = require("@arkade-os/sdk"); var import_sha2 = require("@noble/hashes/sha2"); var import_base = require("@scure/base"); var import_utils = require("@noble/hashes/utils"); var import_btc_signer = require("@scure/btc-signer"); var import_legacy = require("@noble/hashes/legacy"); // src/utils/decoding.ts var import_light_bolt11_decoder = __toESM(require("light-bolt11-decoder"), 1); var decodeInvoice = (invoice) => { const decoded = import_light_bolt11_decoder.default.decode(invoice); const millisats = Number(decoded.sections.find((s) => s.name === "amount")?.value ?? "0"); return { expiry: decoded.expiry ?? 3600, amountSats: Math.floor(millisats / 1e3), description: decoded.sections.find((s) => s.name === "description")?.value ?? "", paymentHash: decoded.sections.find((s) => s.name === "payment_hash")?.value ?? "" }; }; var getInvoiceSatoshis = (invoice) => { return decodeInvoice(invoice).amountSats; }; var getInvoicePaymentHash = (invoice) => { return decodeInvoice(invoice).paymentHash; }; // src/arkade-lightning.ts var DEFAULT_TIMEOUT_CONFIG = { swapExpiryBlocks: 144, invoiceExpirySeconds: 3600, claimDelayBlocks: 10 }; var DEFAULT_FEE_CONFIG = { maxMinerFeeSats: 5e3, maxSwapFeeSats: 1e3 }; var DEFAULT_RETRY_CONFIG = { maxAttempts: 5, delayMs: 2e3 }; var ArkadeLightning = class { wallet; arkProvider; swapProvider; storageProvider; indexerProvider; config; constructor(config) { if (!config.wallet) throw new Error("Wallet is required."); if (!config.arkProvider) throw new Error("Ark provider is required."); if (!config.swapProvider) throw new Error("Swap provider is required."); if (!config.indexerProvider) throw new Error("Indexer provider is required."); this.wallet = config.wallet; this.arkProvider = config.arkProvider; this.swapProvider = config.swapProvider; this.storageProvider = config.storageProvider ?? null; this.indexerProvider = config.indexerProvider; this.config = { ...config, refundHandler: config.refundHandler ?? { onRefundNeeded: async () => { } }, timeoutConfig: { ...DEFAULT_TIMEOUT_CONFIG, ...config.timeoutConfig }, feeConfig: { ...DEFAULT_FEE_CONFIG, ...config.feeConfig }, retryConfig: { ...DEFAULT_RETRY_CONFIG, ...config.retryConfig } }; } // receive from lightning = reverse submarine swap // // 1. create invoice by creating a reverse swap // 2. monitor incoming payment by waiting for the hold invoice to be paid // 3. claim the VHTLC by creating a virtual transaction that spends the VHTLC output // 4. return the preimage and the swap info async createLightningInvoice(args) { return new Promise((resolve, reject) => { this.createReverseSwap(args).then((pendingSwap) => { this.storageProvider?.savePendingReverseSwap(pendingSwap); const decodedInvoice = decodeInvoice(pendingSwap.response.invoice); resolve({ amount: pendingSwap.response.onchainAmount, expiry: decodedInvoice.expiry, invoice: pendingSwap.response.invoice, paymentHash: decodedInvoice.paymentHash, pendingSwap, preimage: pendingSwap.preimage }); }).catch(reject); }); } /** * Sends a Lightning payment. * 1. decode the invoice to get the amount and destination * 2. create submarine swap with the decoded invoice * 3. send the swap address and expected amount to the wallet to create a transaction * 4. wait for the swap settlement and return the preimage and txid * @param args - The arguments for sending a Lightning payment. * @returns The result of the payment. */ async sendLightningPayment(args) { return new Promise((resolve, reject) => { this.createSubmarineSwap(args).then((pendingSwap) => { if (args.maxFeeSats) { const invoiceAmount = decodeInvoice(args.invoice).amountSats; const fees = pendingSwap.response.expectedAmount - invoiceAmount; if (fees > args.maxFeeSats) { reject(new SwapError({ message: `Swap fees ${fees} exceed max allowed ${args.maxFeeSats}` })); } } this.storageProvider?.savePendingSubmarineSwap(pendingSwap); this.wallet.sendBitcoin({ address: pendingSwap.response.address, amount: pendingSwap.response.expectedAmount }).then((txid) => { this.waitForSwapSettlement(pendingSwap).then(async () => { this.storageProvider?.deletePendingSubmarineSwap(pendingSwap.response.id); const finalStatus = await this.swapProvider.getSwapStatus(pendingSwap.response.id); const preimage = finalStatus.transaction?.preimage ?? ""; resolve({ amount: pendingSwap.response.expectedAmount, preimage, txid }); }).catch(({ isRefundable }) => { if (isRefundable) { this.refundVHTLC(pendingSwap).then(reject).catch(reject).finally(() => { this.storageProvider?.deletePendingSubmarineSwap(pendingSwap.response.id); }); } else { reject(new TransactionFailedError()); } }); }).catch(reject); }); }); } // create reverse submarine swap async createSubmarineSwap(args) { const refundPublicKey = import_base.hex.encode(this.wallet.xOnlyPublicKey()); if (!refundPublicKey) throw new SwapError({ message: "Failed to get refund public key from wallet" }); const invoice = args.invoice; if (!invoice) throw new SwapError({ message: "Invoice is required" }); const swapRequest = { invoice, refundPublicKey }; const swapResponse = await this.swapProvider.createSubmarineSwap(swapRequest); return { request: swapRequest, response: swapResponse, status: "invoice.set" }; } // create reverse submarine swap async createReverseSwap(args) { if (args.amount <= 0) throw new SwapError({ message: "Amount must be greater than 0" }); const claimPublicKey = import_base.hex.encode(this.wallet.xOnlyPublicKey()); if (!claimPublicKey) throw new SwapError({ message: "Failed to get claim public key from wallet" }); const preimage = (0, import_utils.randomBytes)(32); const preimageHash = import_base.hex.encode((0, import_sha2.sha256)(preimage)); if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" }); const swapRequest = { invoiceAmount: args.amount, claimPublicKey, preimageHash }; const swapResponse = await this.swapProvider.createReverseSwap(swapRequest); return { preimage: import_base.hex.encode(preimage), request: swapRequest, response: swapResponse, status: "swap.created" }; } // claim the VHTLC by creating a virtual transaction that spends the VHTLC output async claimVHTLC(pendingSwap) { const preimage = import_base.hex.decode(pendingSwap.preimage); const aspInfo = await this.arkProvider.getInfo(); const address = await this.wallet.getAddress(); let receiverXOnlyPublicKey = this.wallet.xOnlyPublicKey(); if (receiverXOnlyPublicKey.length == 33) { receiverXOnlyPublicKey = receiverXOnlyPublicKey.slice(1); } else if (receiverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid receiver public key length: ${receiverXOnlyPublicKey.length}`); } let serverXOnlyPublicKey = import_base.hex.decode(aspInfo.signerPubkey); if (serverXOnlyPublicKey.length == 33) { serverXOnlyPublicKey = serverXOnlyPublicKey.slice(1); } else if (serverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid server public key length: ${serverXOnlyPublicKey.length}`); } const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({ network: aspInfo.network, preimageHash: (0, import_sha2.sha256)(preimage), receiverPubkey: import_base.hex.encode(receiverXOnlyPublicKey), senderPubkey: pendingSwap.response.refundPublicKey, serverPubkey: import_base.hex.encode(serverXOnlyPublicKey), timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights }); if (!vhtlcScript) throw new Error("Failed to create VHTLC script for reverse swap"); if (vhtlcAddress !== pendingSwap.response.lockupAddress) throw new Error("Boltz is trying to scam us"); const spendableVtxos = await this.indexerProvider.getVtxos({ scripts: [import_base.hex.encode(vhtlcScript.pkScript)], spendableOnly: true }); if (spendableVtxos.vtxos.length === 0) throw new Error("No spendable virtual coins found"); const vtxo = spendableVtxos.vtxos[0]; const vhtlcIdentity = { sign: async (tx, inputIndexes) => { const cpy = tx.clone(); let signedTx = await this.wallet.sign(cpy, inputIndexes); signedTx = import_btc_signer.Transaction.fromPSBT(signedTx.toPSBT(), { allowUnknown: true }); (0, import_sdk.setArkPsbtField)(signedTx, 0, import_sdk.ConditionWitness, [preimage]); return signedTx; }, xOnlyPublicKey: receiverXOnlyPublicKey, signerSession: this.wallet.signerSession }; const serverUnrollScript = import_sdk.CSVMultisigTapscript.encode({ pubkeys: [serverXOnlyPublicKey], timelock: { type: aspInfo.unilateralExitDelay < 512 ? "blocks" : "seconds", value: aspInfo.unilateralExitDelay } }); const { arkTx, checkpoints } = (0, import_sdk.buildOffchainTx)( [ { ...spendableVtxos.vtxos[0], tapLeafScript: vhtlcScript.claim(), tapTree: vhtlcScript.encode() } ], [ { amount: BigInt(vtxo.value), script: import_sdk.ArkAddress.decode(address).pkScript } ], serverUnrollScript ); const signedArkTx = await vhtlcIdentity.sign(arkTx); const { arkTxid, finalArkTx, signedCheckpointTxs } = await this.arkProvider.submitTx( import_base.base64.encode(signedArkTx.toPSBT()), checkpoints.map((c) => import_base.base64.encode(c.toPSBT())) ); if (!this.validFinalArkTx(finalArkTx, serverXOnlyPublicKey, vhtlcScript.leaves)) { throw new Error("Invalid final Ark transaction"); } const finalCheckpoints = await Promise.all( signedCheckpointTxs.map(async (c) => { const tx = import_btc_signer.Transaction.fromPSBT(import_base.base64.decode(c), { allowUnknown: true }); const signedCheckpoint = await vhtlcIdentity.sign(tx, [0]); return import_base.base64.encode(signedCheckpoint.toPSBT()); }) ); await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints); this.storageProvider?.deletePendingReverseSwap(pendingSwap.response.id); } async refundVHTLC(pendingSwap) { const aspInfo = await this.arkProvider.getInfo(); const address = await this.wallet.getAddress(); if (!address) throw new Error("Failed to get ark address from service worker wallet"); let receiverXOnlyPublicKey = this.wallet.xOnlyPublicKey(); if (receiverXOnlyPublicKey.length == 33) { receiverXOnlyPublicKey = receiverXOnlyPublicKey.slice(1); } else if (receiverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid receiver public key length: ${receiverXOnlyPublicKey.length}`); } let serverXOnlyPublicKey = import_base.hex.decode(aspInfo.signerPubkey); if (serverXOnlyPublicKey.length == 33) { serverXOnlyPublicKey = serverXOnlyPublicKey.slice(1); } else if (serverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid server public key length: ${serverXOnlyPublicKey.length}`); } const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({ network: aspInfo.network, preimageHash: import_base.hex.decode(getInvoicePaymentHash(pendingSwap.request.invoice)), receiverPubkey: pendingSwap.response.claimPublicKey, senderPubkey: import_base.hex.encode(this.wallet.xOnlyPublicKey()), serverPubkey: aspInfo.signerPubkey, timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights }); if (!vhtlcScript) throw new Error("Failed to create VHTLC script for reverse swap"); if (vhtlcAddress !== pendingSwap.response.address) throw new Error("Boltz is trying to scam us"); const spendableVtxos = await this.indexerProvider.getVtxos({ scripts: [import_base.hex.encode(vhtlcScript.pkScript)], spendableOnly: true }); if (spendableVtxos.vtxos.length === 0) { throw new Error("No spendable virtual coins found"); } const vhtlcIdentity = { sign: async (tx, inputIndexes) => { const cpy = tx.clone(); let signedTx = await this.wallet.sign(cpy, inputIndexes); return import_btc_signer.Transaction.fromPSBT(signedTx.toPSBT(), { allowUnknown: true }); }, xOnlyPublicKey: receiverXOnlyPublicKey, signerSession: this.wallet.signerSession }; const serverUnrollScript = import_sdk.CSVMultisigTapscript.encode({ pubkeys: [serverXOnlyPublicKey], timelock: { type: aspInfo.unilateralExitDelay < 512 ? "blocks" : "seconds", value: aspInfo.unilateralExitDelay } }); const { arkTx, checkpoints } = (0, import_sdk.buildOffchainTx)( [ { ...spendableVtxos.vtxos[0], tapLeafScript: vhtlcScript.refund(), tapTree: vhtlcScript.encode() } ], [ { amount: BigInt(spendableVtxos.vtxos[0].value), script: import_sdk.ArkAddress.decode(address).pkScript } ], serverUnrollScript ); const signedArkTx = await vhtlcIdentity.sign(arkTx); const { arkTxid, finalArkTx, signedCheckpointTxs } = await this.arkProvider.submitTx( import_base.base64.encode(signedArkTx.toPSBT()), checkpoints.map((c) => import_base.base64.encode(c.toPSBT())) ); if (!this.validFinalArkTx(finalArkTx, serverXOnlyPublicKey, vhtlcScript.leaves)) { throw new Error("Invalid final Ark transaction"); } const finalCheckpoints = await Promise.all( signedCheckpointTxs.map(async (c) => { const tx = import_btc_signer.Transaction.fromPSBT(import_base.base64.decode(c), { allowUnknown: true }); const signedCheckpoint = await vhtlcIdentity.sign(tx, [0]); return import_base.base64.encode(signedCheckpoint.toPSBT()); }) ); await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints); console.log("Successfully claimed VHTLC! Transaction ID:", arkTxid); } async waitAndClaim(pendingSwap) { return new Promise((resolve, reject) => { const onStatusUpdate = async (status) => { switch (status) { case "transaction.mempool": case "transaction.confirmed": this.claimVHTLC(pendingSwap).catch(reject); break; case "invoice.settled": { const status2 = await this.swapProvider.getSwapStatus(pendingSwap.response.id); resolve({ txid: status2.transaction?.id ?? "" }); break; } case "invoice.expired": reject(new InvoiceExpiredError({ isRefundable: true, pendingSwap })); break; case "swap.expired": reject(new SwapExpiredError({ isRefundable: true, pendingSwap })); break; case "transaction.failed": reject(new TransactionFailedError()); break; case "transaction.refunded": reject(new TransactionRefundedError()); break; default: break; } }; this.swapProvider.monitorSwap(pendingSwap.response.id, onStatusUpdate); }); } /** * Waits for the swap settlement. * @param pendingSwap - The pending swap. * @returns The status of the swap settlement. */ async waitForSwapSettlement(pendingSwap) { return new Promise((resolve, reject) => { const onStatusUpdate = async (status) => { switch (status) { case "swap.expired": reject(new SwapExpiredError({ isRefundable: true, pendingSwap })); break; case "invoice.failedToPay": reject(new InvoiceFailedToPayError({ isRefundable: true, pendingSwap })); break; case "transaction.lockupFailed": reject(new TransactionLockupFailedError({ isRefundable: true, pendingSwap })); break; case "transaction.claimed": resolve(); break; default: break; } }; this.swapProvider.monitorSwap(pendingSwap.response.id, onStatusUpdate); }); } /** * Waits for the swap settlement. * @param swapData - The swap data to wait for. * @returns The status of the swap settlement. */ async waitForSwapSettlementold(swapData) { const status = await poll( () => this.swapProvider.getSwapStatus(swapData.response.id), (status2) => status2.status === "transaction.claimed", this.config.retryConfig.delayMs ?? 2e3, this.config.retryConfig.maxAttempts ?? 5 ).catch((err) => { this.config.refundHandler.onRefundNeeded(swapData); throw new SwapError({ message: `Swap settlement failed: ${err.message}`, isRefundable: true, pendingSwap: { ...swapData } }); }); if (status.status === "transaction.claimed" && this.storageProvider) { await this.storageProvider.deletePendingSubmarineSwap(swapData.response.id); } return status; } // validators /** * Validates the final Ark transaction. * checks that all inputs have a signature for the given pubkey * and the signature is correct for the given tapscript leaf * TODO: This is a simplified check, we should verify the actual signatures * @param finalArkTx The final Ark transaction in PSBT format. * @param _pubkey The public key of the user. * @param _tapLeaves The taproot script leaves. * @returns True if the final Ark transaction is valid, false otherwise. */ validFinalArkTx = (finalArkTx, _pubkey, _tapLeaves) => { const tx = import_btc_signer.Transaction.fromPSBT(import_base.base64.decode(finalArkTx), { allowUnknown: true }); if (!tx) return false; const inputs = []; for (let i = 0; i < tx.inputsLength; i++) { inputs.push(tx.getInput(i)); } return inputs.every((input) => input.witnessUtxo); }; /** * Creates a VHTLC script for the swap. * works for submarine swaps and reverse swaps * it creates a VHTLC script that can be used to claim or refund the swap * it validates the receiver, sender and server public keys are x-only * it validates the VHTLC script matches the expected lockup address * @param param0 - The parameters for creating the VHTLC script. * @returns The created VHTLC script. */ createVHTLCScript({ network, preimageHash, receiverPubkey, senderPubkey, serverPubkey, timeoutBlockHeights }) { let receiverXOnlyPublicKey = import_base.hex.decode(receiverPubkey); if (receiverXOnlyPublicKey.length == 33) { receiverXOnlyPublicKey = receiverXOnlyPublicKey.slice(1); } else if (receiverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid receiver public key length: ${receiverXOnlyPublicKey.length}`); } let senderXOnlyPublicKey = import_base.hex.decode(senderPubkey); if (senderXOnlyPublicKey.length == 33) { senderXOnlyPublicKey = senderXOnlyPublicKey.slice(1); } else if (senderXOnlyPublicKey.length !== 32) { throw new Error(`Invalid sender public key length: ${senderXOnlyPublicKey.length}`); } let serverXOnlyPublicKey = import_base.hex.decode(serverPubkey); if (serverXOnlyPublicKey.length == 33) { serverXOnlyPublicKey = serverXOnlyPublicKey.slice(1); } else if (serverXOnlyPublicKey.length !== 32) { throw new Error(`Invalid server public key length: ${serverXOnlyPublicKey.length}`); } const vhtlcScript = new import_sdk.VHTLC.Script({ preimageHash: (0, import_legacy.ripemd160)(preimageHash), sender: senderXOnlyPublicKey, receiver: receiverXOnlyPublicKey, server: serverXOnlyPublicKey, refundLocktime: BigInt(timeoutBlockHeights.refund), unilateralClaimDelay: { type: "blocks", value: BigInt(timeoutBlockHeights.unilateralClaim) }, unilateralRefundDelay: { type: "blocks", value: BigInt(timeoutBlockHeights.unilateralRefund) }, unilateralRefundWithoutReceiverDelay: { type: "blocks", value: BigInt(timeoutBlockHeights.unilateralRefundWithoutReceiver) } }); if (!vhtlcScript) throw new Error("Failed to create VHTLC script"); const hrp = network === "bitcoin" ? "ark" : "tark"; const vhtlcAddress = vhtlcScript.address(hrp, serverXOnlyPublicKey).encode(); return { vhtlcScript, vhtlcAddress }; } /** * Retrieves all pending submarine swaps from the storage provider. * This method filters the pending swaps to return only those with a status of 'invoice.set'. * It is useful for checking the status of all pending submarine swaps in the system. * @returns PendingSubmarineSwap[] or null if no storage provider is set. * If no swaps are found, it returns an empty array. */ getPendingSubmarineSwaps() { if (!this.storageProvider) return null; const swaps = this.storageProvider.getPendingSubmarineSwaps(); if (!swaps) return []; return swaps.filter((swap) => swap.status === "invoice.set"); } /** * Retrieves all pending reverse swaps from the storage provider. * This method filters the pending swaps to return only those with a status of 'swap.created'. * It is useful for checking the status of all pending reverse swaps in the system. * @returns PendingReverseSwap[] or null if no storage provider is set. * If no swaps are found, it returns an empty array. */ getPendingReverseSwaps() { if (!this.storageProvider) return null; const swaps = this.storageProvider.getPendingReverseSwaps(); if (!swaps) return []; return swaps.filter((swap) => swap.status === "swap.created"); } }; // src/boltz-swap-provider.ts 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" && typeof data.transaction.hex === "string" && (data.transaction.preimage === void 0 || typeof data.transaction.preimage === "string")); }; var isGetPairsResponse = (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 isCreateSubmarineSwapResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && typeof data.address === "string" && typeof data.expectedAmount === "number" && typeof data.claimPublicKey === "string" && typeof data.acceptZeroConf === "boolean" && data.timeoutBlockHeights && typeof data.timeoutBlockHeights === "object" && typeof data.timeoutBlockHeights.unilateralClaim === "number" && typeof data.timeoutBlockHeights.unilateralRefund === "number" && typeof data.timeoutBlockHeights.unilateralRefundWithoutReceiver === "number"; }; var isCreateReverseSwapResponse = (data) => { return data && typeof data === "object" && typeof data.id === "string" && typeof data.invoice === "string" && typeof data.onchainAmount === "number" && typeof data.lockupAddress === "string" && typeof data.refundPublicKey === "string" && data.timeoutBlockHeights && typeof data.timeoutBlockHeights === "object" && typeof data.timeoutBlockHeights.refund === "number" && typeof data.timeoutBlockHeights.unilateralClaim === "number" && typeof data.timeoutBlockHeights.unilateralRefund === "number" && typeof data.timeoutBlockHeights.unilateralRefundWithoutReceiver === "number"; }; var BASE_URLS = { bitcoin: "https://boltz.arkade.sh", mutinynet: "https://boltz.mutinynet.arkade.sh", testnet: "https://boltz.testnet.arkade.sh", regtest: "http://localhost:9069" }; var BoltzSwapProvider = class { wsUrl; apiUrl; network; constructor(config) { this.network = config.network; this.apiUrl = config.apiUrl || BASE_URLS[config.network]; this.wsUrl = this.apiUrl.replace(/^http(s)?:\/\//, "ws$1://").replace("9069", "9004") + "/v2/ws"; } getNetwork() { return this.network; } async getLimits() { const response = await this.request("/v2/swap/submarine", "GET"); if (!isGetPairsResponse(response)) throw new SchemaError({ message: "error fetching limits" }); return { min: response.ARK.BTC.limits.minimal, max: response.ARK.BTC.limits.maximal }; } async getSwapStatus(id) { const response = await this.request(`/v2/swap/${id}`, "GET"); if (!isGetSwapStatusResponse(response)) throw new SchemaError({ message: `error fetching status for swap: ${id}` }); return response; } async createSubmarineSwap({ invoice, refundPublicKey }) { if (refundPublicKey.length == 64) refundPublicKey = "02" + refundPublicKey; const response = await this.request("/v2/swap/submarine", "POST", { from: "ARK", to: "BTC", invoice, refundPublicKey }); if (!isCreateSubmarineSwapResponse(response)) throw new SchemaError({ message: "Error creating submarine swap" }); return response; } async createReverseSwap({ invoiceAmount, claimPublicKey, preimageHash }) { if (claimPublicKey.length == 64) claimPublicKey = "02" + claimPublicKey; const response = await this.request("/v2/swap/reverse", "POST", { from: "BTC", to: "ARK", invoiceAmount, claimPublicKey, preimageHash }); if (!isCreateReverseSwapResponse(response)) throw new SchemaError({ message: "Error creating reverse swap" }); return response; } async monitorSwap(swapId, update) { return new Promise((resolve, reject) => { const webSocket = new globalThis.WebSocket(this.wsUrl); const connectionTimeout = setTimeout(() => { webSocket.close(); reject(new NetworkError("WebSocket connection timeout")); }, 3e4); webSocket.onerror = (error) => { clearTimeout(connectionTimeout); reject(new NetworkError(`WebSocket error: ${error.message}`)); }; webSocket.onopen = () => { clearTimeout(connectionTimeout); webSocket.send( JSON.stringify({ op: "subscribe", channel: "swap.update", args: [swapId] }) ); }; webSocket.onclose = () => { clearTimeout(connectionTimeout); resolve(); }; webSocket.onmessage = async (rawMsg) => { const msg = JSON.parse(rawMsg.data); if (msg.event !== "update" || msg.args[0].id !== swapId) return; if (msg.args[0].error) { webSocket.close(); reject(new SwapError({ message: msg.args[0].error })); } const status = msg.args[0].status; switch (status) { case "invoice.settled": case "transaction.claimed": case "transaction.refunded": case "invoice.expired": case "invoice.failedToPay": case "transaction.failed": case "transaction.lockupFailed": case "swap.expired": webSocket.close(); update(status); break; case "invoice.paid": case "invoice.pending": case "invoice.set": case "swap.created": case "transaction.claim.pending": case "transaction.confirmed": case "transaction.mempool": update(status); } }; }); } async request(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(); throw new NetworkError(`Boltz API error: ${response.status} ${errorBody}`); } 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/storage-provider.ts var fs = __toESM(require("fs/promises"), 1); var KEY_REVERSE_SWAPS = "reverseSwaps"; var KEY_SUBMARINE_SWAPS = "submarineSwaps"; var Storage = class _Storage { storage; isBrowser; storagePath; localStorage; initPromise; constructor(options = {}) { this.storage = null; this.storagePath = options.storagePath || "./storage.json"; this.isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined" && typeof globalThis.window.localStorage !== "undefined"; if (this.isBrowser) { this.localStorage = globalThis.window.localStorage; } } static async create(options = {}) { const storage = new _Storage(options); if (!storage.isBrowser) await storage.initializeFileStorage(); return storage; } async initializeFileStorage() { try { await fs.access(this.storagePath); const data = await fs.readFile(this.storagePath, "utf8"); this.storage = JSON.parse(data); } catch { this.storage = {}; await this.save(); } } async getItem(key) { if (this.isBrowser) { return this.localStorage.getItem(key); } else { await this.ensureInitialized(); return this.storage[key] || null; } } async setItem(key, value) { if (this.isBrowser) { this.localStorage.setItem(key, value); } else { await this.ensureInitialized(); this.storage[key] = value; await this.save(); } } async removeItem(key) { if (this.isBrowser) { this.localStorage.removeItem(key); } else { await this.ensureInitialized(); delete this.storage[key]; await this.save(); } } async clear() { if (this.isBrowser) { this.localStorage.clear(); } else { await this.ensureInitialized(); this.storage = {}; await this.save(); } } async save() { if (!this.isBrowser) { try { await fs.writeFile(this.storagePath, JSON.stringify(this.storage, null, 2)); } catch (error) { throw new Error(`Failed to save storage: ${error}`); } } } async ensureInitialized() { if (!this.isBrowser && !this.storage) { if (!this.initPromise) { this.initPromise = this.initializeFileStorage(); } await this.initPromise; } } }; var StorageProvider = class _StorageProvider { storageInstance; storage; constructor(instance) { this.storageInstance = instance; this.storage = { reverseSwaps: [], submarineSwaps: [] }; } static async create(options = {}) { const storageInstance = await Storage.create(options); const storage = new _StorageProvider(storageInstance); await storage.initializeStorage(); return storage; } getPendingReverseSwaps() { return this.storage[KEY_REVERSE_SWAPS]; } async savePendingReverseSwap(swap) { return this.savePendingSwap(KEY_REVERSE_SWAPS, swap); } async deletePendingReverseSwap(id) { return this.deletePendingSwap(KEY_REVERSE_SWAPS, id); } getPendingSubmarineSwaps() { return this.storage[KEY_SUBMARINE_SWAPS]; } async savePendingSubmarineSwap(swap) { return this.savePendingSwap(KEY_SUBMARINE_SWAPS, swap); } async deletePendingSubmarineSwap(id) { return this.deletePendingSwap(KEY_SUBMARINE_SWAPS, id); } async savePendingSwap(kind, swap) { const swaps = this.storage[kind]; const found = swaps.findIndex((s) => s.response.id === swap.response.id); if (found !== -1) { swaps[found].status = swap.status; } else { if (kind === KEY_REVERSE_SWAPS) { swaps.push(swap); } if (kind === KEY_SUBMARINE_SWAPS) { swaps.push(swap); } } await this.setSwaps(kind, swaps); } async deletePendingSwap(kind, id) { const swaps = this.storage[kind]; const updatedSwaps = swaps.filter((s) => s.response.id !== id); if (kind === KEY_REVERSE_SWAPS) { await this.setSwaps(kind, updatedSwaps); } else if (kind === KEY_SUBMARINE_SWAPS) { await this.setSwaps(kind, updatedSwaps); } } async setSwaps(kind, swaps) { this.storage = { ...this.storage, [kind]: swaps }; await this.set(kind, swaps); } async set(kind, swaps) { const val = swaps ? JSON.stringify(swaps) : ""; await this.storageInstance.setItem(kind, val); } async get(kind) { const item = await this.storageInstance.getItem(kind); if (!item) return []; try { const swaps = JSON.parse(item); return swaps; } catch (error) { console.error(`Failed to parse stored data for key ${kind}:`, error); return []; } } async initializeStorage() { try { this.storage = { reverseSwaps: await this.get(KEY_REVERSE_SWAPS), submarineSwaps: await this.get(KEY_SUBMARINE_SWAPS) }; } catch (error) { console.error("Failed to initialize storage:", error); this.storage = { reverseSwaps: [], submarineSwaps: [] }; } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ArkadeLightning, BoltzSwapProvider, InsufficientFundsError, InvoiceExpiredError, NetworkError, StorageProvider, SwapError, decodeInvoice, getInvoicePaymentHash, getInvoiceSatoshis });