@arkade-os/boltz-swap
Version:
A production-ready TypeScript package that brings Boltz submarine-swaps to Arkade.
1,626 lines (1,619 loc) • 47.5 kB
JavaScript
import {
ArkadeSwaps,
BoltzRefundError,
BoltzSwapProvider,
IndexedDbSwapRepository,
InsufficientFundsError,
InvoiceExpiredError,
InvoiceFailedToPayError,
NetworkError,
PreimageFetchError,
QuoteRejectedError,
SchemaError,
SwapError,
SwapExpiredError,
SwapManager,
SwapNotFoundError,
TransactionFailedError,
decodeInvoice,
enrichReverseSwapPreimage,
enrichSubmarineSwapInvoice,
getInvoicePaymentHash,
getInvoiceSatoshis,
hasSubmarineStatusReached,
isChainClaimableStatus,
isChainFailedStatus,
isChainFinalStatus,
isChainPendingStatus,
isChainRefundableStatus,
isChainSignableStatus,
isChainSuccessStatus,
isChainSwapClaimable,
isChainSwapRefundable,
isPendingChainSwap,
isPendingReverseSwap,
isPendingSubmarineSwap,
isReverseClaimableStatus,
isReverseFailedStatus,
isReverseFinalStatus,
isReversePendingStatus,
isReverseSuccessStatus,
isReverseSwapClaimable,
isSubmarineFailedStatus,
isSubmarineFinalStatus,
isSubmarinePendingStatus,
isSubmarineRefundableStatus,
isSubmarineSuccessStatus,
isSubmarineSwapRefundable,
isValidArkAddress,
logger,
saveSwap,
setLogger,
updateChainSwapStatus,
updateReverseSwapStatus,
updateSubmarineSwapStatus,
verifySignatures
} from "./chunk-VKHFXGKL.js";
import {
applyCreatedAtOrder,
applySwapsFilter
} from "./chunk-SJQJQO7P.js";
// package.json
var version = "0.3.50";
// src/serviceWorker/arkade-swaps-message-handler.ts
import { RestArkProvider, RestIndexerProvider } from "@arkade-os/sdk";
function toQuoteTransportError(error) {
if (error instanceof QuoteRejectedError) {
return error.toTransportError();
}
return error;
}
var DEFAULT_MESSAGE_TAG = "ARKADE_SWAPS_UPDATER";
var HANDLER_NOT_INITIALIZED = "ArkadeSwaps handler not initialized";
var HandlerNotInitializedError = class extends Error {
constructor() {
super(HANDLER_NOT_INITIALIZED);
this.name = "HandlerNotInitializedError";
}
};
var LONG_RUNNING_ARKADE_SWAPS_REQUEST_TYPES = /* @__PURE__ */ new Set([
"SEND_LIGHTNING_PAYMENT",
"CLAIM_VHTLC",
"REFUND_VHTLC",
"INSPECT_SUBMARINE_RECOVERY",
"SCAN_RECOVERABLE_SUBMARINE_SWAPS",
"RECOVER_SUBMARINE_FUNDS",
"RECOVER_ALL_SUBMARINE_FUNDS",
"WAIT_AND_CLAIM",
"WAIT_FOR_SWAP_SETTLEMENT",
"WAIT_FOR_SWAP_FUNDED",
"RESTORE_SWAPS",
"WAIT_AND_CLAIM_CHAIN",
"WAIT_AND_CLAIM_ARK",
"WAIT_AND_CLAIM_BTC",
"CLAIM_ARK",
"CLAIM_BTC",
"REFUND_ARK",
"SM-WAIT_FOR_COMPLETION"
]);
var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
constructor(swapRepository) {
this.swapRepository = swapRepository;
}
static messageTag = DEFAULT_MESSAGE_TAG;
messageTag = _ArkadeSwapsMessageHandler.messageTag;
arkProvider;
indexerProvider;
swapProvider;
wallet;
handler;
swapManager;
getSwapManagerOrThrow() {
const sm = this.handler?.getSwapManager();
if (!sm) throw new Error("SwapManager is not enabled");
return sm;
}
async start(opts) {
if (!opts.wallet) throw new Error("Wallet is required");
this.wallet = opts.wallet;
}
async stop() {
const handler = this.handler;
if (!handler) return;
const swapManager = this.swapManager ?? handler.getSwapManager();
if (swapManager) {
await swapManager.stop();
}
if (typeof handler.dispose === "function") {
await handler.dispose();
}
this.swapManager = null;
this.handler = void 0;
this.wallet = void 0;
this.arkProvider = void 0;
this.indexerProvider = void 0;
this.swapProvider = void 0;
}
async tick(_now) {
return [];
}
// Flows that surrender control to Boltz, the Ark server, or other
// participants in a batch round: quiet gaps between protocol events can
// easily exceed the bus-level messageTimeoutMs. Liveness is covered
// out-of-band by the page-side PING / MESSAGE_BUS_NOT_INITIALIZED path
// triggered by concurrent short requests (GET_FEES, GET_SWAP_STATUS, ...).
isLongRunning(message) {
return LONG_RUNNING_ARKADE_SWAPS_REQUEST_TYPES.has(message.type);
}
tagged(res) {
return {
...res,
tag: this.messageTag
};
}
async broadcastEvent(event) {
const sw = self;
if (!sw?.clients?.matchAll) return;
const clients = await sw.clients.matchAll();
for (const client of clients) {
try {
client.postMessage(event);
} catch {
}
}
}
async handleMessage(message) {
const id = message.id;
if (message.type === "INIT_ARKADE_SWAPS") {
try {
await this.handleInit(message);
return this.tagged({
id,
type: "ARKADE_SWAPS_INITIALIZED"
});
} catch (error) {
return this.tagged({ id, error });
}
}
if (!this.handler || !this.wallet) {
return this.tagged({
id,
error: new HandlerNotInitializedError()
});
}
try {
switch (message.type) {
case "CREATE_LIGHTNING_INVOICE": {
const res = await this.handler.createLightningInvoice(message.payload);
return this.tagged({
id,
type: "LIGHTNING_INVOICE_CREATED",
payload: res
});
}
case "SEND_LIGHTNING_PAYMENT": {
const res = await this.handler.sendLightningPayment(message.payload);
return this.tagged({
id,
type: "LIGHTNING_PAYMENT_SENT",
payload: res
});
}
case "CREATE_SUBMARINE_SWAP": {
const res = await this.handler.createSubmarineSwap(message.payload);
return this.tagged({
id,
type: "SUBMARINE_SWAP_CREATED",
payload: res
});
}
case "CREATE_REVERSE_SWAP": {
const res = await this.handler.createReverseSwap(message.payload);
return this.tagged({
id,
type: "REVERSE_SWAP_CREATED",
payload: res
});
}
case "CLAIM_VHTLC":
await this.handler.claimVHTLC(message.payload);
return this.tagged({ id, type: "VHTLC_CLAIMED" });
case "REFUND_VHTLC": {
const outcome = await this.handler.refundVHTLC(message.payload);
return this.tagged({
id,
type: "VHTLC_REFUNDED",
payload: outcome
});
}
case "INSPECT_SUBMARINE_RECOVERY": {
const info = await this.handler.inspectSubmarineRecovery(message.payload);
return this.tagged({
id,
type: "SUBMARINE_RECOVERY_INSPECTED",
payload: info
});
}
case "SCAN_RECOVERABLE_SUBMARINE_SWAPS": {
const infos = await this.handler.scanRecoverableSubmarineSwaps();
return this.tagged({
id,
type: "RECOVERABLE_SUBMARINE_SWAPS_SCANNED",
payload: infos
});
}
case "RECOVER_SUBMARINE_FUNDS": {
const outcome = await this.handler.recoverSubmarineFunds(message.payload);
return this.tagged({
id,
type: "SUBMARINE_FUNDS_RECOVERED",
payload: outcome
});
}
case "RECOVER_ALL_SUBMARINE_FUNDS": {
const results = await this.handler.recoverAllSubmarineFunds(message.payload);
return this.tagged({
id,
type: "ALL_SUBMARINE_FUNDS_RECOVERED",
payload: results
});
}
case "WAIT_AND_CLAIM": {
const res = await this.handler.waitAndClaim(message.payload);
return this.tagged({
id,
type: "WAIT_AND_CLAIMED",
payload: res
});
}
case "WAIT_FOR_SWAP_SETTLEMENT": {
const res = await this.handler.waitForSwapSettlement(message.payload);
return this.tagged({
id,
type: "SWAP_SETTLED",
payload: res
});
}
case "WAIT_FOR_SWAP_FUNDED": {
await this.handler.waitForSwapFunded(message.payload);
return this.tagged({ id, type: "SWAP_FUNDED" });
}
case "RESTORE_SWAPS": {
const res = await this.handler.restoreSwaps(message.payload);
return this.tagged({
id,
type: "SWAPS_RESTORED",
payload: res
});
}
case "ENRICH_REVERSE_SWAP_PREIMAGE": {
const res = this.handler.enrichReverseSwapPreimage(
message.payload.swap,
message.payload.preimage
);
return this.tagged({
id,
type: "REVERSE_SWAP_PREIMAGE_ENRICHED",
payload: res
});
}
case "ENRICH_SUBMARINE_SWAP_INVOICE": {
const res = this.handler.enrichSubmarineSwapInvoice(
message.payload.swap,
message.payload.invoice
);
return this.tagged({
id,
type: "SUBMARINE_SWAP_INVOICE_ENRICHED",
payload: res
});
}
case "GET_FEES": {
const res = message.payload ? await this.handler.getFees(message.payload.from, message.payload.to) : await this.handler.getFees();
return this.tagged({ id, type: "FEES", payload: res });
}
case "GET_LIMITS": {
const res = message.payload ? await this.handler.getLimits(message.payload.from, message.payload.to) : await this.handler.getLimits();
return this.tagged({ id, type: "LIMITS", payload: res });
}
case "GET_SWAP_STATUS": {
const res = await this.handler.getSwapStatus(message.payload.swapId);
return this.tagged({
id,
type: "SWAP_STATUS",
payload: res
});
}
case "GET_PENDING_SUBMARINE_SWAPS": {
const res = await this.handler.getPendingSubmarineSwaps();
return this.tagged({
id,
type: "PENDING_SUBMARINE_SWAPS",
payload: res
});
}
case "GET_PENDING_REVERSE_SWAPS": {
const res = await this.handler.getPendingReverseSwaps();
return this.tagged({
id,
type: "PENDING_REVERSE_SWAPS",
payload: res
});
}
case "GET_PENDING_CHAIN_SWAPS": {
const res = await this.handler.getPendingChainSwaps();
return this.tagged({
id,
type: "PENDING_CHAIN_SWAPS",
payload: res
});
}
case "GET_SWAP_HISTORY": {
const res = await this.handler.getSwapHistory();
return this.tagged({
id,
type: "SWAP_HISTORY",
payload: res
});
}
case "REFRESH_SWAPS_STATUS":
await this.handler.refreshSwapsStatus();
return this.tagged({ id, type: "SWAPS_STATUS_REFRESHED" });
case "ARK_TO_BTC": {
const res = await this.handler.arkToBtc(message.payload);
return this.tagged({
id,
type: "ARK_TO_BTC_CREATED",
payload: res
});
}
case "BTC_TO_ARK": {
const res = await this.handler.btcToArk(message.payload);
return this.tagged({
id,
type: "BTC_TO_ARK_CREATED",
payload: res
});
}
case "CREATE_CHAIN_SWAP": {
const res = await this.handler.createChainSwap(message.payload);
return this.tagged({
id,
type: "CHAIN_SWAP_CREATED",
payload: res
});
}
case "WAIT_AND_CLAIM_CHAIN": {
const res = await this.handler.waitAndClaimChain(message.payload);
return this.tagged({
id,
type: "CHAIN_CLAIMED",
payload: res
});
}
case "WAIT_AND_CLAIM_ARK": {
const res = await this.handler.waitAndClaimArk(message.payload);
return this.tagged({
id,
type: "ARK_CLAIMED",
payload: res
});
}
case "WAIT_AND_CLAIM_BTC": {
const res = await this.handler.waitAndClaimBtc(message.payload);
return this.tagged({
id,
type: "BTC_CLAIMED",
payload: res
});
}
case "CLAIM_ARK": {
const res = await this.handler.claimArk(message.payload);
return this.tagged({ id, type: "ARK_CLAIM_EXECUTED", payload: res });
}
case "CLAIM_BTC": {
const res = await this.handler.claimBtc(message.payload);
return this.tagged({ id, type: "BTC_CLAIM_EXECUTED", payload: res });
}
case "REFUND_ARK": {
const outcome = await this.handler.refundArk(message.payload);
return this.tagged({
id,
type: "ARK_REFUND_EXECUTED",
payload: outcome
});
}
case "SIGN_SERVER_CLAIM":
await this.handler.signCooperativeClaimForServer(message.payload);
return this.tagged({ id, type: "SERVER_CLAIM_SIGNED" });
case "VERIFY_CHAIN_SWAP": {
const verified = await this.handler.verifyChainSwap({
...message.payload
});
return this.tagged({
id,
type: "CHAIN_SWAP_VERIFIED",
payload: { verified }
});
}
case "QUOTE_SWAP": {
try {
const amount = await this.handler.quoteSwap(
message.payload.swapId,
message.payload.options
);
return this.tagged({
id,
type: "SWAP_QUOTED",
payload: { amount }
});
} catch (e) {
throw toQuoteTransportError(e);
}
}
case "GET_SWAP_QUOTE": {
try {
const amount = await this.handler.getSwapQuote(message.payload.swapId);
return this.tagged({
id,
type: "SWAP_QUOTE_RETRIEVED",
payload: { amount }
});
} catch (e) {
throw toQuoteTransportError(e);
}
}
case "ACCEPT_SWAP_QUOTE": {
try {
const amount = await this.handler.acceptSwapQuote(
message.payload.swapId,
message.payload.amount,
message.payload.options
);
return this.tagged({
id,
type: "SWAP_QUOTE_ACCEPTED",
payload: { amount }
});
} catch (e) {
throw toQuoteTransportError(e);
}
}
/* --- SwapManager methods --- */
case "SM-START": {
await this.handler.startSwapManager();
return this.tagged({ id, type: "SM-STARTED" });
}
case "SM-STOP": {
await this.handler.stopSwapManager();
return this.tagged({ id, type: "SM-STOPPED" });
}
case "SM-ADD_SWAP": {
await this.getSwapManagerOrThrow().addSwap(message.payload);
return this.tagged({ id, type: "SM-SWAP_ADDED" });
}
case "SM-REMOVE_SWAP": {
await this.getSwapManagerOrThrow().removeSwap(message.payload.swapId);
return this.tagged({ id, type: "SM-SWAP_REMOVED" });
}
case "SM-GET_PENDING_SWAPS": {
const res = await this.getSwapManagerOrThrow().getPendingSwaps();
return this.tagged({
id,
type: "SM-PENDING_SWAPS",
payload: res
});
}
case "SM-HAS_SWAP": {
const has = await this.getSwapManagerOrThrow().hasSwap(message.payload.swapId);
return this.tagged({
id,
type: "SM-HAS_SWAP_RESULT",
payload: { has }
});
}
case "SM-IS_PROCESSING": {
const processing = await this.getSwapManagerOrThrow().isProcessing(
message.payload.swapId
);
return this.tagged({
id,
type: "SM-IS_PROCESSING_RESULT",
payload: { processing }
});
}
case "SM-GET_STATS": {
const stats = await this.getSwapManagerOrThrow().getStats();
return this.tagged({
id,
type: "SM-STATS",
payload: stats
});
}
case "SM-WAIT_FOR_COMPLETION": {
const res = await this.getSwapManagerOrThrow().waitForSwapCompletion(
message.payload.swapId
);
return this.tagged({
id,
type: "SM-COMPLETED",
payload: res
});
}
default:
console.error("Unknown message type", message);
throw new Error("Unknown message");
}
} catch (error) {
return this.tagged({ id, error });
}
}
async handleInit({ payload }) {
if (!this.wallet) {
throw new Error("Wallet is required");
}
const { arkServerUrl } = payload;
this.arkProvider = new RestArkProvider(arkServerUrl);
this.indexerProvider = new RestIndexerProvider(arkServerUrl);
this.swapProvider = new BoltzSwapProvider({
apiUrl: payload.swapProvider.baseUrl,
network: payload.network,
referralId: payload.referralId
});
const handler = new ArkadeSwaps({
wallet: this.wallet,
arkProvider: this.arkProvider,
swapProvider: this.swapProvider,
indexerProvider: this.indexerProvider,
swapRepository: this.swapRepository,
swapManager: payload.swapManager
});
this.handler = handler;
const sm = handler.getSwapManager();
this.swapManager = sm;
if (sm) {
void sm.onSwapUpdate(async (swap, oldStatus) => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-SWAP_UPDATE",
payload: { swap, oldStatus }
});
});
void sm.onSwapCompleted(async (swap) => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-SWAP_COMPLETED",
payload: { swap }
});
});
void sm.onSwapFailed(async (swap, error) => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-SWAP_FAILED",
payload: { swap, error: { message: error.message } }
});
});
void sm.onActionExecuted(async (swap, action) => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-ACTION_EXECUTED",
payload: { swap, action }
});
});
void sm.onWebSocketConnected(async () => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-WS_CONNECTED"
});
});
void sm.onWebSocketDisconnected(async (error) => {
await this.broadcastEvent({
tag: this.messageTag,
type: "SM-EVENT-WS_DISCONNECTED",
payload: error ? { errorMessage: error.message } : void 0
});
});
}
}
};
// src/serviceWorker/arkade-swaps-runtime.ts
import {
getRandomId,
MESSAGE_BUS_NOT_INITIALIZED,
ServiceWorkerTimeoutError
} from "@arkade-os/sdk";
function isMessageBusNotInitializedError(error) {
return error instanceof Error && error.message.includes(MESSAGE_BUS_NOT_INITIALIZED);
}
function isHandlerNotInitializedError(error) {
return error instanceof Error && error.message.includes(HANDLER_NOT_INITIALIZED);
}
function rethrowIfQuoteRejected(error) {
if (error instanceof QuoteRejectedError) throw error;
const rebuilt = QuoteRejectedError.fromTransportError(error);
if (rebuilt) throw rebuilt;
}
var DEFAULT_MESSAGE_TIMEOUT_MS = 3e4;
var NO_MESSAGE_TIMEOUT_MS = 0;
var DEDUPABLE_REQUEST_TYPES = /* @__PURE__ */ new Set([
"GET_FEES",
"GET_LIMITS",
"GET_SWAP_STATUS",
"GET_PENDING_SUBMARINE_SWAPS",
"GET_PENDING_REVERSE_SWAPS",
"GET_PENDING_CHAIN_SWAPS",
"GET_SWAP_HISTORY",
"QUOTE_SWAP",
"SM-GET_PENDING_SWAPS",
"SM-HAS_SWAP",
"SM-IS_PROCESSING",
"SM-GET_STATS"
]);
function getRequestDedupKey(request) {
const { id, tag, ...rest } = request;
return JSON.stringify(rest);
}
var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
constructor(messageTag, serviceWorker, swapRepository, withSwapManager) {
this.messageTag = messageTag;
this.serviceWorker = serviceWorker;
this.swapRepository = swapRepository;
this.withSwapManager = withSwapManager;
}
eventListenerInitialized = false;
swapUpdateListeners = /* @__PURE__ */ new Set();
swapCompletedListeners = /* @__PURE__ */ new Set();
swapFailedListeners = /* @__PURE__ */ new Set();
actionExecutedListeners = /* @__PURE__ */ new Set();
wsConnectedListeners = /* @__PURE__ */ new Set();
wsDisconnectedListeners = /* @__PURE__ */ new Set();
initPayload = null;
reinitPromise = null;
pingPromise = null;
inflightRequests = /* @__PURE__ */ new Map();
static async create(config) {
const messageTag = config.messageTag ?? DEFAULT_MESSAGE_TAG;
const swapRepository = config.swapRepository ?? new IndexedDbSwapRepository();
const svcArkadeSwaps = new _ServiceWorkerArkadeSwaps(
messageTag,
config.serviceWorker,
swapRepository,
Boolean(config.swapManager)
);
const initPayload = {
network: config.network,
arkServerUrl: config.arkServerUrl,
swapProvider: { baseUrl: config.swapProvider.getApiUrl() },
swapManager: config.swapManager,
referralId: config.referralId
};
const initMessage = {
tag: messageTag,
id: getRandomId(),
type: "INIT_ARKADE_SWAPS",
payload: initPayload
};
await svcArkadeSwaps.sendMessage(initMessage);
svcArkadeSwaps.initPayload = initPayload;
return svcArkadeSwaps;
}
async startSwapManager() {
if (!this.withSwapManager) {
throw new Error("SwapManager is not enabled.");
}
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "SM-START"
});
}
async stopSwapManager() {
if (!this.withSwapManager) return;
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "SM-STOP"
});
}
getSwapManager() {
if (!this.withSwapManager) {
return null;
}
this.initEventStream();
const send = this.sendMessage.bind(this);
const tag = this.messageTag;
const proxy = {
start: async () => {
await send({
id: getRandomId(),
tag,
type: "SM-START"
});
},
stop: async () => {
await send({
id: getRandomId(),
tag,
type: "SM-STOP"
});
},
addSwap: async (swap) => {
await send({
id: getRandomId(),
tag,
type: "SM-ADD_SWAP",
payload: swap
});
},
removeSwap: async (swapId) => {
await send({
id: getRandomId(),
tag,
type: "SM-REMOVE_SWAP",
payload: { swapId }
});
},
getPendingSwaps: async () => {
const res = await send({
id: getRandomId(),
tag,
type: "SM-GET_PENDING_SWAPS"
});
return res.payload;
},
hasSwap: async (swapId) => {
const res = await send({
id: getRandomId(),
tag,
type: "SM-HAS_SWAP",
payload: { swapId }
});
return res.payload.has;
},
isProcessing: async (swapId) => {
const res = await send({
id: getRandomId(),
tag,
type: "SM-IS_PROCESSING",
payload: { swapId }
});
return res.payload.processing;
},
getStats: async () => {
const res = await send({
id: getRandomId(),
tag,
type: "SM-GET_STATS"
});
return res.payload;
},
waitForSwapCompletion: async (swapId) => {
const res = await send({
id: getRandomId(),
tag,
type: "SM-WAIT_FOR_COMPLETION",
payload: { swapId }
});
return res.payload;
},
subscribeToSwapUpdates: async (swapId, callback) => {
const filteredListener = (swap, oldStatus) => {
if (swap.id === swapId) {
callback(swap, oldStatus);
}
};
this.swapUpdateListeners.add(filteredListener);
return () => this.swapUpdateListeners.delete(filteredListener);
},
onSwapUpdate: async (listener) => {
this.swapUpdateListeners.add(listener);
return () => this.swapUpdateListeners.delete(listener);
},
onSwapCompleted: async (listener) => {
this.swapCompletedListeners.add(listener);
return () => this.swapCompletedListeners.delete(listener);
},
onSwapFailed: async (listener) => {
this.swapFailedListeners.add(listener);
return () => this.swapFailedListeners.delete(listener);
},
onActionExecuted: async (listener) => {
this.actionExecutedListeners.add(listener);
return () => this.actionExecutedListeners.delete(listener);
},
onWebSocketConnected: async (listener) => {
this.wsConnectedListeners.add(listener);
return () => this.wsConnectedListeners.delete(listener);
},
onWebSocketDisconnected: async (listener) => {
this.wsDisconnectedListeners.add(listener);
return () => this.wsDisconnectedListeners.delete(listener);
},
offSwapUpdate: (listener) => {
this.swapUpdateListeners.delete(listener);
},
offSwapCompleted: (listener) => {
this.swapCompletedListeners.delete(listener);
},
offSwapFailed: (listener) => {
this.swapFailedListeners.delete(listener);
},
offActionExecuted: (listener) => {
this.actionExecutedListeners.delete(listener);
},
offWebSocketConnected: (listener) => {
this.wsConnectedListeners.delete(listener);
},
offWebSocketDisconnected: (listener) => {
this.wsDisconnectedListeners.delete(listener);
}
};
return proxy;
}
async createLightningInvoice(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CREATE_LIGHTNING_INVOICE",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create Lightning Invoice", { cause: e });
}
}
async sendLightningPayment(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "SEND_LIGHTNING_PAYMENT",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot send Lightning payment", { cause: e });
}
}
async createSubmarineSwap(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CREATE_SUBMARINE_SWAP",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create submarine swap", { cause: e });
}
}
async createReverseSwap(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CREATE_REVERSE_SWAP",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create reverse swap", { cause: e });
}
}
async claimVHTLC(pendingSwap) {
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CLAIM_VHTLC",
payload: pendingSwap
});
}
async refundVHTLC(pendingSwap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "REFUND_VHTLC",
payload: pendingSwap
});
return res.payload;
}
async inspectSubmarineRecovery(swap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "INSPECT_SUBMARINE_RECOVERY",
payload: swap
});
return res.payload;
}
async scanRecoverableSubmarineSwaps() {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "SCAN_RECOVERABLE_SUBMARINE_SWAPS"
});
return res.payload;
}
async recoverSubmarineFunds(swap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "RECOVER_SUBMARINE_FUNDS",
payload: swap
});
return res.payload;
}
async recoverAllSubmarineFunds(swaps) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "RECOVER_ALL_SUBMARINE_FUNDS",
payload: swaps
});
return res.payload;
}
async waitAndClaim(pendingSwap) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_AND_CLAIM",
payload: pendingSwap
});
return res.payload;
} catch (e) {
throw new Error("Cannot wait and claim reverse swap", {
cause: e
});
}
}
async waitForSwapSettlement(pendingSwap) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_FOR_SWAP_SETTLEMENT",
payload: pendingSwap
});
return res.payload;
} catch (e) {
throw new Error("Cannot wait for swap settlement", { cause: e });
}
}
async waitForSwapFunded(pendingSwap) {
try {
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_FOR_SWAP_FUNDED",
payload: pendingSwap
});
} catch (e) {
throw new Error("Cannot wait for swap funding", { cause: e });
}
}
async restoreSwaps(boltzFees) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "RESTORE_SWAPS",
payload: boltzFees
});
return res.payload;
} catch (e) {
throw new Error("Cannot restore swaps", { cause: e });
}
}
async arkToBtc(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "ARK_TO_BTC",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create ARK -> BTC chain swap", {
cause: e
});
}
}
async btcToArk(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "BTC_TO_ARK",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create BTC -> ARK chain swap", {
cause: e
});
}
}
async createChainSwap(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CREATE_CHAIN_SWAP",
payload: args
});
return res.payload;
} catch (e) {
throw new Error("Cannot create chain swap", { cause: e });
}
}
async waitAndClaimChain(pendingSwap) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_AND_CLAIM_CHAIN",
payload: pendingSwap
});
return res.payload;
} catch (e) {
throw new Error("Cannot wait and claim chain swap", {
cause: e
});
}
}
async waitAndClaimArk(pendingSwap) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_AND_CLAIM_ARK",
payload: pendingSwap
});
return res.payload;
} catch (e) {
throw new Error("Cannot wait and claim ARK", {
cause: e
});
}
}
async waitAndClaimBtc(pendingSwap) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "WAIT_AND_CLAIM_BTC",
payload: pendingSwap
});
return res.payload;
} catch (e) {
throw new Error("Cannot wait and claim BTC", {
cause: e
});
}
}
async claimArk(pendingSwap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CLAIM_ARK",
payload: pendingSwap
});
return res.payload;
}
async claimBtc(pendingSwap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "CLAIM_BTC",
payload: pendingSwap
});
return res.payload;
}
async refundArk(pendingSwap) {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "REFUND_ARK",
payload: pendingSwap
});
return res.payload;
}
async signCooperativeClaimForServer(pendingSwap) {
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "SIGN_SERVER_CLAIM",
payload: pendingSwap
});
}
async verifyChainSwap(args) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "VERIFY_CHAIN_SWAP",
payload: {
to: args.to,
from: args.from,
swap: args.swap,
arkInfo: args.arkInfo
}
});
return res.payload.verified;
} catch (e) {
throw new Error("Cannot verify chain swap", { cause: e });
}
}
async quoteSwap(swapId, options) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "QUOTE_SWAP",
payload: { swapId, options }
});
return res.payload.amount;
} catch (e) {
rethrowIfQuoteRejected(e);
throw new Error("Cannot quote swap", { cause: e });
}
}
async getSwapQuote(swapId) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_SWAP_QUOTE",
payload: { swapId }
});
return res.payload.amount;
} catch (e) {
rethrowIfQuoteRejected(e);
throw new Error("Cannot get swap quote", { cause: e });
}
}
async acceptSwapQuote(swapId, amount, options) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "ACCEPT_SWAP_QUOTE",
payload: { swapId, amount, options }
});
return res.payload.amount;
} catch (e) {
rethrowIfQuoteRejected(e);
throw new Error("Cannot accept swap quote", { cause: e });
}
}
enrichReverseSwapPreimage(swap, preimage) {
return enrichReverseSwapPreimage(swap, preimage);
}
enrichSubmarineSwapInvoice(swap, invoice) {
return enrichSubmarineSwapInvoice(swap, invoice);
}
createVHTLCScript(_args) {
throw new Error("createVHTLCScript is not supported via service worker");
}
async joinBatch(_identity, _input, _output, _arkInfo, _isRecoverable = true) {
throw new Error("joinBatch is not supported via service worker");
}
async getFees(from, to) {
if (from === void 0 !== (to === void 0)) {
throw new Error("Both 'from' and 'to' must be provided together");
}
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_FEES",
...from !== void 0 && to !== void 0 ? { payload: { from, to } } : {}
});
return res.payload;
} catch (e) {
throw new Error("Cannot get fees", { cause: e });
}
}
async getLimits(from, to) {
if (from === void 0 !== (to === void 0)) {
throw new Error("Both 'from' and 'to' must be provided together");
}
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_LIMITS",
...from !== void 0 && to !== void 0 ? { payload: { from, to } } : {}
});
return res.payload;
} catch (e) {
throw new Error("Cannot get limits", { cause: e });
}
}
async getSwapStatus(swapId) {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_SWAP_STATUS",
payload: { swapId }
});
return res.payload;
} catch (e) {
throw new Error("Cannot get swap status", { cause: e });
}
}
async getPendingSubmarineSwaps() {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_PENDING_SUBMARINE_SWAPS"
});
return res.payload;
} catch (e) {
throw new Error("Cannot get pending submarine swaps", {
cause: e
});
}
}
async getPendingReverseSwaps() {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_PENDING_REVERSE_SWAPS"
});
return res.payload;
} catch (e) {
throw new Error("Cannot get pending reverse swaps", { cause: e });
}
}
async getPendingChainSwaps() {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_PENDING_CHAIN_SWAPS"
});
return res.payload;
} catch (e) {
throw new Error("Cannot get pending chain swaps", { cause: e });
}
}
async getSwapHistory() {
try {
const res = await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "GET_SWAP_HISTORY"
});
return res.payload;
} catch (e) {
throw new Error("Cannot get swap history", { cause: e });
}
}
async refreshSwapsStatus() {
await this.sendMessage({
id: getRandomId(),
tag: this.messageTag,
type: "REFRESH_SWAPS_STATUS"
});
}
/**
* Reset all swap state: stops the SwapManager and clears the swap repository.
*
* **Destructive** — any swap in a non-terminal state will lose its
* refund/claim path. Intended for wallet-reset / dev / test scenarios only.
*/
async reset() {
await this.dispose();
await this.swapRepository.clear();
}
async dispose() {
if (this.withSwapManager) {
await this.stopSwapManager().catch(() => {
});
}
}
async [Symbol.asyncDispose]() {
return this.dispose();
}
sendMessageDirect(request, timeoutMs) {
return new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timeoutId);
navigator.serviceWorker.removeEventListener("message", messageHandler);
};
const timeoutId = timeoutMs > 0 ? setTimeout(() => {
cleanup();
reject(
new ServiceWorkerTimeoutError(
`Service worker message timed out (${request.type})`
)
);
}, timeoutMs) : void 0;
const messageHandler = (event) => {
const response = event.data;
if (!response || response.tag !== this.messageTag || response.id !== request.id) {
return;
}
cleanup();
if (response.error) {
reject(response.error);
} else {
resolve(response);
}
};
navigator.serviceWorker.addEventListener("message", messageHandler);
this.serviceWorker.postMessage(request);
});
}
async sendMessage(request) {
if (!DEDUPABLE_REQUEST_TYPES.has(request.type)) {
return this.sendMessageWithRetry(request);
}
const key = getRequestDedupKey(request);
const existing = this.inflightRequests.get(key);
if (existing) return existing;
const promise = this.sendMessageWithRetry(request).finally(() => {
this.inflightRequests.delete(key);
});
this.inflightRequests.set(key, promise);
return promise;
}
pingServiceWorker() {
if (this.pingPromise) return this.pingPromise;
this.pingPromise = new Promise((resolve, reject) => {
const pingId = getRandomId();
const cleanup = () => {
clearTimeout(timeoutId);
navigator.serviceWorker.removeEventListener("message", onMessage);
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new ServiceWorkerTimeoutError("Service worker ping timed out"));
}, 2e3);
const onMessage = (event) => {
if (event.data?.id === pingId && event.data?.tag === "PONG") {
cleanup();
resolve();
}
};
navigator.serviceWorker.addEventListener("message", onMessage);
this.serviceWorker.postMessage({
id: pingId,
tag: "PING"
});
}).finally(() => {
this.pingPromise = null;
});
return this.pingPromise;
}
// Send a message, retrying up to 2 times if the service worker was
// killed and restarted by the OS (mobile browsers do this aggressively).
async sendMessageWithRetry(request) {
if (this.initPayload) {
try {
await this.pingServiceWorker();
} catch {
await this.reinitialize();
}
}
const timeoutMs = LONG_RUNNING_ARKADE_SWAPS_REQUEST_TYPES.has(request.type) ? NO_MESSAGE_TIMEOUT_MS : DEFAULT_MESSAGE_TIMEOUT_MS;
const maxRetries = 2;
for (let attempt = 0; ; attempt++) {
try {
return await this.sendMessageDirect(request, timeoutMs);
} catch (error) {
const recoverable = isMessageBusNotInitializedError(error) || isHandlerNotInitializedError(error);
if (!recoverable || attempt >= maxRetries) {
throw error;
}
await this.reinitialize();
}
}
}
async reinitialize() {
if (this.reinitPromise) return this.reinitPromise;
this.reinitPromise = (async () => {
if (!this.initPayload) {
throw new Error("Cannot re-initialize: missing configuration");
}
const initMessage = {
tag: this.messageTag,
type: "INIT_ARKADE_SWAPS",
id: getRandomId(),
payload: this.initPayload
};
await this.sendMessageDirect(initMessage, DEFAULT_MESSAGE_TIMEOUT_MS);
})().finally(() => {
this.reinitPromise = null;
});
return this.reinitPromise;
}
initEventStream() {
if (this.eventListenerInitialized) return;
this.eventListenerInitialized = true;
navigator.serviceWorker.addEventListener("message", this.handleEventMessage);
}
handleEventMessage = (event) => {
const data = event.data;
if (!data || data.tag !== this.messageTag) return;
if (typeof data.type !== "string") return;
if (!data.type.startsWith("SM-EVENT-")) return;
switch (data.type) {
case "SM-EVENT-SWAP_UPDATE":
this.swapUpdateListeners.forEach((cb) => {
cb(data.payload.swap, data.payload.oldStatus);
});
break;
case "SM-EVENT-SWAP_COMPLETED":
this.swapCompletedListeners.forEach((cb) => {
cb(data.payload.swap);
});
break;
case "SM-EVENT-SWAP_FAILED": {
const err = new Error(data.payload.error?.message);
this.swapFailedListeners.forEach((cb) => {
cb(data.payload.swap, err);
});
break;
}
case "SM-EVENT-ACTION_EXECUTED":
this.actionExecutedListeners.forEach((cb) => {
cb(data.payload.swap, data.payload.action);
});
break;
case "SM-EVENT-WS_CONNECTED":
this.wsConnectedListeners.forEach((cb) => {
cb();
});
break;
case "SM-EVENT-WS_DISCONNECTED": {
const err = data.payload?.errorMessage ? new Error(data.payload.errorMessage) : void 0;
this.wsDisconnectedListeners.forEach((cb) => {
cb(err);
});
break;
}
default:
break;
}
};
};
// src/repositories/migrationFromContracts.ts
var MIGRATION_KEY = "migration-from-storage-adapter-swaps";
async function migrateToSwapRepository(storageAdapter, fresh) {
try {
const migration = await storageAdapter.getItem(MIGRATION_KEY);
if (migration === "done") {
return false;
}
const reverseSwaps = await getContractCollection(
storageAdapter,
"reverseSwaps"
);
const submarineSwaps = await getContractCollection(
storageAdapter,
"submarineSwaps"
);
for (const swap of reverseSwaps) {
await fresh.saveSwap(swap);
}
for (const swap of submarineSwaps) {
await fresh.saveSwap(swap);
}
await storageAdapter.setItem(MIGRATION_KEY, "done");
return true;
} catch (error) {
if (error instanceof Error && error.message.includes("One of the specified object stores was not found.")) {
return false;
}
throw error;
}
}
async function getContractCollection(storage, contractType) {
const stored = await storage.getItem(`collection:${contractType}`);
if (!stored) return [];
try {
return JSON.parse(stored);
} catch (error) {
const errMessage = "message" in error ? error.message : "";
throw new Error(
`Failed to parse contract collection ${contractType} from storage: ${errMessage}`
);
}
}
// src/repositories/inMemory/swap-repository.ts
var InMemorySwapRepository = class {
version = 1;
swaps = /* @__PURE__ */ new Map();
async saveSwap(swap) {
this.swaps.set(swap.id, swap);
}
async deleteSwap(id) {
this.swaps.delete(id);
}
async getAllSwaps(filter) {
const swaps = [...this.swaps.values()];
if (!filter || Object.keys(filter).length === 0) return swaps;
const filtered = applySwapsFilter(swaps, filter);
return applyCreatedAtOrder(filtered, filter);
}
async clear() {
this.swaps.clear();
}
async [Symbol.asyncDispose]() {
await this.clear();
}
};
// src/index.ts
var sdkVersion = `boltz-swap/${version}`;
export {
ArkadeSwapsMessageHandler as ArkadeLightningMessageHandler,
ArkadeSwaps,
ArkadeSwapsMessageHandler,
BoltzRefundError,
BoltzSwapProvider,
InMemorySwapRepository,
IndexedDbSwapRepository,
InsufficientFundsError,
InvoiceExpiredError,
InvoiceFailedToPayError,
NetworkError,
PreimageFetchError,
QuoteRejectedError,
SchemaError,
ServiceWorkerArkadeSwaps as ServiceWorkerArkadeLightning,
ServiceWorkerArkadeSwaps,
SwapError,
SwapExpiredError,
SwapManager,
SwapNotFoundError,
TransactionFailedError,
decodeInvoice,
enrichReverseSwapPreimage,
enrichSubmarineSwapInvoice,
getInvoicePaymentHash,
getInvoiceSatoshis,
hasSubmarineStatusReached,
isChainClaimableStatus,
isChainFailedStatus,
isChainFinalStatus,
isChainPendingStatus,
isChainRefundableStatus,
isChainSignableStatus,
isChainSuccessStatus,
isChainSwapClaimable,
isChainSwapRefundable,
isPendingChainSwap,
isPendingReverseSwap,
isPendingSubmarineSwap,
isReverseClaimableStatus,
isReverseFailedStatus,
isReverseFinalStatus,
isReversePendingStatus,
isReverseSuccessStatus,
isReverseSwapClaimable,
isSubmarineFailedStatus,
isSubmarineFinalStatus,
isSubmarinePendingStatus,
isSubmarineRefundableStatus,
isSubmarineSuccessStatus,
isSubmarineSwapRefundable,
isValidArkAddress,
logger,
migrateToSwapRepository,
saveSwap,
sdkVersion,
setLogger,
updateChainSwapStatus,
updateReverseSwapStatus,
updateSubmarineSwapStatus,
verifySignatures
};