@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
JavaScript
"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 =