@arkade-os/boltz-swap 0.3.32 → 0.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -26
- package/dist/{arkade-swaps-CS8FZSVL.d.cts → arkade-swaps-BXAD1s8j.d.ts} +69 -8
- package/dist/{arkade-swaps-WiKCanCL.d.ts → arkade-swaps-CfMets16.d.cts} +69 -8
- package/dist/{chunk-NHBWNN6H.js → chunk-5K2FS2FE.js} +8 -27
- package/dist/chunk-SJQJQO7P.js +25 -0
- package/dist/{chunk-B3Q4TFWT.js → chunk-TDBUZE4N.js} +817 -866
- package/dist/expo/background.cjs +840 -892
- package/dist/expo/background.d.cts +3 -3
- package/dist/expo/background.d.ts +3 -3
- package/dist/expo/background.js +9 -20
- package/dist/expo/index.cjs +840 -870
- package/dist/expo/index.d.cts +8 -6
- package/dist/expo/index.d.ts +8 -6
- package/dist/expo/index.js +17 -20
- package/dist/index.cjs +1034 -1022
- package/dist/index.d.cts +95 -11
- package/dist/index.d.ts +95 -11
- package/dist/index.js +164 -119
- package/dist/repositories/realm/index.cjs +10 -22
- package/dist/repositories/realm/index.d.cts +7 -5
- package/dist/repositories/realm/index.d.ts +7 -5
- package/dist/repositories/realm/index.js +8 -22
- package/dist/repositories/sqlite/index.cjs +12 -23
- package/dist/repositories/sqlite/index.d.cts +1 -1
- package/dist/repositories/sqlite/index.d.ts +1 -1
- package/dist/repositories/sqlite/index.js +10 -23
- package/dist/{swapsPollProcessor-BF3uTFae.d.cts → swapsPollProcessor-BpAqG0V6.d.cts} +3 -3
- package/dist/{swapsPollProcessor-wYOMzldd.d.ts → swapsPollProcessor-DFVOAy_-.d.ts} +3 -3
- package/dist/{types-BBI7-KJ0.d.cts → types--axEWA8c.d.cts} +42 -4
- package/dist/{types-BBI7-KJ0.d.ts → types--axEWA8c.d.ts} +42 -4
- package/package.json +10 -25
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCreatedAtOrder,
|
|
3
|
+
applySwapsFilter,
|
|
4
|
+
hasImpossibleSwapsFilter
|
|
5
|
+
} from "./chunk-SJQJQO7P.js";
|
|
6
|
+
|
|
1
7
|
// src/errors.ts
|
|
2
8
|
var SwapError = class extends Error {
|
|
3
9
|
/** Whether the swap can still be claimed (default: false). */
|
|
@@ -7,7 +13,10 @@ var SwapError = class extends Error {
|
|
|
7
13
|
/** The pending swap associated with this error, if available. */
|
|
8
14
|
pendingSwap;
|
|
9
15
|
constructor(options = {}) {
|
|
10
|
-
super(
|
|
16
|
+
super(
|
|
17
|
+
options.message ?? "Error during swap.",
|
|
18
|
+
options.cause !== void 0 ? { cause: options.cause } : void 0
|
|
19
|
+
);
|
|
11
20
|
this.name = "SwapError";
|
|
12
21
|
this.isClaimable = options.isClaimable ?? false;
|
|
13
22
|
this.isRefundable = options.isRefundable ?? false;
|
|
@@ -99,6 +108,100 @@ var TransactionRefundedError = class extends SwapError {
|
|
|
99
108
|
this.name = "TransactionRefundedError";
|
|
100
109
|
}
|
|
101
110
|
};
|
|
111
|
+
var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
|
|
112
|
+
reason;
|
|
113
|
+
quotedAmount;
|
|
114
|
+
floor;
|
|
115
|
+
constructor(options) {
|
|
116
|
+
super({
|
|
117
|
+
message: options.message ?? _QuoteRejectedError.defaultMessage(options),
|
|
118
|
+
...options
|
|
119
|
+
});
|
|
120
|
+
this.name = "QuoteRejectedError";
|
|
121
|
+
this.reason = options.reason;
|
|
122
|
+
this.quotedAmount = "quotedAmount" in options ? options.quotedAmount : void 0;
|
|
123
|
+
this.floor = "floor" in options ? options.floor : void 0;
|
|
124
|
+
}
|
|
125
|
+
static defaultMessage(options) {
|
|
126
|
+
switch (options.reason) {
|
|
127
|
+
case "below_floor":
|
|
128
|
+
return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
|
|
129
|
+
case "non_positive":
|
|
130
|
+
return `Boltz quote ${options.quotedAmount} is not positive`;
|
|
131
|
+
case "non_safe_integer":
|
|
132
|
+
return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
|
|
133
|
+
case "no_baseline":
|
|
134
|
+
return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Serialize into a plain `Error` whose `.message` carries the full
|
|
139
|
+
* rejection payload as JSON behind a marker prefix. Structured clone
|
|
140
|
+
* (used by `postMessage` between page and service worker) preserves
|
|
141
|
+
* `Error.message` reliably but strips custom `.name` and own properties,
|
|
142
|
+
* so we move the typed data into the message field for transport.
|
|
143
|
+
*/
|
|
144
|
+
toTransportError() {
|
|
145
|
+
return new Error(
|
|
146
|
+
QUOTE_REJECTION_TRANSPORT_PREFIX + JSON.stringify({
|
|
147
|
+
reason: this.reason,
|
|
148
|
+
message: this.message,
|
|
149
|
+
quotedAmount: this.quotedAmount,
|
|
150
|
+
floor: this.floor
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Inverse of `toTransportError`. Returns a real `QuoteRejectedError` if
|
|
156
|
+
* `error` carries the transport prefix, else `null`.
|
|
157
|
+
*/
|
|
158
|
+
static fromTransportError(error) {
|
|
159
|
+
if (!(error instanceof Error) || !error.message.startsWith(QUOTE_REJECTION_TRANSPORT_PREFIX)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const payload = error.message.slice(QUOTE_REJECTION_TRANSPORT_PREFIX.length);
|
|
163
|
+
let data;
|
|
164
|
+
try {
|
|
165
|
+
data = JSON.parse(payload);
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (typeof data.reason !== "string" || !QUOTE_REJECTION_REASONS.has(data.reason)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const message = typeof data.message === "string" ? data.message : void 0;
|
|
173
|
+
const reason = data.reason;
|
|
174
|
+
const quotedAmount = typeof data.quotedAmount === "number" ? data.quotedAmount : null;
|
|
175
|
+
const floor = typeof data.floor === "number" ? data.floor : null;
|
|
176
|
+
switch (reason) {
|
|
177
|
+
case "below_floor":
|
|
178
|
+
if (quotedAmount === null || floor === null) return null;
|
|
179
|
+
return new _QuoteRejectedError({
|
|
180
|
+
reason,
|
|
181
|
+
quotedAmount,
|
|
182
|
+
floor,
|
|
183
|
+
message
|
|
184
|
+
});
|
|
185
|
+
case "non_positive":
|
|
186
|
+
case "non_safe_integer":
|
|
187
|
+
if (quotedAmount === null) return null;
|
|
188
|
+
return new _QuoteRejectedError({
|
|
189
|
+
reason,
|
|
190
|
+
quotedAmount,
|
|
191
|
+
message
|
|
192
|
+
});
|
|
193
|
+
case "no_baseline":
|
|
194
|
+
return new _QuoteRejectedError({ reason, message });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
|
|
199
|
+
var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
|
|
200
|
+
"below_floor",
|
|
201
|
+
"non_positive",
|
|
202
|
+
"non_safe_integer",
|
|
203
|
+
"no_baseline"
|
|
204
|
+
]);
|
|
102
205
|
var BoltzRefundError = class extends Error {
|
|
103
206
|
constructor(message, cause) {
|
|
104
207
|
super(message);
|
|
@@ -111,18 +214,10 @@ var BoltzRefundError = class extends Error {
|
|
|
111
214
|
import { Transaction } from "@arkade-os/sdk";
|
|
112
215
|
import { base64 } from "@scure/base";
|
|
113
216
|
var isSubmarineFailedStatus = (status) => {
|
|
114
|
-
return [
|
|
115
|
-
"invoice.failedToPay",
|
|
116
|
-
"transaction.lockupFailed",
|
|
117
|
-
"swap.expired"
|
|
118
|
-
].includes(status);
|
|
217
|
+
return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status);
|
|
119
218
|
};
|
|
120
219
|
var isSubmarineFinalStatus = (status) => {
|
|
121
|
-
return [
|
|
122
|
-
"invoice.failedToPay",
|
|
123
|
-
"transaction.claimed",
|
|
124
|
-
"swap.expired"
|
|
125
|
-
].includes(status);
|
|
220
|
+
return ["invoice.failedToPay", "transaction.claimed", "swap.expired"].includes(status);
|
|
126
221
|
};
|
|
127
222
|
var isSubmarinePendingStatus = (status) => {
|
|
128
223
|
return [
|
|
@@ -136,11 +231,7 @@ var isSubmarinePendingStatus = (status) => {
|
|
|
136
231
|
].includes(status);
|
|
137
232
|
};
|
|
138
233
|
var isSubmarineRefundableStatus = (status) => {
|
|
139
|
-
return [
|
|
140
|
-
"invoice.failedToPay",
|
|
141
|
-
"transaction.lockupFailed",
|
|
142
|
-
"swap.expired"
|
|
143
|
-
].includes(status);
|
|
234
|
+
return ["invoice.failedToPay", "transaction.lockupFailed", "swap.expired"].includes(status);
|
|
144
235
|
};
|
|
145
236
|
var isSubmarineSuccessStatus = (status) => {
|
|
146
237
|
return status === "transaction.claimed";
|
|
@@ -164,11 +255,7 @@ var isReverseFinalStatus = (status) => {
|
|
|
164
255
|
].includes(status);
|
|
165
256
|
};
|
|
166
257
|
var isReversePendingStatus = (status) => {
|
|
167
|
-
return [
|
|
168
|
-
"swap.created",
|
|
169
|
-
"transaction.mempool",
|
|
170
|
-
"transaction.confirmed"
|
|
171
|
-
].includes(status);
|
|
258
|
+
return ["swap.created", "transaction.mempool", "transaction.confirmed"].includes(status);
|
|
172
259
|
};
|
|
173
260
|
var isReverseClaimableStatus = (status) => {
|
|
174
261
|
return ["transaction.mempool", "transaction.confirmed"].includes(status);
|
|
@@ -180,10 +267,7 @@ var isChainFailedStatus = (status) => {
|
|
|
180
267
|
return ["transaction.failed", "swap.expired"].includes(status);
|
|
181
268
|
};
|
|
182
269
|
var isChainClaimableStatus = (status) => {
|
|
183
|
-
return [
|
|
184
|
-
"transaction.server.mempool",
|
|
185
|
-
"transaction.server.confirmed"
|
|
186
|
-
].includes(status);
|
|
270
|
+
return ["transaction.server.mempool", "transaction.server.confirmed"].includes(status);
|
|
187
271
|
};
|
|
188
272
|
var isChainFinalStatus = (status) => {
|
|
189
273
|
return [
|
|
@@ -322,8 +406,7 @@ var BASE_URLS = {
|
|
|
322
406
|
var isSwapNotFoundBody = (error) => {
|
|
323
407
|
const needle = "could not find swap";
|
|
324
408
|
const fromJson = error.errorData?.error;
|
|
325
|
-
if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle))
|
|
326
|
-
return true;
|
|
409
|
+
if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle)) return true;
|
|
327
410
|
return error.message.toLowerCase().includes(needle);
|
|
328
411
|
};
|
|
329
412
|
var BoltzSwapProvider = class {
|
|
@@ -337,10 +420,7 @@ var BoltzSwapProvider = class {
|
|
|
337
420
|
this.network = config.network;
|
|
338
421
|
this.referralId = config.referralId ?? "arkade-ts-sdk";
|
|
339
422
|
const apiUrl = config.apiUrl || BASE_URLS[config.network];
|
|
340
|
-
if (!apiUrl)
|
|
341
|
-
throw new Error(
|
|
342
|
-
`API URL is required for network: ${config.network}`
|
|
343
|
-
);
|
|
423
|
+
if (!apiUrl) throw new Error(`API URL is required for network: ${config.network}`);
|
|
344
424
|
this.apiUrl = apiUrl;
|
|
345
425
|
this.wsUrl = this.apiUrl.replace(/^http(s)?:\/\//, "ws$1://").replace("9069", "9004") + "/v2/ws";
|
|
346
426
|
}
|
|
@@ -359,10 +439,7 @@ var BoltzSwapProvider = class {
|
|
|
359
439
|
/** Returns current Lightning swap fees (submarine + reverse) from Boltz. */
|
|
360
440
|
async getFees() {
|
|
361
441
|
const [submarine, reverse] = await Promise.all([
|
|
362
|
-
this.request(
|
|
363
|
-
"/v2/swap/submarine",
|
|
364
|
-
"GET"
|
|
365
|
-
),
|
|
442
|
+
this.request("/v2/swap/submarine", "GET"),
|
|
366
443
|
this.request("/v2/swap/reverse", "GET")
|
|
367
444
|
]);
|
|
368
445
|
if (!isGetSubmarinePairsResponse(submarine))
|
|
@@ -382,10 +459,7 @@ var BoltzSwapProvider = class {
|
|
|
382
459
|
}
|
|
383
460
|
/** Returns current Lightning swap min/max limits from Boltz. */
|
|
384
461
|
async getLimits() {
|
|
385
|
-
const response = await this.request(
|
|
386
|
-
"/v2/swap/submarine",
|
|
387
|
-
"GET"
|
|
388
|
-
);
|
|
462
|
+
const response = await this.request("/v2/swap/submarine", "GET");
|
|
389
463
|
if (!isGetSubmarinePairsResponse(response))
|
|
390
464
|
throw new SchemaError({ message: "error fetching limits" });
|
|
391
465
|
return {
|
|
@@ -395,10 +469,7 @@ var BoltzSwapProvider = class {
|
|
|
395
469
|
}
|
|
396
470
|
/** Returns the current BTC chain tip height from Boltz. */
|
|
397
471
|
async getChainHeight() {
|
|
398
|
-
const response = await this.request(
|
|
399
|
-
"/v2/chain/heights",
|
|
400
|
-
"GET"
|
|
401
|
-
);
|
|
472
|
+
const response = await this.request("/v2/chain/heights", "GET");
|
|
402
473
|
if (typeof response?.BTC !== "number")
|
|
403
474
|
throw new SchemaError({
|
|
404
475
|
message: "error fetching chain heights"
|
|
@@ -428,10 +499,7 @@ var BoltzSwapProvider = class {
|
|
|
428
499
|
async getSwapStatus(id) {
|
|
429
500
|
let response;
|
|
430
501
|
try {
|
|
431
|
-
response = await this.request(
|
|
432
|
-
`/v2/swap/${id}`,
|
|
433
|
-
"GET"
|
|
434
|
-
);
|
|
502
|
+
response = await this.request(`/v2/swap/${id}`, "GET");
|
|
435
503
|
} catch (error) {
|
|
436
504
|
if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) {
|
|
437
505
|
throw new SwapNotFoundError(id, error.errorData);
|
|
@@ -527,12 +595,10 @@ var BoltzSwapProvider = class {
|
|
|
527
595
|
throw new SwapError({ message: "Invalid 'to' chain" });
|
|
528
596
|
if (["BTC", "ARK"].indexOf(from) === -1)
|
|
529
597
|
throw new SwapError({ message: "Invalid 'from' chain" });
|
|
530
|
-
if (to === from)
|
|
531
|
-
throw new SwapError({ message: "Invalid swap direction" });
|
|
598
|
+
if (to === from) throw new SwapError({ message: "Invalid swap direction" });
|
|
532
599
|
if (!preimageHash || preimageHash.length != 64)
|
|
533
600
|
throw new SwapError({ message: "Invalid preimageHash" });
|
|
534
|
-
if (feeSatsPerByte <= 0)
|
|
535
|
-
throw new SwapError({ message: "Invalid feeSatsPerByte" });
|
|
601
|
+
if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
|
|
536
602
|
if (serverLockAmount !== void 0 && userLockAmount !== void 0 || serverLockAmount === void 0 && userLockAmount === void 0)
|
|
537
603
|
throw new SwapError({
|
|
538
604
|
message: "Either serverLockAmount or userLockAmount must be provided"
|
|
@@ -587,12 +653,8 @@ var BoltzSwapProvider = class {
|
|
|
587
653
|
message: "Error refunding submarine swap"
|
|
588
654
|
});
|
|
589
655
|
return {
|
|
590
|
-
transaction: Transaction.fromPSBT(
|
|
591
|
-
|
|
592
|
-
),
|
|
593
|
-
checkpoint: Transaction.fromPSBT(
|
|
594
|
-
base64.decode(response.checkpoint)
|
|
595
|
-
)
|
|
656
|
+
transaction: Transaction.fromPSBT(base64.decode(response.transaction)),
|
|
657
|
+
checkpoint: Transaction.fromPSBT(base64.decode(response.checkpoint))
|
|
596
658
|
};
|
|
597
659
|
}
|
|
598
660
|
/** Requests Boltz co-signature for a chain swap refund. Returns signed transaction + checkpoint. */
|
|
@@ -611,53 +673,53 @@ var BoltzSwapProvider = class {
|
|
|
611
673
|
message: "Error refunding chain swap"
|
|
612
674
|
});
|
|
613
675
|
return {
|
|
614
|
-
transaction: Transaction.fromPSBT(
|
|
615
|
-
|
|
616
|
-
),
|
|
617
|
-
checkpoint: Transaction.fromPSBT(
|
|
618
|
-
base64.decode(response.checkpoint)
|
|
619
|
-
)
|
|
676
|
+
transaction: Transaction.fromPSBT(base64.decode(response.transaction)),
|
|
677
|
+
checkpoint: Transaction.fromPSBT(base64.decode(response.checkpoint))
|
|
620
678
|
};
|
|
621
679
|
}
|
|
622
|
-
/**
|
|
680
|
+
/**
|
|
681
|
+
* Monitors swap status updates and forwards them to the update callback.
|
|
682
|
+
* Prefers a WebSocket subscription; on connection error or premature close
|
|
683
|
+
* it falls back to REST polling so callers don't fail when the WS endpoint
|
|
684
|
+
* is flaky (observed in CI). Resolves when the swap reaches a terminal
|
|
685
|
+
* status.
|
|
686
|
+
*/
|
|
623
687
|
async monitorSwap(swapId, update) {
|
|
624
688
|
return new Promise((resolve, reject) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
);
|
|
689
|
+
let settled = false;
|
|
690
|
+
let lastStatus = null;
|
|
691
|
+
let pollTimer = null;
|
|
692
|
+
let webSocket = null;
|
|
693
|
+
let connectionTimeout = null;
|
|
694
|
+
const cleanup = () => {
|
|
695
|
+
if (connectionTimeout) {
|
|
696
|
+
clearTimeout(connectionTimeout);
|
|
697
|
+
connectionTimeout = null;
|
|
698
|
+
}
|
|
699
|
+
if (pollTimer) {
|
|
700
|
+
clearInterval(pollTimer);
|
|
701
|
+
pollTimer = null;
|
|
702
|
+
}
|
|
703
|
+
if (webSocket) {
|
|
704
|
+
try {
|
|
705
|
+
webSocket.close();
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
webSocket = null;
|
|
709
|
+
}
|
|
647
710
|
};
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
711
|
+
const finish = (err) => {
|
|
712
|
+
if (settled) return;
|
|
713
|
+
settled = true;
|
|
714
|
+
cleanup();
|
|
715
|
+
if (err) reject(err);
|
|
716
|
+
else resolve();
|
|
651
717
|
};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
reject(new SwapError({ message: msg.args[0].error }));
|
|
658
|
-
}
|
|
659
|
-
const status = msg.args[0].status;
|
|
660
|
-
const negotiable = status === "transaction.lockupFailed" && msg.args[0].failureDetails?.actual !== void 0 && msg.args[0].failureDetails?.expected !== void 0;
|
|
718
|
+
const handleStatus = (status, data) => {
|
|
719
|
+
if (settled) return;
|
|
720
|
+
if (status === lastStatus) return;
|
|
721
|
+
lastStatus = status;
|
|
722
|
+
const negotiable = status === "transaction.lockupFailed" && data?.failureDetails?.actual !== void 0 && data?.failureDetails?.expected !== void 0;
|
|
661
723
|
switch (status) {
|
|
662
724
|
case "invoice.settled":
|
|
663
725
|
case "transaction.claimed":
|
|
@@ -666,13 +728,13 @@ var BoltzSwapProvider = class {
|
|
|
666
728
|
case "invoice.failedToPay":
|
|
667
729
|
case "transaction.failed":
|
|
668
730
|
case "swap.expired":
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
731
|
+
update(status, data);
|
|
732
|
+
finish();
|
|
733
|
+
return;
|
|
672
734
|
case "transaction.lockupFailed":
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
735
|
+
update(status, data);
|
|
736
|
+
if (!negotiable) finish();
|
|
737
|
+
return;
|
|
676
738
|
case "invoice.paid":
|
|
677
739
|
case "invoice.pending":
|
|
678
740
|
case "invoice.set":
|
|
@@ -682,9 +744,77 @@ var BoltzSwapProvider = class {
|
|
|
682
744
|
case "transaction.claim.pending":
|
|
683
745
|
case "transaction.server.mempool":
|
|
684
746
|
case "transaction.server.confirmed":
|
|
685
|
-
update(status,
|
|
747
|
+
update(status, data);
|
|
748
|
+
return;
|
|
686
749
|
}
|
|
687
750
|
};
|
|
751
|
+
const startPolling = () => {
|
|
752
|
+
if (settled || pollTimer) return;
|
|
753
|
+
const poll = async () => {
|
|
754
|
+
if (settled) return;
|
|
755
|
+
try {
|
|
756
|
+
const result = await this.getSwapStatus(swapId);
|
|
757
|
+
handleStatus(result.status, { ...result, id: swapId });
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
pollTimer = setInterval(poll, 1e3);
|
|
762
|
+
poll();
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
webSocket = new globalThis.WebSocket(this.wsUrl);
|
|
766
|
+
connectionTimeout = setTimeout(() => {
|
|
767
|
+
try {
|
|
768
|
+
webSocket?.close();
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
webSocket = null;
|
|
772
|
+
startPolling();
|
|
773
|
+
}, 5e3);
|
|
774
|
+
webSocket.onerror = () => {
|
|
775
|
+
if (connectionTimeout) {
|
|
776
|
+
clearTimeout(connectionTimeout);
|
|
777
|
+
connectionTimeout = null;
|
|
778
|
+
}
|
|
779
|
+
webSocket = null;
|
|
780
|
+
startPolling();
|
|
781
|
+
};
|
|
782
|
+
webSocket.onopen = () => {
|
|
783
|
+
if (connectionTimeout) {
|
|
784
|
+
clearTimeout(connectionTimeout);
|
|
785
|
+
connectionTimeout = null;
|
|
786
|
+
}
|
|
787
|
+
webSocket?.send(
|
|
788
|
+
JSON.stringify({
|
|
789
|
+
op: "subscribe",
|
|
790
|
+
channel: "swap.update",
|
|
791
|
+
args: [swapId]
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
webSocket.onclose = () => {
|
|
796
|
+
if (connectionTimeout) {
|
|
797
|
+
clearTimeout(connectionTimeout);
|
|
798
|
+
connectionTimeout = null;
|
|
799
|
+
}
|
|
800
|
+
if (!settled && !pollTimer) startPolling();
|
|
801
|
+
};
|
|
802
|
+
webSocket.onmessage = (rawMsg) => {
|
|
803
|
+
try {
|
|
804
|
+
const msg = JSON.parse(rawMsg.data);
|
|
805
|
+
if (msg.event !== "update" || msg.args?.[0]?.id !== swapId) return;
|
|
806
|
+
if (msg.args[0].error) {
|
|
807
|
+
finish(new SwapError({ message: msg.args[0].error }));
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
handleStatus(msg.args[0].status, msg.args[0]);
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
} catch {
|
|
815
|
+
webSocket = null;
|
|
816
|
+
startPolling();
|
|
817
|
+
}
|
|
688
818
|
});
|
|
689
819
|
}
|
|
690
820
|
/** Returns current chain swap fees for a given pair (e.g. ARK→BTC). */
|
|
@@ -692,10 +822,7 @@ var BoltzSwapProvider = class {
|
|
|
692
822
|
if (from === to) {
|
|
693
823
|
throw new SwapError({ message: "Invalid chain pair" });
|
|
694
824
|
}
|
|
695
|
-
const response = await this.request(
|
|
696
|
-
"/v2/swap/chain",
|
|
697
|
-
"GET"
|
|
698
|
-
);
|
|
825
|
+
const response = await this.request("/v2/swap/chain", "GET");
|
|
699
826
|
if (!isGetChainPairsResponse(response))
|
|
700
827
|
throw new SchemaError({ message: "error fetching fees" });
|
|
701
828
|
if (!response[from]?.[to]) {
|
|
@@ -710,10 +837,7 @@ var BoltzSwapProvider = class {
|
|
|
710
837
|
if (from === to) {
|
|
711
838
|
throw new SwapError({ message: "Invalid chain pair" });
|
|
712
839
|
}
|
|
713
|
-
const response = await this.request(
|
|
714
|
-
"/v2/swap/chain",
|
|
715
|
-
"GET"
|
|
716
|
-
);
|
|
840
|
+
const response = await this.request("/v2/swap/chain", "GET");
|
|
717
841
|
if (!isGetChainPairsResponse(response))
|
|
718
842
|
throw new SchemaError({ message: "error fetching limits" });
|
|
719
843
|
if (!response[from]?.[to]) {
|
|
@@ -842,9 +966,7 @@ var BoltzSwapProvider = class {
|
|
|
842
966
|
return await response.json();
|
|
843
967
|
} catch (error) {
|
|
844
968
|
if (error instanceof NetworkError) throw error;
|
|
845
|
-
throw new NetworkError(
|
|
846
|
-
`Request to ${url} failed: ${error.message}`
|
|
847
|
-
);
|
|
969
|
+
throw new NetworkError(`Request to ${url} failed: ${error.message}`);
|
|
848
970
|
}
|
|
849
971
|
}
|
|
850
972
|
};
|
|
@@ -854,9 +976,7 @@ import bolt11 from "light-bolt11-decoder";
|
|
|
854
976
|
import { ArkAddress } from "@arkade-os/sdk";
|
|
855
977
|
var decodeInvoice = (invoice) => {
|
|
856
978
|
const decoded = bolt11.decode(invoice);
|
|
857
|
-
const millisats = Number(
|
|
858
|
-
decoded.sections.find((s) => s.name === "amount")?.value ?? "0"
|
|
859
|
-
);
|
|
979
|
+
const millisats = Number(decoded.sections.find((s) => s.name === "amount")?.value ?? "0");
|
|
860
980
|
return {
|
|
861
981
|
expiry: decoded.expiry ?? 3600,
|
|
862
982
|
amountSats: Math.floor(millisats / 1e3),
|
|
@@ -925,6 +1045,13 @@ var SwapManager = class _SwapManager {
|
|
|
925
1045
|
* enough that a real "swap unknown to this provider" surfaces quickly.
|
|
926
1046
|
*/
|
|
927
1047
|
static NOT_FOUND_THRESHOLD = 10;
|
|
1048
|
+
/**
|
|
1049
|
+
* Delay between re-attempts of a chain refund that left VTXOs deferred
|
|
1050
|
+
* (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
|
|
1051
|
+
* has elapsed). Boltz won't send another status update once the swap
|
|
1052
|
+
* is `swap.expired`, so the manager owns the local retry cadence.
|
|
1053
|
+
*/
|
|
1054
|
+
static REFUND_RETRY_DELAY_MS = 6e4;
|
|
928
1055
|
swapProvider;
|
|
929
1056
|
config;
|
|
930
1057
|
// Event listeners storage (supports multiple listeners per event)
|
|
@@ -943,6 +1070,11 @@ var SwapManager = class _SwapManager {
|
|
|
943
1070
|
reconnectTimer = null;
|
|
944
1071
|
initialPollTimer = null;
|
|
945
1072
|
pollRetryTimers = /* @__PURE__ */ new Map();
|
|
1073
|
+
// Per-swap retry timers for chain refunds that left work undone
|
|
1074
|
+
// (refundArk returned `skipped > 0`). The swap is held in
|
|
1075
|
+
// `monitoredSwaps` past its terminal Boltz status until the local
|
|
1076
|
+
// refund completes or the manager stops.
|
|
1077
|
+
refundRetryTimers = /* @__PURE__ */ new Map();
|
|
946
1078
|
// Per-swap counter of consecutive `SwapNotFoundError` responses from
|
|
947
1079
|
// `getSwapStatus`. Reset on any successful poll. Once a swap reaches
|
|
948
1080
|
// `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
|
|
@@ -996,9 +1128,7 @@ var SwapManager = class _SwapManager {
|
|
|
996
1128
|
this.wsConnectedListeners.add(config.events.onWebSocketConnected);
|
|
997
1129
|
}
|
|
998
1130
|
if (config.events?.onWebSocketDisconnected) {
|
|
999
|
-
this.wsDisconnectedListeners.add(
|
|
1000
|
-
config.events.onWebSocketDisconnected
|
|
1001
|
-
);
|
|
1131
|
+
this.wsDisconnectedListeners.add(config.events.onWebSocketDisconnected);
|
|
1002
1132
|
}
|
|
1003
1133
|
this.currentReconnectDelay = this.config.reconnectDelayMs;
|
|
1004
1134
|
this.currentPollRetryDelay = this.config.pollRetryDelayMs;
|
|
@@ -1141,6 +1271,10 @@ var SwapManager = class _SwapManager {
|
|
|
1141
1271
|
clearTimeout(timer);
|
|
1142
1272
|
}
|
|
1143
1273
|
this.pollRetryTimers.clear();
|
|
1274
|
+
for (const timer of this.refundRetryTimers.values()) {
|
|
1275
|
+
clearTimeout(timer);
|
|
1276
|
+
}
|
|
1277
|
+
this.refundRetryTimers.clear();
|
|
1144
1278
|
this.notFoundCounts.clear();
|
|
1145
1279
|
}
|
|
1146
1280
|
/**
|
|
@@ -1149,9 +1283,7 @@ var SwapManager = class _SwapManager {
|
|
|
1149
1283
|
*/
|
|
1150
1284
|
setPollInterval(ms) {
|
|
1151
1285
|
if (ms <= 0) {
|
|
1152
|
-
throw new RangeError(
|
|
1153
|
-
`setPollInterval: ms must be a positive number, got ${ms}`
|
|
1154
|
-
);
|
|
1286
|
+
throw new RangeError(`setPollInterval: ms must be a positive number, got ${ms}`);
|
|
1155
1287
|
}
|
|
1156
1288
|
const cappedInterval = Math.min(ms, this.config.maxPollIntervalMs);
|
|
1157
1289
|
if (cappedInterval !== ms) {
|
|
@@ -1160,10 +1292,7 @@ var SwapManager = class _SwapManager {
|
|
|
1160
1292
|
);
|
|
1161
1293
|
}
|
|
1162
1294
|
this.config.pollInterval = cappedInterval;
|
|
1163
|
-
this.currentPollRetryDelay = Math.min(
|
|
1164
|
-
cappedInterval,
|
|
1165
|
-
this.config.pollRetryDelayMs
|
|
1166
|
-
);
|
|
1295
|
+
this.currentPollRetryDelay = Math.min(cappedInterval, this.config.pollRetryDelayMs);
|
|
1167
1296
|
if (this.isRunning) {
|
|
1168
1297
|
if (this.usePollingFallback) {
|
|
1169
1298
|
this.startPollingFallback();
|
|
@@ -1202,6 +1331,11 @@ var SwapManager = class _SwapManager {
|
|
|
1202
1331
|
clearTimeout(retryTimer);
|
|
1203
1332
|
this.pollRetryTimers.delete(swapId);
|
|
1204
1333
|
}
|
|
1334
|
+
const refundRetryTimer = this.refundRetryTimers.get(swapId);
|
|
1335
|
+
if (refundRetryTimer) {
|
|
1336
|
+
clearTimeout(refundRetryTimer);
|
|
1337
|
+
this.refundRetryTimers.delete(swapId);
|
|
1338
|
+
}
|
|
1205
1339
|
this.notFoundCounts.delete(swapId);
|
|
1206
1340
|
logger.log(`Removed swap ${swapId} from monitoring`);
|
|
1207
1341
|
}
|
|
@@ -1248,9 +1382,7 @@ var SwapManager = class _SwapManager {
|
|
|
1248
1382
|
}
|
|
1249
1383
|
if (this.isFinalStatus(swap)) {
|
|
1250
1384
|
if (isPendingReverseSwap(swap)) {
|
|
1251
|
-
const response = await this.swapProvider.getReverseSwapTxId(
|
|
1252
|
-
swap.id
|
|
1253
|
-
);
|
|
1385
|
+
const response = await this.swapProvider.getReverseSwapTxId(swap.id);
|
|
1254
1386
|
return { txid: response.id };
|
|
1255
1387
|
}
|
|
1256
1388
|
if (isPendingSubmarineSwap(swap)) {
|
|
@@ -1267,31 +1399,19 @@ var SwapManager = class _SwapManager {
|
|
|
1267
1399
|
if (updatedSwap.status === "invoice.settled") {
|
|
1268
1400
|
this.swapProvider.getReverseSwapTxId(updatedSwap.id).then((response) => resolve({ txid: response.id })).catch((error) => reject(error));
|
|
1269
1401
|
} else {
|
|
1270
|
-
reject(
|
|
1271
|
-
new Error(
|
|
1272
|
-
`Swap failed with status: ${updatedSwap.status}`
|
|
1273
|
-
)
|
|
1274
|
-
);
|
|
1402
|
+
reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
|
|
1275
1403
|
}
|
|
1276
1404
|
} else if (isPendingSubmarineSwap(updatedSwap)) {
|
|
1277
1405
|
if (updatedSwap.status === "transaction.claimed") {
|
|
1278
1406
|
resolve({ txid: updatedSwap.id });
|
|
1279
1407
|
} else {
|
|
1280
|
-
reject(
|
|
1281
|
-
new Error(
|
|
1282
|
-
`Swap failed with status: ${updatedSwap.status}`
|
|
1283
|
-
)
|
|
1284
|
-
);
|
|
1408
|
+
reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
|
|
1285
1409
|
}
|
|
1286
1410
|
} else if (isPendingChainSwap(updatedSwap)) {
|
|
1287
1411
|
if (updatedSwap.status === "transaction.claimed") {
|
|
1288
1412
|
resolve({ txid: updatedSwap.id });
|
|
1289
1413
|
} else {
|
|
1290
|
-
reject(
|
|
1291
|
-
new Error(
|
|
1292
|
-
`Swap failed with status: ${updatedSwap.status}`
|
|
1293
|
-
)
|
|
1294
|
-
);
|
|
1414
|
+
reject(new Error(`Swap failed with status: ${updatedSwap.status}`));
|
|
1295
1415
|
}
|
|
1296
1416
|
}
|
|
1297
1417
|
};
|
|
@@ -1334,16 +1454,12 @@ var SwapManager = class _SwapManager {
|
|
|
1334
1454
|
const connectionTimeout = setTimeout(() => {
|
|
1335
1455
|
logger.error("WebSocket connection timeout");
|
|
1336
1456
|
this.websocket?.close();
|
|
1337
|
-
this.enterPollingFallback(
|
|
1338
|
-
new NetworkError("WebSocket connection failed")
|
|
1339
|
-
);
|
|
1457
|
+
this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
|
|
1340
1458
|
}, 1e4);
|
|
1341
1459
|
this.websocket.onerror = (error) => {
|
|
1342
1460
|
clearTimeout(connectionTimeout);
|
|
1343
1461
|
logger.error("WebSocket error:", error);
|
|
1344
|
-
this.enterPollingFallback(
|
|
1345
|
-
new NetworkError("WebSocket connection failed")
|
|
1346
|
-
);
|
|
1462
|
+
this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
|
|
1347
1463
|
};
|
|
1348
1464
|
this.websocket.onopen = () => {
|
|
1349
1465
|
clearTimeout(connectionTimeout);
|
|
@@ -1383,9 +1499,7 @@ var SwapManager = class _SwapManager {
|
|
|
1383
1499
|
};
|
|
1384
1500
|
} catch (error) {
|
|
1385
1501
|
logger.error("Failed to create WebSocket:", error);
|
|
1386
|
-
this.enterPollingFallback(
|
|
1387
|
-
new NetworkError("WebSocket connection failed")
|
|
1388
|
-
);
|
|
1502
|
+
this.enterPollingFallback(new NetworkError("WebSocket connection failed"));
|
|
1389
1503
|
}
|
|
1390
1504
|
}
|
|
1391
1505
|
/**
|
|
@@ -1420,11 +1534,8 @@ var SwapManager = class _SwapManager {
|
|
|
1420
1534
|
* Schedule WebSocket reconnection with exponential backoff
|
|
1421
1535
|
*/
|
|
1422
1536
|
scheduleReconnect() {
|
|
1423
|
-
if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport())
|
|
1424
|
-
|
|
1425
|
-
logger.log(
|
|
1426
|
-
`Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`
|
|
1427
|
-
);
|
|
1537
|
+
if (this.reconnectTimer || this.webSocketUnavailable || !this.hasWebSocketSupport()) return;
|
|
1538
|
+
logger.log(`Scheduling WebSocket reconnect in ${this.currentReconnectDelay}ms`);
|
|
1428
1539
|
this.reconnectTimer = setTimeout(() => {
|
|
1429
1540
|
this.reconnectTimer = null;
|
|
1430
1541
|
this.isReconnecting = false;
|
|
@@ -1439,8 +1550,7 @@ var SwapManager = class _SwapManager {
|
|
|
1439
1550
|
* Subscribe to a specific swap ID on the WebSocket
|
|
1440
1551
|
*/
|
|
1441
1552
|
subscribeToSwap(swapId) {
|
|
1442
|
-
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)
|
|
1443
|
-
return;
|
|
1553
|
+
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) return;
|
|
1444
1554
|
this.websocket.send(
|
|
1445
1555
|
JSON.stringify({
|
|
1446
1556
|
op: "subscribe",
|
|
@@ -1463,9 +1573,7 @@ var SwapManager = class _SwapManager {
|
|
|
1463
1573
|
if (msg.args[0].error) {
|
|
1464
1574
|
logger.error(`Swap ${swapId} error:`, msg.args[0].error);
|
|
1465
1575
|
const error = new Error(msg.args[0].error);
|
|
1466
|
-
this.swapFailedListeners.forEach(
|
|
1467
|
-
(listener) => listener(swap, error)
|
|
1468
|
-
);
|
|
1576
|
+
this.swapFailedListeners.forEach((listener) => listener(swap, error));
|
|
1469
1577
|
return;
|
|
1470
1578
|
}
|
|
1471
1579
|
const newStatus = msg.args[0].status;
|
|
@@ -1483,19 +1591,14 @@ var SwapManager = class _SwapManager {
|
|
|
1483
1591
|
const oldStatus = swap.status;
|
|
1484
1592
|
if (oldStatus === newStatus) return;
|
|
1485
1593
|
swap.status = newStatus;
|
|
1486
|
-
this.swapUpdateListeners.forEach(
|
|
1487
|
-
(listener) => listener(swap, oldStatus)
|
|
1488
|
-
);
|
|
1594
|
+
this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
|
|
1489
1595
|
const subscribers = this.swapSubscriptions.get(swap.id);
|
|
1490
1596
|
if (subscribers) {
|
|
1491
1597
|
subscribers.forEach((callback) => {
|
|
1492
1598
|
try {
|
|
1493
1599
|
callback(swap, oldStatus);
|
|
1494
1600
|
} catch (error) {
|
|
1495
|
-
logger.error(
|
|
1496
|
-
`Error in swap subscription callback for ${swap.id}:`,
|
|
1497
|
-
error
|
|
1498
|
-
);
|
|
1601
|
+
logger.error(`Error in swap subscription callback for ${swap.id}:`, error);
|
|
1499
1602
|
}
|
|
1500
1603
|
});
|
|
1501
1604
|
}
|
|
@@ -1504,15 +1607,57 @@ var SwapManager = class _SwapManager {
|
|
|
1504
1607
|
await this.executeAutonomousAction(swap);
|
|
1505
1608
|
}
|
|
1506
1609
|
if (this.isFinalStatus(swap)) {
|
|
1507
|
-
this.
|
|
1508
|
-
|
|
1509
|
-
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
1510
|
-
if (retryTimer) {
|
|
1511
|
-
clearTimeout(retryTimer);
|
|
1512
|
-
this.pollRetryTimers.delete(swap.id);
|
|
1610
|
+
if (this.refundRetryTimers.has(swap.id)) {
|
|
1611
|
+
return;
|
|
1513
1612
|
}
|
|
1514
|
-
this.
|
|
1613
|
+
this.finalizeMonitoredSwap(swap);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Drop a swap from monitoring and emit the terminal completion event.
|
|
1618
|
+
* Shared between the on-status-update finalization path and the
|
|
1619
|
+
* refund-retry finalization path (used when a previously-deferred
|
|
1620
|
+
* chain refund has finished its remaining work).
|
|
1621
|
+
*/
|
|
1622
|
+
finalizeMonitoredSwap(swap) {
|
|
1623
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
1624
|
+
this.monitoredSwaps.delete(swap.id);
|
|
1625
|
+
this.swapSubscriptions.delete(swap.id);
|
|
1626
|
+
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
1627
|
+
if (retryTimer) {
|
|
1628
|
+
clearTimeout(retryTimer);
|
|
1629
|
+
this.pollRetryTimers.delete(swap.id);
|
|
1515
1630
|
}
|
|
1631
|
+
const refundRetry = this.refundRetryTimers.get(swap.id);
|
|
1632
|
+
if (refundRetry) {
|
|
1633
|
+
clearTimeout(refundRetry);
|
|
1634
|
+
this.refundRetryTimers.delete(swap.id);
|
|
1635
|
+
}
|
|
1636
|
+
this.swapCompletedListeners.forEach((listener) => listener(swap));
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Schedule another `executeAutonomousAction` run for a chain swap whose
|
|
1640
|
+
* refund left VTXOs deferred. After the retry completes, if no further
|
|
1641
|
+
* deferral was reported, finalize monitoring cleanup.
|
|
1642
|
+
*/
|
|
1643
|
+
scheduleRefundRetry(swap, delayMs) {
|
|
1644
|
+
const existing = this.refundRetryTimers.get(swap.id);
|
|
1645
|
+
if (existing) clearTimeout(existing);
|
|
1646
|
+
this.refundRetryTimers.set(
|
|
1647
|
+
swap.id,
|
|
1648
|
+
setTimeout(async () => {
|
|
1649
|
+
this.refundRetryTimers.delete(swap.id);
|
|
1650
|
+
if (!this.isRunning) return;
|
|
1651
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
1652
|
+
try {
|
|
1653
|
+
await this.executeAutonomousAction(swap);
|
|
1654
|
+
} finally {
|
|
1655
|
+
if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
|
|
1656
|
+
this.finalizeMonitoredSwap(swap);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}, delayMs)
|
|
1660
|
+
);
|
|
1516
1661
|
}
|
|
1517
1662
|
/**
|
|
1518
1663
|
* Execute autonomous action based on swap status
|
|
@@ -1520,9 +1665,7 @@ var SwapManager = class _SwapManager {
|
|
|
1520
1665
|
*/
|
|
1521
1666
|
async executeAutonomousAction(swap) {
|
|
1522
1667
|
if (this.swapsInProgress.has(swap.id)) {
|
|
1523
|
-
logger.log(
|
|
1524
|
-
`Swap ${swap.id} is already being processed, skipping autonomous action`
|
|
1525
|
-
);
|
|
1668
|
+
logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
|
|
1526
1669
|
return;
|
|
1527
1670
|
}
|
|
1528
1671
|
try {
|
|
@@ -1537,9 +1680,7 @@ var SwapManager = class _SwapManager {
|
|
|
1537
1680
|
if (isReverseClaimableStatus(swap.status)) {
|
|
1538
1681
|
logger.log(`Auto-claiming reverse swap ${swap.id}`);
|
|
1539
1682
|
await this.executeClaimAction(swap);
|
|
1540
|
-
this.actionExecutedListeners.forEach(
|
|
1541
|
-
(listener) => listener(swap, "claim")
|
|
1542
|
-
);
|
|
1683
|
+
this.actionExecutedListeners.forEach((listener) => listener(swap, "claim"));
|
|
1543
1684
|
}
|
|
1544
1685
|
} else if (isPendingSubmarineSwap(swap)) {
|
|
1545
1686
|
if (!swap.request?.invoice || swap.request.invoice.length === 0) {
|
|
@@ -1551,9 +1692,7 @@ var SwapManager = class _SwapManager {
|
|
|
1551
1692
|
if (isSubmarineRefundableStatus(swap.status)) {
|
|
1552
1693
|
logger.log(`Auto-refunding submarine swap ${swap.id}`);
|
|
1553
1694
|
await this.executeRefundAction(swap);
|
|
1554
|
-
this.actionExecutedListeners.forEach(
|
|
1555
|
-
(listener) => listener(swap, "refund")
|
|
1556
|
-
);
|
|
1695
|
+
this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
|
|
1557
1696
|
}
|
|
1558
1697
|
} else if (isPendingChainSwap(swap)) {
|
|
1559
1698
|
if (isChainClaimableStatus(swap.status)) {
|
|
@@ -1573,10 +1712,27 @@ var SwapManager = class _SwapManager {
|
|
|
1573
1712
|
} else if (isChainRefundableStatus(swap.status)) {
|
|
1574
1713
|
if (swap.request.from === "ARK") {
|
|
1575
1714
|
logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
(
|
|
1579
|
-
|
|
1715
|
+
try {
|
|
1716
|
+
const outcome = await this.executeRefundArkAction(swap);
|
|
1717
|
+
if (outcome && outcome.skipped > 0) {
|
|
1718
|
+
logger.log(
|
|
1719
|
+
`Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
|
|
1720
|
+
);
|
|
1721
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
1722
|
+
}
|
|
1723
|
+
this.actionExecutedListeners.forEach(
|
|
1724
|
+
(listener) => listener(swap, "refundArk")
|
|
1725
|
+
);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
logger.error(
|
|
1728
|
+
`Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
|
|
1729
|
+
error
|
|
1730
|
+
);
|
|
1731
|
+
this.swapFailedListeners.forEach(
|
|
1732
|
+
(listener) => listener(swap, error)
|
|
1733
|
+
);
|
|
1734
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
1735
|
+
}
|
|
1580
1736
|
}
|
|
1581
1737
|
if (swap.request.from === "BTC") {
|
|
1582
1738
|
logger.warn(
|
|
@@ -1603,13 +1759,8 @@ var SwapManager = class _SwapManager {
|
|
|
1603
1759
|
}
|
|
1604
1760
|
}
|
|
1605
1761
|
} catch (error) {
|
|
1606
|
-
logger.error(
|
|
1607
|
-
|
|
1608
|
-
error
|
|
1609
|
-
);
|
|
1610
|
-
this.swapFailedListeners.forEach(
|
|
1611
|
-
(listener) => listener(swap, error)
|
|
1612
|
-
);
|
|
1762
|
+
logger.error(`Failed to execute autonomous action for swap ${swap.id}:`, error);
|
|
1763
|
+
this.swapFailedListeners.forEach((listener) => listener(swap, error));
|
|
1613
1764
|
} finally {
|
|
1614
1765
|
this.swapsInProgress.delete(swap.id);
|
|
1615
1766
|
}
|
|
@@ -1662,7 +1813,7 @@ var SwapManager = class _SwapManager {
|
|
|
1662
1813
|
logger.error("refundArk callback not set");
|
|
1663
1814
|
return;
|
|
1664
1815
|
}
|
|
1665
|
-
|
|
1816
|
+
return this.refundArkCallback(swap);
|
|
1666
1817
|
}
|
|
1667
1818
|
/**
|
|
1668
1819
|
* Execute sign server claim action for chain swap.
|
|
@@ -1710,9 +1861,7 @@ var SwapManager = class _SwapManager {
|
|
|
1710
1861
|
logger.log(`Resuming chain refund for swap ${swap.id}`);
|
|
1711
1862
|
await this.executeAutonomousAction(swap);
|
|
1712
1863
|
} else if (isPendingChainSwap(swap) && swap.request.to === "ARK" && isChainSignableStatus(swap.status)) {
|
|
1713
|
-
logger.log(
|
|
1714
|
-
`Resuming server claim signing for swap ${swap.id}`
|
|
1715
|
-
);
|
|
1864
|
+
logger.log(`Resuming server claim signing for swap ${swap.id}`);
|
|
1716
1865
|
await this.executeAutonomousAction(swap);
|
|
1717
1866
|
}
|
|
1718
1867
|
} catch (error) {
|
|
@@ -1764,16 +1913,12 @@ var SwapManager = class _SwapManager {
|
|
|
1764
1913
|
*/
|
|
1765
1914
|
async pollAllSwaps() {
|
|
1766
1915
|
if (this.monitoredSwaps.size === 0) return;
|
|
1767
|
-
const pollPromises = Array.from(this.monitoredSwaps.values()).map(
|
|
1768
|
-
(swap) => this.pollSingleSwap(swap)
|
|
1769
|
-
);
|
|
1916
|
+
const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
|
|
1770
1917
|
await Promise.allSettled(pollPromises);
|
|
1771
1918
|
}
|
|
1772
1919
|
async pollSingleSwap(swap) {
|
|
1773
1920
|
try {
|
|
1774
|
-
const statusResponse = await this.swapProvider.getSwapStatus(
|
|
1775
|
-
swap.id
|
|
1776
|
-
);
|
|
1921
|
+
const statusResponse = await this.swapProvider.getSwapStatus(swap.id);
|
|
1777
1922
|
this.notFoundCounts.delete(swap.id);
|
|
1778
1923
|
if (statusResponse.status !== swap.status) {
|
|
1779
1924
|
await this.handleSwapStatusUpdate(swap, statusResponse.status);
|
|
@@ -1784,9 +1929,7 @@ var SwapManager = class _SwapManager {
|
|
|
1784
1929
|
return;
|
|
1785
1930
|
}
|
|
1786
1931
|
if (error instanceof NetworkError && error.statusCode === 429) {
|
|
1787
|
-
logger.warn(
|
|
1788
|
-
`Rate-limited polling swap ${swap.id}, retrying in 2s`
|
|
1789
|
-
);
|
|
1932
|
+
logger.warn(`Rate-limited polling swap ${swap.id}, retrying in 2s`);
|
|
1790
1933
|
const existing = this.pollRetryTimers.get(swap.id);
|
|
1791
1934
|
if (existing) clearTimeout(existing);
|
|
1792
1935
|
this.pollRetryTimers.set(
|
|
@@ -1794,25 +1937,17 @@ var SwapManager = class _SwapManager {
|
|
|
1794
1937
|
setTimeout(async () => {
|
|
1795
1938
|
this.pollRetryTimers.delete(swap.id);
|
|
1796
1939
|
try {
|
|
1797
|
-
const retry = await this.swapProvider.getSwapStatus(
|
|
1798
|
-
swap.id
|
|
1799
|
-
);
|
|
1940
|
+
const retry = await this.swapProvider.getSwapStatus(swap.id);
|
|
1800
1941
|
this.notFoundCounts.delete(swap.id);
|
|
1801
1942
|
if (retry.status !== swap.status) {
|
|
1802
|
-
await this.handleSwapStatusUpdate(
|
|
1803
|
-
swap,
|
|
1804
|
-
retry.status
|
|
1805
|
-
);
|
|
1943
|
+
await this.handleSwapStatusUpdate(swap, retry.status);
|
|
1806
1944
|
}
|
|
1807
1945
|
} catch (retryError) {
|
|
1808
1946
|
if (retryError instanceof SwapNotFoundError) {
|
|
1809
1947
|
await this.handleSwapNotFound(swap);
|
|
1810
1948
|
return;
|
|
1811
1949
|
}
|
|
1812
|
-
logger.error(
|
|
1813
|
-
`Retry poll for swap ${swap.id} also failed:`,
|
|
1814
|
-
retryError
|
|
1815
|
-
);
|
|
1950
|
+
logger.error(`Retry poll for swap ${swap.id} also failed:`, retryError);
|
|
1816
1951
|
}
|
|
1817
1952
|
}, 2e3)
|
|
1818
1953
|
);
|
|
@@ -1831,6 +1966,7 @@ var SwapManager = class _SwapManager {
|
|
|
1831
1966
|
* Boltz endpoint).
|
|
1832
1967
|
*/
|
|
1833
1968
|
async handleSwapNotFound(swap) {
|
|
1969
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
1834
1970
|
const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
|
|
1835
1971
|
this.notFoundCounts.set(swap.id, count);
|
|
1836
1972
|
logger.warn(
|
|
@@ -1851,6 +1987,7 @@ var SwapManager = class _SwapManager {
|
|
|
1851
1987
|
* 404s without recovering anything.
|
|
1852
1988
|
*/
|
|
1853
1989
|
async markSwapAsUnknownToProvider(swap) {
|
|
1990
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
1854
1991
|
if (!this.monitoredSwaps.has(swap.id)) {
|
|
1855
1992
|
this.notFoundCounts.delete(swap.id);
|
|
1856
1993
|
return;
|
|
@@ -1863,10 +2000,13 @@ var SwapManager = class _SwapManager {
|
|
|
1863
2000
|
clearTimeout(retryTimer);
|
|
1864
2001
|
this.pollRetryTimers.delete(swap.id);
|
|
1865
2002
|
}
|
|
2003
|
+
const refundRetryTimer = this.refundRetryTimers.get(swap.id);
|
|
2004
|
+
if (refundRetryTimer) {
|
|
2005
|
+
clearTimeout(refundRetryTimer);
|
|
2006
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2007
|
+
}
|
|
1866
2008
|
this.notFoundCounts.delete(swap.id);
|
|
1867
|
-
this.swapUpdateListeners.forEach(
|
|
1868
|
-
(listener) => listener(swap, oldStatus)
|
|
1869
|
-
);
|
|
2009
|
+
this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
|
|
1870
2010
|
const subscribers = this.swapSubscriptions.get(swap.id);
|
|
1871
2011
|
if (subscribers) {
|
|
1872
2012
|
subscribers.forEach((callback) => {
|
|
@@ -1931,9 +2071,7 @@ async function saveSwap(swap, saver) {
|
|
|
1931
2071
|
if (saver.saveSubmarineSwap) {
|
|
1932
2072
|
await saver.saveSubmarineSwap(swap);
|
|
1933
2073
|
} else {
|
|
1934
|
-
console.warn(
|
|
1935
|
-
"No saveSubmarineSwap handler provided, swap not saved"
|
|
1936
|
-
);
|
|
2074
|
+
console.warn("No saveSubmarineSwap handler provided, swap not saved");
|
|
1937
2075
|
}
|
|
1938
2076
|
} else if (isPendingChainSwap(swap)) {
|
|
1939
2077
|
if (saver.saveChainSwap) {
|
|
@@ -2012,6 +2150,10 @@ function initDatabase(db) {
|
|
|
2012
2150
|
swapStore.createIndex("createdAt", "createdAt", { unique: false });
|
|
2013
2151
|
}
|
|
2014
2152
|
}
|
|
2153
|
+
function asArray(v) {
|
|
2154
|
+
if (v === void 0) return void 0;
|
|
2155
|
+
return Array.isArray(v) ? v : [v];
|
|
2156
|
+
}
|
|
2015
2157
|
var IndexedDbSwapRepository = class {
|
|
2016
2158
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
2017
2159
|
this.dbName = dbName;
|
|
@@ -2026,10 +2168,7 @@ var IndexedDbSwapRepository = class {
|
|
|
2026
2168
|
async saveSwap(swap) {
|
|
2027
2169
|
const db = await this.getDB();
|
|
2028
2170
|
return new Promise((resolve, reject) => {
|
|
2029
|
-
const transaction = db.transaction(
|
|
2030
|
-
[STORE_SWAPS_STATE],
|
|
2031
|
-
"readwrite"
|
|
2032
|
-
);
|
|
2171
|
+
const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
|
|
2033
2172
|
const store = transaction.objectStore(STORE_SWAPS_STATE);
|
|
2034
2173
|
const request = store.put(swap);
|
|
2035
2174
|
request.onsuccess = () => resolve();
|
|
@@ -2039,10 +2178,7 @@ var IndexedDbSwapRepository = class {
|
|
|
2039
2178
|
async deleteSwap(id) {
|
|
2040
2179
|
const db = await this.getDB();
|
|
2041
2180
|
return new Promise((resolve, reject) => {
|
|
2042
|
-
const transaction = db.transaction(
|
|
2043
|
-
[STORE_SWAPS_STATE],
|
|
2044
|
-
"readwrite"
|
|
2045
|
-
);
|
|
2181
|
+
const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
|
|
2046
2182
|
const store = transaction.objectStore(STORE_SWAPS_STATE);
|
|
2047
2183
|
const request = store.delete(id);
|
|
2048
2184
|
request.onsuccess = () => resolve();
|
|
@@ -2055,10 +2191,7 @@ var IndexedDbSwapRepository = class {
|
|
|
2055
2191
|
async clear() {
|
|
2056
2192
|
const db = await this.getDB();
|
|
2057
2193
|
return new Promise((resolve, reject) => {
|
|
2058
|
-
const transaction = db.transaction(
|
|
2059
|
-
[STORE_SWAPS_STATE],
|
|
2060
|
-
"readwrite"
|
|
2061
|
-
);
|
|
2194
|
+
const transaction = db.transaction([STORE_SWAPS_STATE], "readwrite");
|
|
2062
2195
|
const store = transaction.objectStore(STORE_SWAPS_STATE);
|
|
2063
2196
|
const request = store.clear();
|
|
2064
2197
|
request.onsuccess = () => resolve();
|
|
@@ -2075,11 +2208,10 @@ var IndexedDbSwapRepository = class {
|
|
|
2075
2208
|
request.onsuccess = () => resolve(request.result ?? []);
|
|
2076
2209
|
})
|
|
2077
2210
|
);
|
|
2078
|
-
return Promise.all(requests).then(
|
|
2079
|
-
(results) => results.flatMap((result) => result)
|
|
2080
|
-
);
|
|
2211
|
+
return Promise.all(requests).then((results) => results.flatMap((result) => result));
|
|
2081
2212
|
}
|
|
2082
2213
|
async getAllSwapsFromStore(filter) {
|
|
2214
|
+
if (hasImpossibleSwapsFilter(filter)) return [];
|
|
2083
2215
|
const db = await this.getDB();
|
|
2084
2216
|
const store = db.transaction([STORE_SWAPS_STATE], "readonly").objectStore(STORE_SWAPS_STATE);
|
|
2085
2217
|
if (!filter || Object.keys(filter).length === 0) {
|
|
@@ -2089,9 +2221,8 @@ var IndexedDbSwapRepository = class {
|
|
|
2089
2221
|
request.onerror = () => reject(request.error);
|
|
2090
2222
|
});
|
|
2091
2223
|
}
|
|
2092
|
-
const
|
|
2093
|
-
if (
|
|
2094
|
-
const ids = normalizedFilter.get("id");
|
|
2224
|
+
const ids = asArray(filter.id);
|
|
2225
|
+
if (ids) {
|
|
2095
2226
|
const swaps = await Promise.all(
|
|
2096
2227
|
ids.map(
|
|
2097
2228
|
(id) => new Promise((resolve, reject) => {
|
|
@@ -2101,34 +2232,17 @@ var IndexedDbSwapRepository = class {
|
|
|
2101
2232
|
})
|
|
2102
2233
|
)
|
|
2103
2234
|
);
|
|
2104
|
-
return
|
|
2105
|
-
this.applySwapsFilter(swaps, normalizedFilter),
|
|
2106
|
-
filter
|
|
2107
|
-
);
|
|
2235
|
+
return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
|
|
2108
2236
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
const swaps = await this.getSwapsByIndexValues(
|
|
2112
|
-
|
|
2113
|
-
"type",
|
|
2114
|
-
types
|
|
2115
|
-
);
|
|
2116
|
-
return this.sortIfNeeded(
|
|
2117
|
-
this.applySwapsFilter(swaps, normalizedFilter),
|
|
2118
|
-
filter
|
|
2119
|
-
);
|
|
2237
|
+
const types = asArray(filter.type);
|
|
2238
|
+
if (types) {
|
|
2239
|
+
const swaps = await this.getSwapsByIndexValues(store, "type", types);
|
|
2240
|
+
return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
|
|
2120
2241
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
const swaps = await this.getSwapsByIndexValues(
|
|
2124
|
-
|
|
2125
|
-
"status",
|
|
2126
|
-
ids
|
|
2127
|
-
);
|
|
2128
|
-
return this.sortIfNeeded(
|
|
2129
|
-
this.applySwapsFilter(swaps, normalizedFilter),
|
|
2130
|
-
filter
|
|
2131
|
-
);
|
|
2242
|
+
const statuses = asArray(filter.status);
|
|
2243
|
+
if (statuses) {
|
|
2244
|
+
const swaps = await this.getSwapsByIndexValues(store, "status", statuses);
|
|
2245
|
+
return applyCreatedAtOrder(applySwapsFilter(swaps, filter), filter);
|
|
2132
2246
|
}
|
|
2133
2247
|
if (filter.orderBy === "createdAt") {
|
|
2134
2248
|
return this.getAllSwapsByCreatedAt(store, filter.orderDirection);
|
|
@@ -2138,22 +2252,7 @@ var IndexedDbSwapRepository = class {
|
|
|
2138
2252
|
request.onsuccess = () => resolve(request.result ?? []);
|
|
2139
2253
|
request.onerror = () => reject(request.error);
|
|
2140
2254
|
});
|
|
2141
|
-
return
|
|
2142
|
-
this.applySwapsFilter(allSwaps, normalizedFilter),
|
|
2143
|
-
filter
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
applySwapsFilter(swaps, filter) {
|
|
2147
|
-
return swaps.filter((swap) => {
|
|
2148
|
-
if (swap === void 0) return false;
|
|
2149
|
-
if (filter.has("id") && !filter.get("id")?.includes(swap.id))
|
|
2150
|
-
return false;
|
|
2151
|
-
if (filter.has("status") && !filter.get("status")?.includes(swap.status))
|
|
2152
|
-
return false;
|
|
2153
|
-
if (filter.has("type") && !filter.get("type")?.includes(swap.type))
|
|
2154
|
-
return false;
|
|
2155
|
-
return true;
|
|
2156
|
-
});
|
|
2255
|
+
return applyCreatedAtOrder(applySwapsFilter(allSwaps, filter), filter);
|
|
2157
2256
|
}
|
|
2158
2257
|
async getAllSwapsByCreatedAt(store, orderDirection) {
|
|
2159
2258
|
const index = store.index("createdAt");
|
|
@@ -2173,30 +2272,12 @@ var IndexedDbSwapRepository = class {
|
|
|
2173
2272
|
};
|
|
2174
2273
|
});
|
|
2175
2274
|
}
|
|
2176
|
-
sortIfNeeded(swaps, filter) {
|
|
2177
|
-
if (filter?.orderBy !== "createdAt") return swaps;
|
|
2178
|
-
const direction = filter.orderDirection === "asc" ? 1 : -1;
|
|
2179
|
-
return swaps.slice().sort((a, b) => (a.createdAt - b.createdAt) * direction);
|
|
2180
|
-
}
|
|
2181
2275
|
async [Symbol.asyncDispose]() {
|
|
2182
2276
|
if (!this.db) return;
|
|
2183
2277
|
await closeDatabase(this.dbName);
|
|
2184
2278
|
this.db = null;
|
|
2185
2279
|
}
|
|
2186
2280
|
};
|
|
2187
|
-
var FILTER_FIELDS = ["id", "status", "type"];
|
|
2188
|
-
function normalizeFilter(filter) {
|
|
2189
|
-
const res = /* @__PURE__ */ new Map();
|
|
2190
|
-
FILTER_FIELDS.forEach((current) => {
|
|
2191
|
-
if (!filter?.[current]) return;
|
|
2192
|
-
if (Array.isArray(filter[current])) {
|
|
2193
|
-
res.set(current, filter[current]);
|
|
2194
|
-
} else {
|
|
2195
|
-
res.set(current, [filter[current]]);
|
|
2196
|
-
}
|
|
2197
|
-
});
|
|
2198
|
-
return res;
|
|
2199
|
-
}
|
|
2200
2281
|
|
|
2201
2282
|
// src/arkade-swaps.ts
|
|
2202
2283
|
import {
|
|
@@ -2225,8 +2306,7 @@ var findKeyIndex = (keys, target) => keys.findIndex((k) => equalBytes(target, k)
|
|
|
2225
2306
|
var assertPublicKeys = (keys) => {
|
|
2226
2307
|
const seen = /* @__PURE__ */ new Set();
|
|
2227
2308
|
for (const key of keys) {
|
|
2228
|
-
if (key.length !== 33)
|
|
2229
|
-
throw new Error(`public key must be 33 bytes, got ${key.length}`);
|
|
2309
|
+
if (key.length !== 33) throw new Error(`public key must be 33 bytes, got ${key.length}`);
|
|
2230
2310
|
const enc = hex3.encode(key);
|
|
2231
2311
|
if (seen.has(enc)) throw new Error(`duplicate public key ${enc}`);
|
|
2232
2312
|
seen.add(enc);
|
|
@@ -2234,9 +2314,7 @@ var assertPublicKeys = (keys) => {
|
|
|
2234
2314
|
};
|
|
2235
2315
|
var aggregateKeys = (publicKeys, tweak) => {
|
|
2236
2316
|
assertPublicKeys([...publicKeys]);
|
|
2237
|
-
return keyAggExport(
|
|
2238
|
-
keyAggregate([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : [])
|
|
2239
|
-
);
|
|
2317
|
+
return keyAggExport(keyAggregate([...publicKeys], tweak ? [tweak] : [], tweak ? [true] : []));
|
|
2240
2318
|
};
|
|
2241
2319
|
var MusigKeyAgg = class _MusigKeyAgg {
|
|
2242
2320
|
constructor(privateKey, myPublicKey, publicKeys, myIndex, aggPubkey, internalKey, _tweak) {
|
|
@@ -2283,18 +2361,12 @@ var MusigWithMessage = class {
|
|
|
2283
2361
|
this.msg = msg;
|
|
2284
2362
|
}
|
|
2285
2363
|
generateNonce() {
|
|
2286
|
-
const nonce = nonceGen(
|
|
2287
|
-
this.myPublicKey,
|
|
2288
|
-
this.privateKey,
|
|
2289
|
-
this.aggPubkey,
|
|
2290
|
-
this.msg
|
|
2291
|
-
);
|
|
2364
|
+
const nonce = nonceGen(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
|
|
2292
2365
|
return new MusigWithNonce(
|
|
2293
2366
|
this.privateKey,
|
|
2294
2367
|
this.myPublicKey,
|
|
2295
2368
|
this.publicKeys,
|
|
2296
2369
|
this.myIndex,
|
|
2297
|
-
this.aggPubkey,
|
|
2298
2370
|
this.tweak,
|
|
2299
2371
|
this.msg,
|
|
2300
2372
|
nonce
|
|
@@ -2302,12 +2374,11 @@ var MusigWithMessage = class {
|
|
|
2302
2374
|
}
|
|
2303
2375
|
};
|
|
2304
2376
|
var MusigWithNonce = class {
|
|
2305
|
-
constructor(privateKey, myPublicKey, publicKeys, myIndex,
|
|
2377
|
+
constructor(privateKey, myPublicKey, publicKeys, myIndex, tweak, msg, nonce) {
|
|
2306
2378
|
this.privateKey = privateKey;
|
|
2307
2379
|
this.myPublicKey = myPublicKey;
|
|
2308
2380
|
this.publicKeys = publicKeys;
|
|
2309
2381
|
this.myIndex = myIndex;
|
|
2310
|
-
this.aggPubkey = aggPubkey;
|
|
2311
2382
|
this.tweak = tweak;
|
|
2312
2383
|
this.msg = msg;
|
|
2313
2384
|
this.nonce = nonce;
|
|
@@ -2339,10 +2410,8 @@ var MusigWithNonce = class {
|
|
|
2339
2410
|
const aggregatedNonce = nonceAggregate([...ordered]);
|
|
2340
2411
|
return new MusigNoncesAggregated(
|
|
2341
2412
|
this.privateKey,
|
|
2342
|
-
this.myPublicKey,
|
|
2343
2413
|
this.publicKeys,
|
|
2344
2414
|
this.myIndex,
|
|
2345
|
-
this.aggPubkey,
|
|
2346
2415
|
this.tweak,
|
|
2347
2416
|
this.msg,
|
|
2348
2417
|
this.nonce,
|
|
@@ -2352,12 +2421,10 @@ var MusigWithNonce = class {
|
|
|
2352
2421
|
}
|
|
2353
2422
|
};
|
|
2354
2423
|
var MusigNoncesAggregated = class {
|
|
2355
|
-
constructor(privateKey,
|
|
2424
|
+
constructor(privateKey, publicKeys, myIndex, tweak, msg, nonce, pubNonces, aggregatedNonce) {
|
|
2356
2425
|
this.privateKey = privateKey;
|
|
2357
|
-
this.myPublicKey = myPublicKey;
|
|
2358
2426
|
this.publicKeys = publicKeys;
|
|
2359
2427
|
this.myIndex = myIndex;
|
|
2360
|
-
this.aggPubkey = aggPubkey;
|
|
2361
2428
|
this.tweak = tweak;
|
|
2362
2429
|
this.msg = msg;
|
|
2363
2430
|
this.nonce = nonce;
|
|
@@ -2403,11 +2470,7 @@ var MusigSession = class {
|
|
|
2403
2470
|
const index = typeof publicKeyOrIndex === "number" ? publicKeyOrIndex : findKeyIndex(this.publicKeys, publicKeyOrIndex);
|
|
2404
2471
|
if (index < 0 || index >= this.publicKeys.length)
|
|
2405
2472
|
throw new Error("public key not found or index out of range");
|
|
2406
|
-
if (!this.session.partialSigVerify(
|
|
2407
|
-
signature,
|
|
2408
|
-
[...this.pubNonces],
|
|
2409
|
-
index
|
|
2410
|
-
)) {
|
|
2473
|
+
if (!this.session.partialSigVerify(signature, [...this.pubNonces], index)) {
|
|
2411
2474
|
throw new Error("invalid partial signature");
|
|
2412
2475
|
}
|
|
2413
2476
|
this.partialSignatures[index] = signature;
|
|
@@ -2416,12 +2479,7 @@ var MusigSession = class {
|
|
|
2416
2479
|
signPartial() {
|
|
2417
2480
|
const sig = this.session.sign(this.nonce.secret, this.privateKey, true);
|
|
2418
2481
|
this.partialSignatures[this.myIndex] = sig;
|
|
2419
|
-
return new MusigSigned(
|
|
2420
|
-
this.session,
|
|
2421
|
-
[...this.partialSignatures],
|
|
2422
|
-
sig,
|
|
2423
|
-
this.nonce.public
|
|
2424
|
-
);
|
|
2482
|
+
return new MusigSigned(this.session, [...this.partialSignatures], sig, this.nonce.public);
|
|
2425
2483
|
}
|
|
2426
2484
|
};
|
|
2427
2485
|
var MusigSigned = class {
|
|
@@ -2435,14 +2493,11 @@ var MusigSigned = class {
|
|
|
2435
2493
|
if (this.partialSignatures.some((s) => s === null)) {
|
|
2436
2494
|
throw new Error("not all partial signatures are set");
|
|
2437
2495
|
}
|
|
2438
|
-
return this.session.partialSigAgg(
|
|
2439
|
-
this.partialSignatures
|
|
2440
|
-
);
|
|
2496
|
+
return this.session.partialSigAgg(this.partialSignatures);
|
|
2441
2497
|
}
|
|
2442
2498
|
};
|
|
2443
2499
|
var create = (privateKey, publicKeys) => {
|
|
2444
|
-
if (publicKeys.length < 2)
|
|
2445
|
-
throw new Error("need at least 2 keys to aggregate");
|
|
2500
|
+
if (publicKeys.length < 2) throw new Error("need at least 2 keys to aggregate");
|
|
2446
2501
|
const keys = [...publicKeys];
|
|
2447
2502
|
assertPublicKeys(keys);
|
|
2448
2503
|
Object.freeze(keys);
|
|
@@ -2450,14 +2505,7 @@ var create = (privateKey, publicKeys) => {
|
|
|
2450
2505
|
const myIndex = findKeyIndex(keys, myPublicKey);
|
|
2451
2506
|
if (myIndex === -1) throw new Error("our key is not in publicKeys");
|
|
2452
2507
|
const aggPubkey = aggregateKeys(keys);
|
|
2453
|
-
return new MusigKeyAgg(
|
|
2454
|
-
privateKey,
|
|
2455
|
-
myPublicKey,
|
|
2456
|
-
keys,
|
|
2457
|
-
myIndex,
|
|
2458
|
-
aggPubkey,
|
|
2459
|
-
aggPubkey
|
|
2460
|
-
);
|
|
2508
|
+
return new MusigKeyAgg(privateKey, myPublicKey, keys, myIndex, aggPubkey, aggPubkey);
|
|
2461
2509
|
};
|
|
2462
2510
|
|
|
2463
2511
|
// src/utils/boltz-swap-tx.ts
|
|
@@ -2533,9 +2581,7 @@ var taprootHashTree = (tree) => {
|
|
|
2533
2581
|
};
|
|
2534
2582
|
var tweakMusig = (musig, tree) => {
|
|
2535
2583
|
const tweak = taprootHashTree(tree).hash;
|
|
2536
|
-
return musig.xonlyTweakAdd(
|
|
2537
|
-
schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak)
|
|
2538
|
-
);
|
|
2584
|
+
return musig.xonlyTweakAdd(schnorr.utils.taggedHash("TapTweak", musig.aggPubkey, tweak));
|
|
2539
2585
|
};
|
|
2540
2586
|
var toXOnly = (pubKey) => {
|
|
2541
2587
|
if (pubKey.length === 32) return pubKey;
|
|
@@ -2547,9 +2593,7 @@ var toXOnly = (pubKey) => {
|
|
|
2547
2593
|
}
|
|
2548
2594
|
return pubKey.subarray(1, 33);
|
|
2549
2595
|
}
|
|
2550
|
-
throw new Error(
|
|
2551
|
-
`Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`
|
|
2552
|
-
);
|
|
2596
|
+
throw new Error(`Invalid public key length: expected 32 or 33 bytes, got ${pubKey.length}`);
|
|
2553
2597
|
};
|
|
2554
2598
|
var p2trScript = (publicKey) => Script.encode(["OP_1", toXOnly(publicKey)]);
|
|
2555
2599
|
var detectSwapOutput = (tweakedKey, transaction) => {
|
|
@@ -2564,8 +2608,7 @@ var detectSwapOutput = (tweakedKey, transaction) => {
|
|
|
2564
2608
|
};
|
|
2565
2609
|
var DUMMY_TAPROOT_SIGNATURE = new Uint8Array(64);
|
|
2566
2610
|
var constructClaimTransaction = (utxo, destinationScript, fee) => {
|
|
2567
|
-
if (fee < BigInt(0) || fee >= utxo.amount)
|
|
2568
|
-
throw new Error("fee exceeds utxo amount");
|
|
2611
|
+
if (fee < BigInt(0) || fee >= utxo.amount) throw new Error("fee exceeds utxo amount");
|
|
2569
2612
|
const tx = new Transaction2({ version: 2 });
|
|
2570
2613
|
tx.addOutput({
|
|
2571
2614
|
amount: utxo.amount - fee,
|
|
@@ -2584,9 +2627,7 @@ var constructClaimTransaction = (utxo, destinationScript, fee) => {
|
|
|
2584
2627
|
};
|
|
2585
2628
|
var targetFee = (satPerVbyte, constructTx) => {
|
|
2586
2629
|
const tx = constructTx(BigInt(1));
|
|
2587
|
-
return constructTx(
|
|
2588
|
-
BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte))
|
|
2589
|
-
);
|
|
2630
|
+
return constructTx(BigInt(Math.ceil((tx.vsize + tx.inputsLength) * satPerVbyte)));
|
|
2590
2631
|
};
|
|
2591
2632
|
|
|
2592
2633
|
// src/utils/restoration.ts
|
|
@@ -2610,10 +2651,7 @@ function extractTimeLockFromLeafOutput(scriptHex) {
|
|
|
2610
2651
|
const data = opcodes[hasCSV - 1];
|
|
2611
2652
|
if (data instanceof Uint8Array) {
|
|
2612
2653
|
const dataBytes = new Uint8Array(data).reverse();
|
|
2613
|
-
const {
|
|
2614
|
-
blocks,
|
|
2615
|
-
seconds
|
|
2616
|
-
} = bip68.decode(
|
|
2654
|
+
const { blocks, seconds } = bip68.decode(
|
|
2617
2655
|
parseInt(hex5.encode(dataBytes), 16)
|
|
2618
2656
|
);
|
|
2619
2657
|
return blocks ?? seconds ?? 0;
|
|
@@ -2634,11 +2672,7 @@ function extractInvoiceAmount(amountSats, fees) {
|
|
|
2634
2672
|
}
|
|
2635
2673
|
|
|
2636
2674
|
// src/utils/identity.ts
|
|
2637
|
-
import {
|
|
2638
|
-
ConditionWitness,
|
|
2639
|
-
setArkPsbtField,
|
|
2640
|
-
Transaction as Transaction3
|
|
2641
|
-
} from "@arkade-os/sdk";
|
|
2675
|
+
import { ConditionWitness, setArkPsbtField, Transaction as Transaction3 } from "@arkade-os/sdk";
|
|
2642
2676
|
function claimVHTLCIdentity(identity, preimage) {
|
|
2643
2677
|
return {
|
|
2644
2678
|
...identity,
|
|
@@ -2647,13 +2681,8 @@ function claimVHTLCIdentity(identity, preimage) {
|
|
|
2647
2681
|
let signedTx = await identity.sign(cpy, inputIndexes);
|
|
2648
2682
|
signedTx = Transaction3.fromPSBT(signedTx.toPSBT());
|
|
2649
2683
|
if (preimage) {
|
|
2650
|
-
for (const inputIndex of inputIndexes || Array.from(
|
|
2651
|
-
|
|
2652
|
-
(_, i) => i
|
|
2653
|
-
)) {
|
|
2654
|
-
setArkPsbtField(signedTx, inputIndex, ConditionWitness, [
|
|
2655
|
-
preimage
|
|
2656
|
-
]);
|
|
2684
|
+
for (const inputIndex of inputIndexes || Array.from({ length: signedTx.inputsLength }, (_, i) => i)) {
|
|
2685
|
+
setArkPsbtField(signedTx, inputIndex, ConditionWitness, [preimage]);
|
|
2657
2686
|
}
|
|
2658
2687
|
}
|
|
2659
2688
|
return signedTx;
|
|
@@ -2728,17 +2757,13 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
|
|
|
2728
2757
|
if (!sweepTapTreeRoot) {
|
|
2729
2758
|
throw new Error("Sweep tap tree root not set");
|
|
2730
2759
|
}
|
|
2731
|
-
const xOnlyPublicKeys = event.cosignersPublicKeys.map(
|
|
2732
|
-
(k) => k.slice(2)
|
|
2733
|
-
);
|
|
2760
|
+
const xOnlyPublicKeys = event.cosignersPublicKeys.map((k) => k.slice(2));
|
|
2734
2761
|
const signerPublicKey = await session.getPublicKey();
|
|
2735
2762
|
const xonlySignerPublicKey = signerPublicKey.subarray(1);
|
|
2736
2763
|
if (!xOnlyPublicKeys.includes(hex6.encode(xonlySignerPublicKey))) {
|
|
2737
2764
|
return { skip: true };
|
|
2738
2765
|
}
|
|
2739
|
-
const commitmentTx = Transaction4.fromPSBT(
|
|
2740
|
-
base642.decode(event.unsignedCommitmentTx)
|
|
2741
|
-
);
|
|
2766
|
+
const commitmentTx = Transaction4.fromPSBT(base642.decode(event.unsignedCommitmentTx));
|
|
2742
2767
|
validateVtxoTxGraph(vtxoTree, commitmentTx, sweepTapTreeRoot);
|
|
2743
2768
|
const sharedOutput = commitmentTx.getOutput(0);
|
|
2744
2769
|
if (!sharedOutput?.amount) {
|
|
@@ -2754,18 +2779,11 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
|
|
|
2754
2779
|
if (!session) {
|
|
2755
2780
|
return { fullySigned: true };
|
|
2756
2781
|
}
|
|
2757
|
-
const { hasAllNonces } = await session.aggregatedNonces(
|
|
2758
|
-
event.txid,
|
|
2759
|
-
event.nonces
|
|
2760
|
-
);
|
|
2782
|
+
const { hasAllNonces } = await session.aggregatedNonces(event.txid, event.nonces);
|
|
2761
2783
|
if (!hasAllNonces) return { fullySigned: false };
|
|
2762
2784
|
const signatures = await session.sign();
|
|
2763
2785
|
const pubkey = hex6.encode(await session.getPublicKey());
|
|
2764
|
-
await arkProvider.submitTreeSignatures(
|
|
2765
|
-
event.id,
|
|
2766
|
-
pubkey,
|
|
2767
|
-
signatures
|
|
2768
|
-
);
|
|
2786
|
+
await arkProvider.submitTreeSignatures(event.id, pubkey, signatures);
|
|
2769
2787
|
return { fullySigned: true };
|
|
2770
2788
|
},
|
|
2771
2789
|
onBatchFinalization: async (event, _, connectorTree) => {
|
|
@@ -2773,9 +2791,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
|
|
|
2773
2791
|
return;
|
|
2774
2792
|
}
|
|
2775
2793
|
if (!connectorTree) {
|
|
2776
|
-
throw new Error(
|
|
2777
|
-
"BatchFinalizationEvent: expected connector tree to be defined"
|
|
2778
|
-
);
|
|
2794
|
+
throw new Error("BatchFinalizationEvent: expected connector tree to be defined");
|
|
2779
2795
|
}
|
|
2780
2796
|
validateConnectorsTxGraph(event.commitmentTx, connectorTree);
|
|
2781
2797
|
const connectors = connectorTree.leaves();
|
|
@@ -2790,9 +2806,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
|
|
|
2790
2806
|
connectors[connectorIndex]
|
|
2791
2807
|
);
|
|
2792
2808
|
const signedForfeitTx = await identity.sign(forfeitTx);
|
|
2793
|
-
await arkProvider.submitSignedForfeitTxs([
|
|
2794
|
-
base642.encode(signedForfeitTx.toPSBT())
|
|
2795
|
-
]);
|
|
2809
|
+
await arkProvider.submitSignedForfeitTxs([base642.encode(signedForfeitTx.toPSBT())]);
|
|
2796
2810
|
}
|
|
2797
2811
|
};
|
|
2798
2812
|
}
|
|
@@ -2852,36 +2866,22 @@ var createVHTLCScript = (args) => {
|
|
|
2852
2866
|
serverPubkey,
|
|
2853
2867
|
timeoutBlockHeights: vhtlcTimeouts
|
|
2854
2868
|
} = args;
|
|
2855
|
-
const receiverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
);
|
|
2859
|
-
const senderXOnlyPublicKey = normalizeToXOnlyKey(
|
|
2860
|
-
hex7.decode(senderPubkey),
|
|
2861
|
-
"sender"
|
|
2862
|
-
);
|
|
2863
|
-
const serverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
2864
|
-
hex7.decode(serverPubkey),
|
|
2865
|
-
"server"
|
|
2866
|
-
);
|
|
2869
|
+
const receiverXOnlyPublicKey = normalizeToXOnlyKey(hex7.decode(receiverPubkey), "receiver");
|
|
2870
|
+
const senderXOnlyPublicKey = normalizeToXOnlyKey(hex7.decode(senderPubkey), "sender");
|
|
2871
|
+
const serverXOnlyPublicKey = normalizeToXOnlyKey(hex7.decode(serverPubkey), "server");
|
|
2867
2872
|
const vhtlcScript = new VHTLC.Script({
|
|
2868
2873
|
preimageHash: ripemd160(preimageHash),
|
|
2869
2874
|
sender: senderXOnlyPublicKey,
|
|
2870
2875
|
receiver: receiverXOnlyPublicKey,
|
|
2871
2876
|
server: serverXOnlyPublicKey,
|
|
2872
2877
|
refundLocktime: BigInt(vhtlcTimeouts.refund),
|
|
2873
|
-
unilateralClaimDelay: toBip68RelativeTimelock(
|
|
2874
|
-
|
|
2875
|
-
),
|
|
2876
|
-
unilateralRefundDelay: toBip68RelativeTimelock(
|
|
2877
|
-
vhtlcTimeouts.unilateralRefund
|
|
2878
|
-
),
|
|
2878
|
+
unilateralClaimDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralClaim),
|
|
2879
|
+
unilateralRefundDelay: toBip68RelativeTimelock(vhtlcTimeouts.unilateralRefund),
|
|
2879
2880
|
unilateralRefundWithoutReceiverDelay: toBip68RelativeTimelock(
|
|
2880
2881
|
vhtlcTimeouts.unilateralRefundWithoutReceiver
|
|
2881
2882
|
)
|
|
2882
2883
|
});
|
|
2883
|
-
if (!vhtlcScript.claimScript)
|
|
2884
|
-
throw new Error("Failed to create VHTLC script");
|
|
2884
|
+
if (!vhtlcScript.claimScript) throw new Error("Failed to create VHTLC script");
|
|
2885
2885
|
const hrp = network === "bitcoin" ? "ark" : "tark";
|
|
2886
2886
|
const vhtlcAddress = vhtlcScript.address(hrp, serverXOnlyPublicKey).encode();
|
|
2887
2887
|
return { vhtlcScript, vhtlcAddress };
|
|
@@ -2915,11 +2915,7 @@ var joinBatch = async (arkProvider, identity, input, output, {
|
|
|
2915
2915
|
unknown: [VtxoTaprootTree.encode(input.tapTree)],
|
|
2916
2916
|
sequence: getSequence2(input.tapLeafScript)
|
|
2917
2917
|
};
|
|
2918
|
-
const registerIntent = Intent.create(
|
|
2919
|
-
intentMessage,
|
|
2920
|
-
[intentInput],
|
|
2921
|
-
[output]
|
|
2922
|
-
);
|
|
2918
|
+
const registerIntent = Intent.create(intentMessage, [intentInput], [output]);
|
|
2923
2919
|
const deleteIntent = Intent.create(deleteMessage, [intentInput]);
|
|
2924
2920
|
const [signedRegisterIntent, signedDeleteIntent] = await Promise.all([
|
|
2925
2921
|
identity.sign(registerIntent),
|
|
@@ -2943,14 +2939,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
|
|
|
2943
2939
|
normalizeToXOnlyKey(forfeitPubkey, "forfeit"),
|
|
2944
2940
|
isRecoverable2 ? void 0 : OutScript.encode(decodedAddress)
|
|
2945
2941
|
);
|
|
2946
|
-
const topics = [
|
|
2947
|
-
|
|
2948
|
-
`${input.txid}:${input.vout}`
|
|
2949
|
-
];
|
|
2950
|
-
const eventStream = arkProvider.getEventStream(
|
|
2951
|
-
abortController.signal,
|
|
2952
|
-
topics
|
|
2953
|
-
);
|
|
2942
|
+
const topics = [hex7.encode(signerPublicKey), `${input.txid}:${input.vout}`];
|
|
2943
|
+
const eventStream = arkProvider.getEventStream(abortController.signal, topics);
|
|
2954
2944
|
const commitmentTxid = await Batch.join(eventStream, handler, {
|
|
2955
2945
|
abortController
|
|
2956
2946
|
});
|
|
@@ -2971,14 +2961,8 @@ var joinBatch = async (arkProvider, identity, input, output, {
|
|
|
2971
2961
|
};
|
|
2972
2962
|
var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKey, input, output, arkInfo, arkProvider) => {
|
|
2973
2963
|
const rawCheckpointTapscript = hex7.decode(arkInfo.checkpointTapscript);
|
|
2974
|
-
const serverUnrollScript = CSVMultisigTapscript2.decode(
|
|
2975
|
-
|
|
2976
|
-
);
|
|
2977
|
-
const { arkTx, checkpoints } = buildOffchainTx(
|
|
2978
|
-
[input],
|
|
2979
|
-
[output],
|
|
2980
|
-
serverUnrollScript
|
|
2981
|
-
);
|
|
2964
|
+
const serverUnrollScript = CSVMultisigTapscript2.decode(rawCheckpointTapscript);
|
|
2965
|
+
const { arkTx, checkpoints } = buildOffchainTx([input], [output], serverUnrollScript);
|
|
2982
2966
|
const signedArkTx = await identity.sign(arkTx);
|
|
2983
2967
|
const { arkTxid, finalArkTx, signedCheckpointTxs } = await arkProvider.submitTx(
|
|
2984
2968
|
base643.encode(signedArkTx.toPSBT()),
|
|
@@ -2986,9 +2970,7 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
|
|
|
2986
2970
|
);
|
|
2987
2971
|
const finalTx = Transaction5.fromPSBT(base643.decode(finalArkTx));
|
|
2988
2972
|
const serverPubkeyHex = hex7.encode(serverXOnlyPublicKey);
|
|
2989
|
-
const claimLeafHash = tapLeafHash3(
|
|
2990
|
-
scriptFromTapLeafScript(vhtlcScript.claim())
|
|
2991
|
-
);
|
|
2973
|
+
const claimLeafHash = tapLeafHash3(scriptFromTapLeafScript(vhtlcScript.claim()));
|
|
2992
2974
|
for (let i = 0; i < finalTx.inputsLength; i++) {
|
|
2993
2975
|
if (!verifySignatures(finalTx, i, [serverPubkeyHex], claimLeafHash)) {
|
|
2994
2976
|
throw new Error("Invalid final Ark transaction");
|
|
@@ -2998,13 +2980,9 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
|
|
|
2998
2980
|
signedCheckpointTxs.map(async (c, idx) => {
|
|
2999
2981
|
const tx = Transaction5.fromPSBT(base643.decode(c));
|
|
3000
2982
|
const checkpointLeaf = checkpoints[idx].getInput(0).tapLeafScript[0];
|
|
3001
|
-
const cpLeafHash = tapLeafHash3(
|
|
3002
|
-
scriptFromTapLeafScript(checkpointLeaf)
|
|
3003
|
-
);
|
|
2983
|
+
const cpLeafHash = tapLeafHash3(scriptFromTapLeafScript(checkpointLeaf));
|
|
3004
2984
|
if (!verifySignatures(tx, 0, [serverPubkeyHex], cpLeafHash)) {
|
|
3005
|
-
throw new Error(
|
|
3006
|
-
"Invalid server signature in checkpoint transaction"
|
|
3007
|
-
);
|
|
2985
|
+
throw new Error("Invalid server signature in checkpoint transaction");
|
|
3008
2986
|
}
|
|
3009
2987
|
const signedCheckpoint = await identity.sign(tx, [0]);
|
|
3010
2988
|
return base643.encode(signedCheckpoint.toPSBT());
|
|
@@ -3014,61 +2992,37 @@ var claimVHTLCwithOffchainTx = async (identity, vhtlcScript, serverXOnlyPublicKe
|
|
|
3014
2992
|
};
|
|
3015
2993
|
var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnlyPublicKey, ourXOnlyPublicKey, serverXOnlyPublicKey, input, output, arkInfo, refundFunc) => {
|
|
3016
2994
|
const rawCheckpointTapscript = hex7.decode(arkInfo.checkpointTapscript);
|
|
3017
|
-
const serverUnrollScript = CSVMultisigTapscript2.decode(
|
|
3018
|
-
|
|
2995
|
+
const serverUnrollScript = CSVMultisigTapscript2.decode(rawCheckpointTapscript);
|
|
2996
|
+
const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = buildOffchainTx(
|
|
2997
|
+
[input],
|
|
2998
|
+
[output],
|
|
2999
|
+
serverUnrollScript
|
|
3019
3000
|
);
|
|
3020
|
-
const { arkTx: unsignedRefundTx, checkpoints: checkpointPtxs } = buildOffchainTx([input], [output], serverUnrollScript);
|
|
3021
3001
|
if (checkpointPtxs.length !== 1)
|
|
3022
|
-
throw new Error(
|
|
3023
|
-
`Expected one checkpoint transaction, got ${checkpointPtxs.length}`
|
|
3024
|
-
);
|
|
3002
|
+
throw new Error(`Expected one checkpoint transaction, got ${checkpointPtxs.length}`);
|
|
3025
3003
|
const unsignedCheckpointTx = checkpointPtxs[0];
|
|
3026
3004
|
let boltzSignedRefundTx;
|
|
3027
3005
|
let boltzSignedCheckpointTx;
|
|
3028
3006
|
try {
|
|
3029
|
-
const result = await refundFunc(
|
|
3030
|
-
swapId,
|
|
3031
|
-
unsignedRefundTx,
|
|
3032
|
-
unsignedCheckpointTx
|
|
3033
|
-
);
|
|
3007
|
+
const result = await refundFunc(swapId, unsignedRefundTx, unsignedCheckpointTx);
|
|
3034
3008
|
boltzSignedRefundTx = result.transaction;
|
|
3035
3009
|
boltzSignedCheckpointTx = result.checkpoint;
|
|
3036
3010
|
} catch (error) {
|
|
3037
|
-
throw new BoltzRefundError(
|
|
3038
|
-
`Boltz rejected refund for swap ${swapId}`,
|
|
3039
|
-
error
|
|
3040
|
-
);
|
|
3011
|
+
throw new BoltzRefundError(`Boltz rejected refund for swap ${swapId}`, error);
|
|
3041
3012
|
}
|
|
3042
3013
|
const boltzXOnlyPublicKeyHex = hex7.encode(boltzXOnlyPublicKey);
|
|
3043
|
-
const refundLeafHash = tapLeafHash3(
|
|
3044
|
-
|
|
3045
|
-
);
|
|
3046
|
-
if (!verifySignatures(
|
|
3047
|
-
boltzSignedRefundTx,
|
|
3048
|
-
0,
|
|
3049
|
-
[boltzXOnlyPublicKeyHex],
|
|
3050
|
-
refundLeafHash
|
|
3051
|
-
)) {
|
|
3014
|
+
const refundLeafHash = tapLeafHash3(scriptFromTapLeafScript(input.tapLeafScript));
|
|
3015
|
+
if (!verifySignatures(boltzSignedRefundTx, 0, [boltzXOnlyPublicKeyHex], refundLeafHash)) {
|
|
3052
3016
|
throw new Error("Invalid Boltz signature in refund transaction");
|
|
3053
3017
|
}
|
|
3054
3018
|
const checkpointLeaf = unsignedCheckpointTx.getInput(0).tapLeafScript[0];
|
|
3055
|
-
const checkpointLeafHash = tapLeafHash3(
|
|
3056
|
-
|
|
3057
|
-
);
|
|
3058
|
-
if (!verifySignatures(
|
|
3059
|
-
boltzSignedCheckpointTx,
|
|
3060
|
-
0,
|
|
3061
|
-
[boltzXOnlyPublicKeyHex],
|
|
3062
|
-
checkpointLeafHash
|
|
3063
|
-
)) {
|
|
3019
|
+
const checkpointLeafHash = tapLeafHash3(scriptFromTapLeafScript(checkpointLeaf));
|
|
3020
|
+
if (!verifySignatures(boltzSignedCheckpointTx, 0, [boltzXOnlyPublicKeyHex], checkpointLeafHash)) {
|
|
3064
3021
|
throw new Error("Invalid Boltz signature in checkpoint transaction");
|
|
3065
3022
|
}
|
|
3066
3023
|
const signedRefundTx = await identity.sign(unsignedRefundTx);
|
|
3067
3024
|
const signedCheckpointTx = await identity.sign(unsignedCheckpointTx);
|
|
3068
|
-
const combinedSignedRefundTx = combineTapscriptSigs(
|
|
3069
|
-
boltzSignedRefundTx,
|
|
3070
|
-
signedRefundTx
|
|
3071
|
-
);
|
|
3025
|
+
const combinedSignedRefundTx = combineTapscriptSigs(boltzSignedRefundTx, signedRefundTx);
|
|
3072
3026
|
const combinedSignedCheckpointTx = combineTapscriptSigs(
|
|
3073
3027
|
boltzSignedCheckpointTx,
|
|
3074
3028
|
signedCheckpointTx
|
|
@@ -3092,25 +3046,16 @@ var refundVHTLCwithOffchainTx = async (swapId, identity, arkProvider, boltzXOnly
|
|
|
3092
3046
|
`Expected one signed checkpoint transaction, got ${signedCheckpointTxs.length}`
|
|
3093
3047
|
);
|
|
3094
3048
|
}
|
|
3095
|
-
const serverSignedCheckpointTx = Transaction5.fromPSBT(
|
|
3096
|
-
base643.decode(signedCheckpointTxs[0])
|
|
3097
|
-
);
|
|
3049
|
+
const serverSignedCheckpointTx = Transaction5.fromPSBT(base643.decode(signedCheckpointTxs[0]));
|
|
3098
3050
|
const serverPubkeyHex = hex7.encode(serverXOnlyPublicKey);
|
|
3099
|
-
if (!verifySignatures(
|
|
3100
|
-
serverSignedCheckpointTx,
|
|
3101
|
-
0,
|
|
3102
|
-
[serverPubkeyHex],
|
|
3103
|
-
checkpointLeafHash
|
|
3104
|
-
)) {
|
|
3051
|
+
if (!verifySignatures(serverSignedCheckpointTx, 0, [serverPubkeyHex], checkpointLeafHash)) {
|
|
3105
3052
|
throw new Error("Invalid server signature in checkpoint transaction");
|
|
3106
3053
|
}
|
|
3107
3054
|
const finalCheckpointTx = combineTapscriptSigs(
|
|
3108
3055
|
combinedSignedCheckpointTx,
|
|
3109
3056
|
serverSignedCheckpointTx
|
|
3110
3057
|
);
|
|
3111
|
-
await arkProvider.finalizeTx(arkTxid, [
|
|
3112
|
-
base643.encode(finalCheckpointTx.toPSBT())
|
|
3113
|
-
]);
|
|
3058
|
+
await arkProvider.finalizeTx(arkTxid, [base643.encode(finalCheckpointTx.toPSBT())]);
|
|
3114
3059
|
};
|
|
3115
3060
|
function scriptFromTapLeafScript(leaf) {
|
|
3116
3061
|
return leaf[1].subarray(0, leaf[1].length - 1);
|
|
@@ -3118,21 +3063,21 @@ function scriptFromTapLeafScript(leaf) {
|
|
|
3118
3063
|
|
|
3119
3064
|
// src/arkade-swaps.ts
|
|
3120
3065
|
var dedupeVtxos = (vtxos) => [
|
|
3121
|
-
...new Map(
|
|
3122
|
-
vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])
|
|
3123
|
-
).values()
|
|
3066
|
+
...new Map(vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])).values()
|
|
3124
3067
|
];
|
|
3125
3068
|
var hasNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
3126
3069
|
var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
|
|
3127
3070
|
const hasRequiredSwapMetadata = hasNonEmptyString(swap.id) && hasNonEmptyString(swap.request.refundPublicKey) && hasNonEmptyString(swap.response.address) && hasNonEmptyString(swap.response.claimPublicKey) && !!swap.response.timeoutBlockHeights;
|
|
3128
3071
|
if (!hasRequiredSwapMetadata) return false;
|
|
3129
|
-
return refundableVtxos.some(
|
|
3130
|
-
(vtxo) => !vtxo.isSpent && !isRecoverable(vtxo)
|
|
3131
|
-
);
|
|
3072
|
+
return refundableVtxos.some((vtxo) => !vtxo.isSpent && !isRecoverable(vtxo));
|
|
3132
3073
|
};
|
|
3133
3074
|
var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
|
|
3134
3075
|
var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
|
|
3135
3076
|
var CLAIM_VTXO_RETRY_DELAY_MS = 500;
|
|
3077
|
+
var quoteOptionsForSwap = (swap) => {
|
|
3078
|
+
const amount = swap.response?.claimDetails?.amount;
|
|
3079
|
+
return typeof amount === "number" ? { minAcceptableAmount: amount } : void 0;
|
|
3080
|
+
};
|
|
3136
3081
|
var ArkadeSwaps = class _ArkadeSwaps {
|
|
3137
3082
|
/** The Arkade wallet instance used for signing and address generation. */
|
|
3138
3083
|
wallet;
|
|
@@ -3168,10 +3113,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3168
3113
|
return new _ArkadeSwaps(config);
|
|
3169
3114
|
}
|
|
3170
3115
|
const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
|
|
3171
|
-
if (!arkProvider)
|
|
3172
|
-
throw new Error(
|
|
3173
|
-
"Ark provider is required either in wallet or config."
|
|
3174
|
-
);
|
|
3116
|
+
if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
|
|
3175
3117
|
const arkInfo = await arkProvider.getInfo();
|
|
3176
3118
|
const network = arkInfo.network;
|
|
3177
3119
|
const swapProvider = new BoltzSwapProvider({ network });
|
|
@@ -3182,16 +3124,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3182
3124
|
if (!config.swapProvider) throw new Error("Swap provider is required.");
|
|
3183
3125
|
this.wallet = config.wallet;
|
|
3184
3126
|
const arkProvider = config.arkProvider ?? config.wallet.arkProvider;
|
|
3185
|
-
if (!arkProvider)
|
|
3186
|
-
throw new Error(
|
|
3187
|
-
"Ark provider is required either in wallet or config."
|
|
3188
|
-
);
|
|
3127
|
+
if (!arkProvider) throw new Error("Ark provider is required either in wallet or config.");
|
|
3189
3128
|
this.arkProvider = arkProvider;
|
|
3190
3129
|
const indexerProvider = config.indexerProvider ?? config.wallet.indexerProvider;
|
|
3191
3130
|
if (!indexerProvider)
|
|
3192
|
-
throw new Error(
|
|
3193
|
-
"Indexer provider is required either in wallet or config."
|
|
3194
|
-
);
|
|
3131
|
+
throw new Error("Indexer provider is required either in wallet or config.");
|
|
3195
3132
|
this.indexerProvider = indexerProvider;
|
|
3196
3133
|
this.swapProvider = config.swapProvider;
|
|
3197
3134
|
if (config.swapRepository) {
|
|
@@ -3202,10 +3139,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3202
3139
|
if (config.swapManager !== false) {
|
|
3203
3140
|
const swapManagerConfig = !config.swapManager || config.swapManager === true ? {} : config.swapManager;
|
|
3204
3141
|
const shouldAutostart = swapManagerConfig.autoStart ?? true;
|
|
3205
|
-
this.swapManager = new SwapManager(
|
|
3206
|
-
this.swapProvider,
|
|
3207
|
-
swapManagerConfig
|
|
3208
|
-
);
|
|
3142
|
+
this.swapManager = new SwapManager(this.swapProvider, swapManagerConfig);
|
|
3209
3143
|
this.swapManager.setCallbacks({
|
|
3210
3144
|
claim: async (swap) => {
|
|
3211
3145
|
await this.claimVHTLC(swap);
|
|
@@ -3220,7 +3154,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3220
3154
|
await this.claimBtc(swap);
|
|
3221
3155
|
},
|
|
3222
3156
|
refundArk: async (swap) => {
|
|
3223
|
-
|
|
3157
|
+
return this.refundArk(swap);
|
|
3224
3158
|
},
|
|
3225
3159
|
signServerClaim: async (swap) => {
|
|
3226
3160
|
await this.signCooperativeClaimForServer(swap);
|
|
@@ -3353,19 +3287,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3353
3287
|
* @throws {SwapError} If amount is <= 0 or key retrieval fails.
|
|
3354
3288
|
*/
|
|
3355
3289
|
async createReverseSwap(args) {
|
|
3356
|
-
if (args.amount <= 0)
|
|
3357
|
-
|
|
3358
|
-
const claimPublicKey = hex8.encode(
|
|
3359
|
-
await this.wallet.identity.compressedPublicKey()
|
|
3360
|
-
);
|
|
3290
|
+
if (args.amount <= 0) throw new SwapError({ message: "Amount must be greater than 0" });
|
|
3291
|
+
const claimPublicKey = hex8.encode(await this.wallet.identity.compressedPublicKey());
|
|
3361
3292
|
if (!claimPublicKey)
|
|
3362
3293
|
throw new SwapError({
|
|
3363
3294
|
message: "Failed to get claim public key from wallet"
|
|
3364
3295
|
});
|
|
3365
3296
|
const preimage = randomBytes(32);
|
|
3366
3297
|
const preimageHash = hex8.encode(sha2563(preimage));
|
|
3367
|
-
if (!preimageHash)
|
|
3368
|
-
throw new SwapError({ message: "Failed to get preimage hash" });
|
|
3298
|
+
if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
|
|
3369
3299
|
const swapRequest = {
|
|
3370
3300
|
invoiceAmount: args.amount,
|
|
3371
3301
|
claimPublicKey,
|
|
@@ -3395,18 +3325,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3395
3325
|
*/
|
|
3396
3326
|
async claimVHTLC(pendingSwap) {
|
|
3397
3327
|
if (!pendingSwap.preimage)
|
|
3398
|
-
throw new Error(
|
|
3399
|
-
`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
|
|
3400
|
-
);
|
|
3328
|
+
throw new Error(`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`);
|
|
3401
3329
|
const {
|
|
3402
3330
|
refundPublicKey,
|
|
3403
3331
|
lockupAddress,
|
|
3404
3332
|
timeoutBlockHeights: vhtlcTimeouts
|
|
3405
3333
|
} = pendingSwap.response;
|
|
3406
3334
|
if (!refundPublicKey || !lockupAddress || !vhtlcTimeouts)
|
|
3407
|
-
throw new Error(
|
|
3408
|
-
`Swap ${pendingSwap.id}: incomplete reverse swap response`
|
|
3409
|
-
);
|
|
3335
|
+
throw new Error(`Swap ${pendingSwap.id}: incomplete reverse swap response`);
|
|
3410
3336
|
const preimage = hex8.decode(pendingSwap.preimage);
|
|
3411
3337
|
const arkInfo = await this.arkProvider.getInfo();
|
|
3412
3338
|
const address = await this.wallet.getAddress();
|
|
@@ -3441,58 +3367,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3441
3367
|
throw new Error(
|
|
3442
3368
|
`Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
|
|
3443
3369
|
);
|
|
3444
|
-
let
|
|
3370
|
+
let unspentVtxos = [];
|
|
3371
|
+
let rawVtxos = [];
|
|
3445
3372
|
for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
|
|
3446
|
-
const
|
|
3373
|
+
const result = await this.indexerProvider.getVtxos({
|
|
3447
3374
|
scripts: [hex8.encode(vhtlcScript.pkScript)]
|
|
3448
3375
|
});
|
|
3449
|
-
|
|
3450
|
-
|
|
3376
|
+
rawVtxos = result.vtxos;
|
|
3377
|
+
unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
3378
|
+
if (unspentVtxos.length > 0) {
|
|
3451
3379
|
break;
|
|
3452
3380
|
}
|
|
3453
3381
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
3454
|
-
await new Promise(
|
|
3455
|
-
(resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
|
|
3456
|
-
);
|
|
3382
|
+
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
3457
3383
|
}
|
|
3458
3384
|
}
|
|
3459
|
-
if (
|
|
3460
|
-
|
|
3461
|
-
`Swap ${pendingSwap.id}: no spendable virtual coins found`
|
|
3462
|
-
|
|
3463
|
-
}
|
|
3464
|
-
if (vtxo.isSpent) {
|
|
3385
|
+
if (unspentVtxos.length === 0) {
|
|
3386
|
+
if (rawVtxos.length === 0) {
|
|
3387
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
3388
|
+
}
|
|
3465
3389
|
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3466
3390
|
}
|
|
3467
|
-
const
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3391
|
+
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
3392
|
+
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
3393
|
+
const claimErrors = [];
|
|
3394
|
+
let usedOffchainClaim = false;
|
|
3395
|
+
for (const vtxo of unspentVtxos) {
|
|
3396
|
+
const input = {
|
|
3397
|
+
...vtxo,
|
|
3398
|
+
tapLeafScript: vhtlcScript.claim(),
|
|
3399
|
+
tapTree: vhtlcScript.encode()
|
|
3400
|
+
};
|
|
3401
|
+
const output = {
|
|
3402
|
+
amount: BigInt(vtxo.value),
|
|
3403
|
+
script: outputScript
|
|
3404
|
+
};
|
|
3405
|
+
try {
|
|
3406
|
+
if (isRecoverable(vtxo)) {
|
|
3407
|
+
await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
|
|
3408
|
+
} else {
|
|
3409
|
+
await claimVHTLCwithOffchainTx(
|
|
3410
|
+
vhtlcIdentity,
|
|
3411
|
+
vhtlcScript,
|
|
3412
|
+
serverXOnly,
|
|
3413
|
+
input,
|
|
3414
|
+
output,
|
|
3415
|
+
arkInfo,
|
|
3416
|
+
this.arkProvider
|
|
3417
|
+
);
|
|
3418
|
+
usedOffchainClaim = true;
|
|
3419
|
+
}
|
|
3420
|
+
} catch (error) {
|
|
3421
|
+
claimErrors.push({ vtxo, error });
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
if (claimErrors.length > 0) {
|
|
3425
|
+
const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
|
|
3426
|
+
throw new Error(
|
|
3427
|
+
`Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
|
|
3493
3428
|
);
|
|
3494
|
-
finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
|
|
3495
3429
|
}
|
|
3430
|
+
const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
|
|
3496
3431
|
await updateReverseSwapStatus(
|
|
3497
3432
|
pendingSwap,
|
|
3498
3433
|
finalStatus,
|
|
@@ -3595,9 +3530,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3595
3530
|
async sendLightningPayment(args) {
|
|
3596
3531
|
const pendingSwap = await this.createSubmarineSwap(args);
|
|
3597
3532
|
if (!pendingSwap.response.address)
|
|
3598
|
-
throw new Error(
|
|
3599
|
-
`Swap ${pendingSwap.id}: missing address in submarine swap response`
|
|
3600
|
-
);
|
|
3533
|
+
throw new Error(`Swap ${pendingSwap.id}: missing address in submarine swap response`);
|
|
3601
3534
|
await this.savePendingSubmarineSwap(pendingSwap);
|
|
3602
3535
|
const txid = await this.wallet.send({
|
|
3603
3536
|
address: pendingSwap.response.address,
|
|
@@ -3630,9 +3563,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3630
3563
|
* @throws {SwapError} If invoice is missing or key retrieval fails.
|
|
3631
3564
|
*/
|
|
3632
3565
|
async createSubmarineSwap(args) {
|
|
3633
|
-
const refundPublicKey = hex8.encode(
|
|
3634
|
-
await this.wallet.identity.compressedPublicKey()
|
|
3635
|
-
);
|
|
3566
|
+
const refundPublicKey = hex8.encode(await this.wallet.identity.compressedPublicKey());
|
|
3636
3567
|
if (!refundPublicKey)
|
|
3637
3568
|
throw new SwapError({
|
|
3638
3569
|
message: "Failed to get refund public key from wallet"
|
|
@@ -3670,9 +3601,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3670
3601
|
async buildSubmarineVHTLCContext(swap, arkInfo) {
|
|
3671
3602
|
const preimageHash = swap.request.invoice ? getInvoicePaymentHash(swap.request.invoice) : swap.preimageHash;
|
|
3672
3603
|
if (!preimageHash)
|
|
3673
|
-
throw new Error(
|
|
3674
|
-
`Swap ${swap.id}: preimage hash is required to refund VHTLC`
|
|
3675
|
-
);
|
|
3604
|
+
throw new Error(`Swap ${swap.id}: preimage hash is required to refund VHTLC`);
|
|
3676
3605
|
const resolvedArkInfo = arkInfo ?? await this.arkProvider.getInfo();
|
|
3677
3606
|
const ourXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3678
3607
|
await this.wallet.identity.xOnlyPublicKey(),
|
|
@@ -3686,9 +3615,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3686
3615
|
);
|
|
3687
3616
|
const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
|
|
3688
3617
|
if (!claimPublicKey || !vhtlcTimeouts)
|
|
3689
|
-
throw new Error(
|
|
3690
|
-
`Swap ${swap.id}: incomplete submarine swap response`
|
|
3691
|
-
);
|
|
3618
|
+
throw new Error(`Swap ${swap.id}: incomplete submarine swap response`);
|
|
3692
3619
|
const boltzXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3693
3620
|
hex8.decode(claimPublicKey),
|
|
3694
3621
|
"boltz",
|
|
@@ -3703,9 +3630,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3703
3630
|
timeoutBlockHeights: vhtlcTimeouts
|
|
3704
3631
|
});
|
|
3705
3632
|
if (!vhtlcScript.claimScript)
|
|
3706
|
-
throw new Error(
|
|
3707
|
-
`Swap ${swap.id}: failed to create VHTLC script for submarine swap`
|
|
3708
|
-
);
|
|
3633
|
+
throw new Error(`Swap ${swap.id}: failed to create VHTLC script for submarine swap`);
|
|
3709
3634
|
if (vhtlcAddress !== swap.response.address)
|
|
3710
3635
|
throw new Error(
|
|
3711
3636
|
`VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
|
|
@@ -3744,10 +3669,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3744
3669
|
recoverableOnly: true
|
|
3745
3670
|
})
|
|
3746
3671
|
]);
|
|
3747
|
-
const refundableVtxos = dedupeVtxos([
|
|
3748
|
-
...spendableResult.vtxos,
|
|
3749
|
-
...recoverableResult.vtxos
|
|
3750
|
-
]);
|
|
3672
|
+
const refundableVtxos = dedupeVtxos([...spendableResult.vtxos, ...recoverableResult.vtxos]);
|
|
3751
3673
|
let diagnostic;
|
|
3752
3674
|
if (refundableVtxos.length === 0) {
|
|
3753
3675
|
const { vtxos: allVtxos } = await this.indexerProvider.getVtxos({
|
|
@@ -3767,13 +3689,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3767
3689
|
submarineRecoveryInfoFromLookup(swap, lookup) {
|
|
3768
3690
|
const { refundableVtxos, diagnostic, vhtlcTimeouts } = lookup;
|
|
3769
3691
|
if (refundableVtxos.length > 0) {
|
|
3770
|
-
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3771
|
-
|
|
3772
|
-
);
|
|
3773
|
-
const amountSats = refundableVtxos.reduce(
|
|
3774
|
-
(sum, vtxo) => sum + Number(vtxo.value),
|
|
3775
|
-
0
|
|
3776
|
-
);
|
|
3692
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
|
|
3693
|
+
const amountSats = refundableVtxos.reduce((sum, vtxo) => sum + Number(vtxo.value), 0);
|
|
3777
3694
|
const isRecoverable2 = cltvSatisfied || canRecoverViaBoltz3of3(refundableVtxos, swap);
|
|
3778
3695
|
return {
|
|
3779
3696
|
swap,
|
|
@@ -3837,19 +3754,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3837
3754
|
);
|
|
3838
3755
|
}
|
|
3839
3756
|
if (diagnostic.allSpent) {
|
|
3840
|
-
throw new Error(
|
|
3841
|
-
`Swap ${pendingSwap.id}: VHTLC is already spent`
|
|
3842
|
-
);
|
|
3757
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3843
3758
|
}
|
|
3844
|
-
throw new Error(
|
|
3845
|
-
`Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`
|
|
3846
|
-
);
|
|
3759
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC has no refundable VTXOs yet`);
|
|
3847
3760
|
}
|
|
3848
3761
|
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
3849
3762
|
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
3850
|
-
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3851
|
-
vhtlcTimeouts.refund
|
|
3852
|
-
);
|
|
3763
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund);
|
|
3853
3764
|
let boltzCallCount = 0;
|
|
3854
3765
|
let sweptCount = 0;
|
|
3855
3766
|
let skippedCount = 0;
|
|
@@ -3891,6 +3802,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3891
3802
|
if (boltzCallCount > 0) {
|
|
3892
3803
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
3893
3804
|
}
|
|
3805
|
+
boltzCallCount++;
|
|
3894
3806
|
await refundVHTLCwithOffchainTx(
|
|
3895
3807
|
pendingSwap.id,
|
|
3896
3808
|
this.wallet.identity,
|
|
@@ -3901,11 +3813,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3901
3813
|
input,
|
|
3902
3814
|
output,
|
|
3903
3815
|
arkInfo,
|
|
3904
|
-
this.swapProvider.refundSubmarineSwap.bind(
|
|
3905
|
-
this.swapProvider
|
|
3906
|
-
)
|
|
3816
|
+
this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
|
|
3907
3817
|
);
|
|
3908
|
-
boltzCallCount++;
|
|
3909
3818
|
sweptCount++;
|
|
3910
3819
|
} catch (error) {
|
|
3911
3820
|
if (!(error instanceof BoltzRefundError)) {
|
|
@@ -3926,13 +3835,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3926
3835
|
tapLeafScript: refundWithoutReceiverLeaf,
|
|
3927
3836
|
tapTree: vhtlcScript.encode()
|
|
3928
3837
|
};
|
|
3929
|
-
await this.joinBatch(
|
|
3930
|
-
this.wallet.identity,
|
|
3931
|
-
fallbackInput,
|
|
3932
|
-
output,
|
|
3933
|
-
arkInfo,
|
|
3934
|
-
false
|
|
3935
|
-
);
|
|
3838
|
+
await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
|
|
3936
3839
|
sweptCount++;
|
|
3937
3840
|
}
|
|
3938
3841
|
}
|
|
@@ -4028,10 +3931,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4028
3931
|
try {
|
|
4029
3932
|
return {
|
|
4030
3933
|
swap,
|
|
4031
|
-
context: await this.buildSubmarineVHTLCContext(
|
|
4032
|
-
swap,
|
|
4033
|
-
arkInfo
|
|
4034
|
-
)
|
|
3934
|
+
context: await this.buildSubmarineVHTLCContext(swap, arkInfo)
|
|
4035
3935
|
};
|
|
4036
3936
|
} catch (err) {
|
|
4037
3937
|
return {
|
|
@@ -4044,9 +3944,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4044
3944
|
const valid = prepared.filter(
|
|
4045
3945
|
(item) => "context" in item
|
|
4046
3946
|
);
|
|
4047
|
-
const scripts = [
|
|
4048
|
-
...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))
|
|
4049
|
-
];
|
|
3947
|
+
const scripts = [...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))];
|
|
4050
3948
|
const refundableByScript = /* @__PURE__ */ new Map();
|
|
4051
3949
|
if (scripts.length > 0) {
|
|
4052
3950
|
const [spendableResult, recoverableResult] = await Promise.all([
|
|
@@ -4081,9 +3979,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4081
3979
|
error: item.error
|
|
4082
3980
|
};
|
|
4083
3981
|
}
|
|
4084
|
-
const refundableVtxos = refundableByScript.get(
|
|
4085
|
-
item.context.vhtlcPkScriptHex.toLowerCase()
|
|
4086
|
-
) ?? [];
|
|
3982
|
+
const refundableVtxos = refundableByScript.get(item.context.vhtlcPkScriptHex.toLowerCase()) ?? [];
|
|
4087
3983
|
return this.submarineRecoveryInfoFromLookup(item.swap, {
|
|
4088
3984
|
...item.context,
|
|
4089
3985
|
refundableVtxos
|
|
@@ -4262,9 +4158,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4262
4158
|
*/
|
|
4263
4159
|
async waitAndClaimBtc(pendingSwap) {
|
|
4264
4160
|
if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
|
|
4265
|
-
const { txid } = await this.swapManager.waitForSwapCompletion(
|
|
4266
|
-
pendingSwap.id
|
|
4267
|
-
);
|
|
4161
|
+
const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
|
|
4268
4162
|
return { txid };
|
|
4269
4163
|
}
|
|
4270
4164
|
return new Promise((resolve, reject) => {
|
|
@@ -4291,24 +4185,25 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4291
4185
|
}
|
|
4292
4186
|
case "transaction.claimed":
|
|
4293
4187
|
await updateSwapStatus();
|
|
4294
|
-
const claimedStatus = await this.getSwapStatus(
|
|
4295
|
-
pendingSwap.id
|
|
4296
|
-
);
|
|
4188
|
+
const claimedStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4297
4189
|
resolve({
|
|
4298
4190
|
txid: claimedStatus.transaction?.id ?? ""
|
|
4299
4191
|
});
|
|
4300
4192
|
break;
|
|
4301
4193
|
case "transaction.lockupFailed":
|
|
4302
4194
|
await updateSwapStatus();
|
|
4303
|
-
await this.quoteSwap(swap.response.id).catch(
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4195
|
+
await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
|
|
4196
|
+
(err) => {
|
|
4197
|
+
reject(
|
|
4198
|
+
new SwapError({
|
|
4199
|
+
message: `Failed to renegotiate quote: ${err.message}`,
|
|
4200
|
+
isRefundable: true,
|
|
4201
|
+
pendingSwap: swap,
|
|
4202
|
+
cause: err
|
|
4203
|
+
})
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
);
|
|
4312
4207
|
break;
|
|
4313
4208
|
case "swap.expired":
|
|
4314
4209
|
await updateSwapStatus();
|
|
@@ -4346,30 +4241,18 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4346
4241
|
*/
|
|
4347
4242
|
async claimBtc(pendingSwap) {
|
|
4348
4243
|
if (!pendingSwap.toAddress)
|
|
4349
|
-
throw new Error(
|
|
4350
|
-
`Swap ${pendingSwap.id}: destination address is required`
|
|
4351
|
-
);
|
|
4244
|
+
throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
|
|
4352
4245
|
if (!pendingSwap.response.claimDetails.swapTree)
|
|
4353
|
-
throw new Error(
|
|
4354
|
-
`Swap ${pendingSwap.id}: missing swap tree in claim details`
|
|
4355
|
-
);
|
|
4246
|
+
throw new Error(`Swap ${pendingSwap.id}: missing swap tree in claim details`);
|
|
4356
4247
|
if (!pendingSwap.response.claimDetails.serverPublicKey)
|
|
4357
|
-
throw new Error(
|
|
4358
|
-
`Swap ${pendingSwap.id}: missing server public key in claim details`
|
|
4359
|
-
);
|
|
4248
|
+
throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
|
|
4360
4249
|
const swapStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4361
4250
|
if (!swapStatus.transaction?.hex)
|
|
4362
|
-
throw new Error(
|
|
4363
|
-
|
|
4364
|
-
);
|
|
4365
|
-
const lockupTx = Transaction6.fromRaw(
|
|
4366
|
-
hex8.decode(swapStatus.transaction.hex)
|
|
4367
|
-
);
|
|
4251
|
+
throw new Error(`Swap ${pendingSwap.id}: BTC transaction hex is required`);
|
|
4252
|
+
const lockupTx = Transaction6.fromRaw(hex8.decode(swapStatus.transaction.hex));
|
|
4368
4253
|
const arkInfo = await this.arkProvider.getInfo();
|
|
4369
4254
|
const network = arkInfo.network === "bitcoin" ? NETWORK : arkInfo.network === "mutinynet" ? MUTINYNET_NETWORK : REGTEST_NETWORK;
|
|
4370
|
-
const swapTree = deserializeSwapTree(
|
|
4371
|
-
pendingSwap.response.claimDetails.swapTree
|
|
4372
|
-
);
|
|
4255
|
+
const swapTree = deserializeSwapTree(pendingSwap.response.claimDetails.swapTree);
|
|
4373
4256
|
const musig = tweakMusig(
|
|
4374
4257
|
create(hex8.decode(pendingSwap.ephemeralKey), [
|
|
4375
4258
|
hex8.decode(pendingSwap.response.claimDetails.serverPublicKey),
|
|
@@ -4390,19 +4273,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4390
4273
|
vout: swapOutput.vout,
|
|
4391
4274
|
transactionId: lockupTx.id
|
|
4392
4275
|
},
|
|
4393
|
-
OutScript2.encode(
|
|
4394
|
-
Address2(network).decode(pendingSwap.toAddress)
|
|
4395
|
-
),
|
|
4276
|
+
OutScript2.encode(Address2(network).decode(pendingSwap.toAddress)),
|
|
4396
4277
|
feeToDeliverExactAmount > fee ? feeToDeliverExactAmount : fee
|
|
4397
4278
|
)
|
|
4398
4279
|
);
|
|
4399
4280
|
const musigMessage = musig.message(
|
|
4400
|
-
claimTx.preimageWitnessV1(
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
SigHash2.DEFAULT,
|
|
4404
|
-
[swapOutput.amount]
|
|
4405
|
-
)
|
|
4281
|
+
claimTx.preimageWitnessV1(0, [swapOutput.script], SigHash2.DEFAULT, [
|
|
4282
|
+
swapOutput.amount
|
|
4283
|
+
])
|
|
4406
4284
|
).generateNonce();
|
|
4407
4285
|
const signedTxData = await this.swapProvider.postChainClaimDetails(
|
|
4408
4286
|
pendingSwap.response.id,
|
|
@@ -4416,14 +4294,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4416
4294
|
}
|
|
4417
4295
|
);
|
|
4418
4296
|
if (!signedTxData.pubNonce || !signedTxData.partialSignature)
|
|
4419
|
-
throw new Error(
|
|
4420
|
-
`Swap ${pendingSwap.id}: invalid signature data from server`
|
|
4421
|
-
);
|
|
4297
|
+
throw new Error(`Swap ${pendingSwap.id}: invalid signature data from server`);
|
|
4422
4298
|
const musigSession = musigMessage.aggregateNonces([
|
|
4423
4299
|
[
|
|
4424
|
-
hex8.decode(
|
|
4425
|
-
pendingSwap.response.claimDetails.serverPublicKey
|
|
4426
|
-
),
|
|
4300
|
+
hex8.decode(pendingSwap.response.claimDetails.serverPublicKey),
|
|
4427
4301
|
hex8.decode(signedTxData.pubNonce)
|
|
4428
4302
|
]
|
|
4429
4303
|
]).initializeSession();
|
|
@@ -4438,18 +4312,23 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4438
4312
|
await this.swapProvider.postBtcTransaction(claimTx.hex);
|
|
4439
4313
|
}
|
|
4440
4314
|
/**
|
|
4441
|
-
* When an ARK to BTC swap fails, refund
|
|
4315
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
4316
|
+
* swap's ARK lockup address.
|
|
4317
|
+
*
|
|
4318
|
+
* Path selection per VTXO:
|
|
4319
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
4320
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
4321
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
4322
|
+
*
|
|
4442
4323
|
* @param pendingSwap - The pending chain swap to refund.
|
|
4324
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
4325
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
4443
4326
|
*/
|
|
4444
4327
|
async refundArk(pendingSwap) {
|
|
4445
4328
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
4446
|
-
throw new Error(
|
|
4447
|
-
`Swap ${pendingSwap.id}: missing server public key in lockup details`
|
|
4448
|
-
);
|
|
4329
|
+
throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
|
|
4449
4330
|
if (!pendingSwap.response.lockupDetails.timeouts)
|
|
4450
|
-
throw new Error(
|
|
4451
|
-
`Swap ${pendingSwap.id}: missing timeouts in lockup details`
|
|
4452
|
-
);
|
|
4331
|
+
throw new Error(`Swap ${pendingSwap.id}: missing timeouts in lockup details`);
|
|
4453
4332
|
const arkInfo = await this.arkProvider.getInfo();
|
|
4454
4333
|
const address = await this.wallet.getAddress();
|
|
4455
4334
|
const ourXOnlyPublicKey = normalizeToXOnlyKey(
|
|
@@ -4467,21 +4346,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4467
4346
|
"boltz",
|
|
4468
4347
|
pendingSwap.id
|
|
4469
4348
|
);
|
|
4470
|
-
const vhtlcPkScript = ArkAddress2.decode(
|
|
4471
|
-
pendingSwap.response.lockupDetails.lockupAddress
|
|
4472
|
-
).pkScript;
|
|
4473
|
-
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4474
|
-
scripts: [hex8.encode(vhtlcPkScript)]
|
|
4475
|
-
});
|
|
4476
|
-
if (vtxos.length === 0) {
|
|
4477
|
-
throw new Error(
|
|
4478
|
-
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4479
|
-
);
|
|
4480
|
-
}
|
|
4481
|
-
const vtxo = vtxos[0];
|
|
4482
|
-
if (vtxo.isSpent) {
|
|
4483
|
-
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4484
|
-
}
|
|
4485
4349
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4486
4350
|
network: arkInfo.network,
|
|
4487
4351
|
preimageHash: hex8.decode(pendingSwap.request.preimageHash),
|
|
@@ -4491,45 +4355,111 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4491
4355
|
timeoutBlockHeights: pendingSwap.response.lockupDetails.timeouts
|
|
4492
4356
|
});
|
|
4493
4357
|
if (!vhtlcScript.refundScript)
|
|
4494
|
-
throw new Error(
|
|
4495
|
-
`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
|
|
4496
|
-
);
|
|
4358
|
+
throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
|
|
4497
4359
|
if (pendingSwap.response.lockupDetails.lockupAddress !== vhtlcAddress) {
|
|
4498
4360
|
throw new SwapError({
|
|
4499
4361
|
message: "Unable to claim: invalid VHTLC address"
|
|
4500
4362
|
});
|
|
4501
4363
|
}
|
|
4502
|
-
const
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
const output = {
|
|
4509
|
-
amount: BigInt(vtxo.value),
|
|
4510
|
-
script: ArkAddress2.decode(address).pkScript
|
|
4511
|
-
};
|
|
4512
|
-
if (isRecoverableVtxo) {
|
|
4513
|
-
await this.joinBatch(this.wallet.identity, input, output, arkInfo);
|
|
4514
|
-
} else {
|
|
4515
|
-
await refundVHTLCwithOffchainTx(
|
|
4516
|
-
pendingSwap.id,
|
|
4517
|
-
this.wallet.identity,
|
|
4518
|
-
this.arkProvider,
|
|
4519
|
-
boltzXOnlyPublicKey,
|
|
4520
|
-
ourXOnlyPublicKey,
|
|
4521
|
-
serverXOnlyPublicKey,
|
|
4522
|
-
input,
|
|
4523
|
-
output,
|
|
4524
|
-
arkInfo,
|
|
4525
|
-
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4364
|
+
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4365
|
+
scripts: [hex8.encode(vhtlcScript.pkScript)]
|
|
4366
|
+
});
|
|
4367
|
+
if (vtxos.length === 0) {
|
|
4368
|
+
throw new Error(
|
|
4369
|
+
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4526
4370
|
);
|
|
4527
4371
|
}
|
|
4372
|
+
const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
4373
|
+
if (unspentVtxos.length === 0) {
|
|
4374
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4375
|
+
}
|
|
4376
|
+
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
4377
|
+
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
4378
|
+
const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
|
|
4379
|
+
let boltzCallCount = 0;
|
|
4380
|
+
let sweptCount = 0;
|
|
4381
|
+
let skippedCount = 0;
|
|
4382
|
+
for (const vtxo of unspentVtxos) {
|
|
4383
|
+
const isRecoverableVtxo = isRecoverable(vtxo);
|
|
4384
|
+
const output = {
|
|
4385
|
+
amount: BigInt(vtxo.value),
|
|
4386
|
+
script: outputScript
|
|
4387
|
+
};
|
|
4388
|
+
if (isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4389
|
+
const input2 = {
|
|
4390
|
+
...vtxo,
|
|
4391
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4392
|
+
tapTree: vhtlcScript.encode()
|
|
4393
|
+
};
|
|
4394
|
+
await this.joinBatch(
|
|
4395
|
+
this.wallet.identity,
|
|
4396
|
+
input2,
|
|
4397
|
+
output,
|
|
4398
|
+
arkInfo,
|
|
4399
|
+
isRecoverableVtxo
|
|
4400
|
+
);
|
|
4401
|
+
sweptCount++;
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
if (isRecoverableVtxo) {
|
|
4405
|
+
logger.error(
|
|
4406
|
+
`Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${refundLocktime}, currentTimestamp=${Math.floor(Date.now() / 1e3)}). Refund will be retried after locktime.`
|
|
4407
|
+
);
|
|
4408
|
+
skippedCount++;
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
const input = {
|
|
4412
|
+
...vtxo,
|
|
4413
|
+
tapLeafScript: vhtlcScript.refund(),
|
|
4414
|
+
tapTree: vhtlcScript.encode()
|
|
4415
|
+
};
|
|
4416
|
+
try {
|
|
4417
|
+
if (boltzCallCount > 0) {
|
|
4418
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
4419
|
+
}
|
|
4420
|
+
boltzCallCount++;
|
|
4421
|
+
await refundVHTLCwithOffchainTx(
|
|
4422
|
+
pendingSwap.id,
|
|
4423
|
+
this.wallet.identity,
|
|
4424
|
+
this.arkProvider,
|
|
4425
|
+
boltzXOnlyPublicKey,
|
|
4426
|
+
ourXOnlyPublicKey,
|
|
4427
|
+
serverXOnlyPublicKey,
|
|
4428
|
+
input,
|
|
4429
|
+
output,
|
|
4430
|
+
arkInfo,
|
|
4431
|
+
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4432
|
+
);
|
|
4433
|
+
sweptCount++;
|
|
4434
|
+
} catch (error) {
|
|
4435
|
+
if (!(error instanceof BoltzRefundError)) {
|
|
4436
|
+
throw error;
|
|
4437
|
+
}
|
|
4438
|
+
if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4439
|
+
logger.error(
|
|
4440
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (currentTimestamp=${Math.floor(Date.now() / 1e3)}, locktime=${refundLocktime}). Refund will be retried after locktime.`
|
|
4441
|
+
);
|
|
4442
|
+
skippedCount++;
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
logger.warn(
|
|
4446
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
|
|
4447
|
+
);
|
|
4448
|
+
const fallbackInput = {
|
|
4449
|
+
...vtxo,
|
|
4450
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4451
|
+
tapTree: vhtlcScript.encode()
|
|
4452
|
+
};
|
|
4453
|
+
await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
|
|
4454
|
+
sweptCount++;
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4528
4457
|
const finalStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4529
4458
|
await this.savePendingChainSwap({
|
|
4530
4459
|
...pendingSwap,
|
|
4531
4460
|
status: finalStatus.status
|
|
4532
4461
|
});
|
|
4462
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
4533
4463
|
}
|
|
4534
4464
|
// =========================================================================
|
|
4535
4465
|
// Chain swaps: BTC -> ARK
|
|
@@ -4574,9 +4504,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4574
4504
|
*/
|
|
4575
4505
|
async waitAndClaimArk(pendingSwap) {
|
|
4576
4506
|
if (this.swapManager && await this.swapManager.hasSwap(pendingSwap.id)) {
|
|
4577
|
-
const { txid } = await this.swapManager.waitForSwapCompletion(
|
|
4578
|
-
pendingSwap.id
|
|
4579
|
-
);
|
|
4507
|
+
const { txid } = await this.swapManager.waitForSwapCompletion(pendingSwap.id);
|
|
4580
4508
|
return { txid };
|
|
4581
4509
|
}
|
|
4582
4510
|
return new Promise((resolve, reject) => {
|
|
@@ -4597,37 +4525,33 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4597
4525
|
break;
|
|
4598
4526
|
case "transaction.claimed":
|
|
4599
4527
|
await updateSwapStatus();
|
|
4600
|
-
const claimedStatus = await this.getSwapStatus(
|
|
4601
|
-
pendingSwap.id
|
|
4602
|
-
);
|
|
4528
|
+
const claimedStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4603
4529
|
resolve({
|
|
4604
4530
|
txid: claimedStatus.transaction?.id ?? ""
|
|
4605
4531
|
});
|
|
4606
4532
|
break;
|
|
4607
4533
|
case "transaction.claim.pending":
|
|
4608
4534
|
await updateSwapStatus();
|
|
4609
|
-
await this.signCooperativeClaimForServer(swap).catch(
|
|
4535
|
+
await this.signCooperativeClaimForServer(swap).catch((err) => {
|
|
4536
|
+
logger.error(`Failed to sign cooperative claim for ${swap.id}:`, err);
|
|
4537
|
+
});
|
|
4538
|
+
break;
|
|
4539
|
+
case "transaction.lockupFailed":
|
|
4540
|
+
await updateSwapStatus();
|
|
4541
|
+
await this.quoteSwap(swap.response.id, quoteOptionsForSwap(swap)).catch(
|
|
4610
4542
|
(err) => {
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4543
|
+
reject(
|
|
4544
|
+
new SwapError({
|
|
4545
|
+
message: `Failed to renegotiate quote: ${err.message}`,
|
|
4546
|
+
isRefundable: false,
|
|
4547
|
+
// TODO btc refund not implemented yet
|
|
4548
|
+
pendingSwap: swap,
|
|
4549
|
+
cause: err
|
|
4550
|
+
})
|
|
4614
4551
|
);
|
|
4615
4552
|
}
|
|
4616
4553
|
);
|
|
4617
4554
|
break;
|
|
4618
|
-
case "transaction.lockupFailed":
|
|
4619
|
-
await updateSwapStatus();
|
|
4620
|
-
await this.quoteSwap(swap.response.id).catch((err) => {
|
|
4621
|
-
reject(
|
|
4622
|
-
new SwapError({
|
|
4623
|
-
message: `Failed to renegotiate quote: ${err.message}`,
|
|
4624
|
-
isRefundable: false,
|
|
4625
|
-
// TODO btc refund not implemented yet
|
|
4626
|
-
pendingSwap: swap
|
|
4627
|
-
})
|
|
4628
|
-
);
|
|
4629
|
-
});
|
|
4630
|
-
break;
|
|
4631
4555
|
case "swap.expired":
|
|
4632
4556
|
await updateSwapStatus();
|
|
4633
4557
|
reject(
|
|
@@ -4667,17 +4591,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4667
4591
|
*/
|
|
4668
4592
|
async claimArk(pendingSwap) {
|
|
4669
4593
|
if (!pendingSwap.toAddress)
|
|
4670
|
-
throw new Error(
|
|
4671
|
-
`Swap ${pendingSwap.id}: destination address is required`
|
|
4672
|
-
);
|
|
4594
|
+
throw new Error(`Swap ${pendingSwap.id}: destination address is required`);
|
|
4673
4595
|
if (!pendingSwap.response.claimDetails.serverPublicKey)
|
|
4674
|
-
throw new Error(
|
|
4675
|
-
`Swap ${pendingSwap.id}: missing server public key in claim details`
|
|
4676
|
-
);
|
|
4596
|
+
throw new Error(`Swap ${pendingSwap.id}: missing server public key in claim details`);
|
|
4677
4597
|
if (!pendingSwap.response.claimDetails.timeouts)
|
|
4678
|
-
throw new Error(
|
|
4679
|
-
`Swap ${pendingSwap.id}: missing timeouts in claim details`
|
|
4680
|
-
);
|
|
4598
|
+
throw new Error(`Swap ${pendingSwap.id}: missing timeouts in claim details`);
|
|
4681
4599
|
const arkInfo = await this.arkProvider.getInfo();
|
|
4682
4600
|
const preimage = hex8.decode(pendingSwap.preimage);
|
|
4683
4601
|
const address = await this.wallet.getAddress();
|
|
@@ -4689,10 +4607,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4689
4607
|
pendingSwap.response.claimDetails.serverPublicKey,
|
|
4690
4608
|
"sender"
|
|
4691
4609
|
);
|
|
4692
|
-
const serverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
4693
|
-
arkInfo.signerPubkey,
|
|
4694
|
-
"server"
|
|
4695
|
-
);
|
|
4610
|
+
const serverXOnlyPublicKey = normalizeToXOnlyKey(arkInfo.signerPubkey, "server");
|
|
4696
4611
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4697
4612
|
network: arkInfo.network,
|
|
4698
4613
|
preimageHash: hex8.decode(pendingSwap.request.preimageHash),
|
|
@@ -4702,9 +4617,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4702
4617
|
timeoutBlockHeights: pendingSwap.response.claimDetails.timeouts
|
|
4703
4618
|
});
|
|
4704
4619
|
if (!vhtlcScript.claimScript)
|
|
4705
|
-
throw new Error(
|
|
4706
|
-
`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`
|
|
4707
|
-
);
|
|
4620
|
+
throw new Error(`Swap ${pendingSwap.id}: failed to create VHTLC script for chain swap`);
|
|
4708
4621
|
if (pendingSwap.response.claimDetails.lockupAddress !== vhtlcAddress) {
|
|
4709
4622
|
throw new SwapError({
|
|
4710
4623
|
message: "Unable to claim: invalid VHTLC address"
|
|
@@ -4721,15 +4634,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4721
4634
|
break;
|
|
4722
4635
|
}
|
|
4723
4636
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
4724
|
-
await new Promise(
|
|
4725
|
-
(resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS)
|
|
4726
|
-
);
|
|
4637
|
+
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
4727
4638
|
}
|
|
4728
4639
|
}
|
|
4729
4640
|
if (!vtxo) {
|
|
4730
|
-
throw new Error(
|
|
4731
|
-
`Swap ${pendingSwap.id}: no spendable virtual coins found`
|
|
4732
|
-
);
|
|
4641
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
4733
4642
|
}
|
|
4734
4643
|
const input = {
|
|
4735
4644
|
...vtxo,
|
|
@@ -4740,10 +4649,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4740
4649
|
amount: BigInt(vtxo.value),
|
|
4741
4650
|
script: ArkAddress2.decode(address).pkScript
|
|
4742
4651
|
};
|
|
4743
|
-
const vhtlcIdentity = claimVHTLCIdentity(
|
|
4744
|
-
this.wallet.identity,
|
|
4745
|
-
preimage
|
|
4746
|
-
);
|
|
4652
|
+
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
4747
4653
|
if (isRecoverable(vtxo)) {
|
|
4748
4654
|
await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
|
|
4749
4655
|
} else {
|
|
@@ -4769,16 +4675,10 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4769
4675
|
*/
|
|
4770
4676
|
async signCooperativeClaimForServer(pendingSwap) {
|
|
4771
4677
|
if (!pendingSwap.response.lockupDetails.swapTree)
|
|
4772
|
-
throw new Error(
|
|
4773
|
-
`Swap ${pendingSwap.id}: missing swap tree in lockup details`
|
|
4774
|
-
);
|
|
4678
|
+
throw new Error(`Swap ${pendingSwap.id}: missing swap tree in lockup details`);
|
|
4775
4679
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
4776
|
-
throw new Error(
|
|
4777
|
-
|
|
4778
|
-
);
|
|
4779
|
-
const claimDetails = await this.swapProvider.getChainClaimDetails(
|
|
4780
|
-
pendingSwap.id
|
|
4781
|
-
);
|
|
4680
|
+
throw new Error(`Swap ${pendingSwap.id}: missing server public key in lockup details`);
|
|
4681
|
+
const claimDetails = await this.swapProvider.getChainClaimDetails(pendingSwap.id);
|
|
4782
4682
|
const serverPubKey = pendingSwap.response.lockupDetails.serverPublicKey;
|
|
4783
4683
|
if (claimDetails.publicKey !== serverPubKey) {
|
|
4784
4684
|
throw new Error(
|
|
@@ -4794,9 +4694,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4794
4694
|
);
|
|
4795
4695
|
const musigNonces = musig.message(hex8.decode(claimDetails.transactionHash)).generateNonce().aggregateNonces([
|
|
4796
4696
|
[
|
|
4797
|
-
hex8.decode(
|
|
4798
|
-
pendingSwap.response.lockupDetails.serverPublicKey
|
|
4799
|
-
),
|
|
4697
|
+
hex8.decode(pendingSwap.response.lockupDetails.serverPublicKey),
|
|
4800
4698
|
hex8.decode(claimDetails.pubNonce)
|
|
4801
4699
|
]
|
|
4802
4700
|
]).initializeSession();
|
|
@@ -4815,10 +4713,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4815
4713
|
* @returns The transaction ID of the claim.
|
|
4816
4714
|
*/
|
|
4817
4715
|
async waitAndClaimChain(pendingSwap) {
|
|
4818
|
-
if (pendingSwap.request.to === "ARK")
|
|
4819
|
-
|
|
4820
|
-
if (pendingSwap.request.to === "BTC")
|
|
4821
|
-
return this.waitAndClaimBtc(pendingSwap);
|
|
4716
|
+
if (pendingSwap.request.to === "ARK") return this.waitAndClaimArk(pendingSwap);
|
|
4717
|
+
if (pendingSwap.request.to === "BTC") return this.waitAndClaimBtc(pendingSwap);
|
|
4822
4718
|
throw new SwapError({
|
|
4823
4719
|
message: `Unsupported swap destination: ${pendingSwap.request.to}`
|
|
4824
4720
|
});
|
|
@@ -4833,11 +4729,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4833
4729
|
*/
|
|
4834
4730
|
async createChainSwap(args) {
|
|
4835
4731
|
const { to, from, receiverLockAmount, senderLockAmount, toAddress } = args;
|
|
4836
|
-
if (!toAddress)
|
|
4837
|
-
throw new SwapError({ message: "Destination address is required" });
|
|
4732
|
+
if (!toAddress) throw new SwapError({ message: "Destination address is required" });
|
|
4838
4733
|
const feeSatsPerByte = args.feeSatsPerByte ?? 1;
|
|
4839
|
-
if (feeSatsPerByte <= 0)
|
|
4840
|
-
throw new SwapError({ message: "Invalid feeSatsPerByte" });
|
|
4734
|
+
if (feeSatsPerByte <= 0) throw new SwapError({ message: "Invalid feeSatsPerByte" });
|
|
4841
4735
|
let amount, serverLockAmount, userLockAmount;
|
|
4842
4736
|
if (receiverLockAmount) {
|
|
4843
4737
|
amount = receiverLockAmount;
|
|
@@ -4852,8 +4746,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4852
4746
|
}
|
|
4853
4747
|
const preimage = randomBytes(32);
|
|
4854
4748
|
const preimageHash = hex8.encode(sha2563(preimage));
|
|
4855
|
-
if (!preimageHash)
|
|
4856
|
-
throw new SwapError({ message: "Failed to get preimage hash" });
|
|
4749
|
+
if (!preimageHash) throw new SwapError({ message: "Failed to get preimage hash" });
|
|
4857
4750
|
const ephemeralKey = secp256k12.utils.randomSecretKey();
|
|
4858
4751
|
const refundPublicKey = to === "ARK" ? hex8.encode(secp256k12.getPublicKey(ephemeralKey)) : hex8.encode(await this.wallet.identity.compressedPublicKey());
|
|
4859
4752
|
if (!refundPublicKey)
|
|
@@ -4902,30 +4795,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4902
4795
|
const { to, from, swap, arkInfo } = args;
|
|
4903
4796
|
if (from === "ARK") {
|
|
4904
4797
|
if (!swap.response.lockupDetails.serverPublicKey)
|
|
4905
|
-
throw new Error(
|
|
4906
|
-
`Swap ${swap.id}: missing serverPublicKey in lockup details`
|
|
4907
|
-
);
|
|
4798
|
+
throw new Error(`Swap ${swap.id}: missing serverPublicKey in lockup details`);
|
|
4908
4799
|
if (!swap.response.lockupDetails.timeouts)
|
|
4909
|
-
throw new Error(
|
|
4910
|
-
`Swap ${swap.id}: missing timeouts in lockup details`
|
|
4911
|
-
);
|
|
4800
|
+
throw new Error(`Swap ${swap.id}: missing timeouts in lockup details`);
|
|
4912
4801
|
}
|
|
4913
4802
|
if (to === "ARK") {
|
|
4914
4803
|
if (!swap.response.claimDetails.serverPublicKey)
|
|
4915
|
-
throw new Error(
|
|
4916
|
-
`Swap ${swap.id}: missing serverPublicKey in claim details`
|
|
4917
|
-
);
|
|
4804
|
+
throw new Error(`Swap ${swap.id}: missing serverPublicKey in claim details`);
|
|
4918
4805
|
if (!swap.response.claimDetails.timeouts)
|
|
4919
|
-
throw new Error(
|
|
4920
|
-
`Swap ${swap.id}: missing timeouts in claim details`
|
|
4921
|
-
);
|
|
4806
|
+
throw new Error(`Swap ${swap.id}: missing timeouts in claim details`);
|
|
4922
4807
|
}
|
|
4923
4808
|
const lockupAddress = to === "ARK" ? swap.response.claimDetails.lockupAddress : swap.response.lockupDetails.lockupAddress;
|
|
4924
4809
|
const receiverPubkey = to === "ARK" ? swap.request.claimPublicKey : swap.response.lockupDetails.serverPublicKey;
|
|
4925
4810
|
const senderPubkey = to === "ARK" ? swap.response.claimDetails.serverPublicKey : swap.request.refundPublicKey;
|
|
4926
|
-
const serverPubkey = hex8.encode(
|
|
4927
|
-
normalizeToXOnlyKey(arkInfo.signerPubkey, "server")
|
|
4928
|
-
);
|
|
4811
|
+
const serverPubkey = hex8.encode(normalizeToXOnlyKey(arkInfo.signerPubkey, "server"));
|
|
4929
4812
|
const vhtlcTimeouts = to === "ARK" ? swap.response.claimDetails.timeouts : swap.response.lockupDetails.timeouts;
|
|
4930
4813
|
const { vhtlcAddress } = this.createVHTLCScript({
|
|
4931
4814
|
network: arkInfo.network,
|
|
@@ -4943,15 +4826,122 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4943
4826
|
return true;
|
|
4944
4827
|
}
|
|
4945
4828
|
/**
|
|
4946
|
-
* Renegotiates the quote for an existing swap.
|
|
4829
|
+
* Renegotiates the quote for an existing chain swap. Convenience wrapper
|
|
4830
|
+
* over `getSwapQuote` + `acceptSwapQuote` with a safety floor.
|
|
4831
|
+
*
|
|
4832
|
+
* The floor is resolved in order:
|
|
4833
|
+
* 1. `options.minAcceptableAmount` if provided.
|
|
4834
|
+
* 2. The original `response.claimDetails.amount` of the stored
|
|
4835
|
+
* pending swap (Boltz-confirmed server-lock amount at creation).
|
|
4836
|
+
* 3. Otherwise throws `QuoteRejectedError({ reason: "no_baseline" })`.
|
|
4837
|
+
*
|
|
4838
|
+
* `options.maxSlippageBps` (default 0) relaxes the floor by basis points.
|
|
4839
|
+
* Quotes ≤ 0 are always rejected. On rejection the acceptance is NOT
|
|
4840
|
+
* posted to Boltz.
|
|
4841
|
+
*
|
|
4842
|
+
* Prefer `getSwapQuote` / `acceptSwapQuote` for callers that want to
|
|
4843
|
+
* inspect the quote before committing.
|
|
4844
|
+
*
|
|
4947
4845
|
* @param swapId - The ID of the swap.
|
|
4846
|
+
* @param options - Optional floor and slippage configuration.
|
|
4948
4847
|
* @returns The accepted quote amount.
|
|
4848
|
+
* @throws QuoteRejectedError if the quote is non-positive, below the
|
|
4849
|
+
* effective floor, or no baseline is available.
|
|
4949
4850
|
*/
|
|
4950
|
-
async quoteSwap(swapId) {
|
|
4851
|
+
async quoteSwap(swapId, options) {
|
|
4852
|
+
const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
|
|
4853
|
+
const amount = await this.getSwapQuote(swapId);
|
|
4854
|
+
this.validateQuote(amount, effectiveFloor);
|
|
4855
|
+
await this.swapProvider.postChainQuote(swapId, { amount });
|
|
4856
|
+
return amount;
|
|
4857
|
+
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Fetches a renegotiated quote from Boltz without accepting it.
|
|
4860
|
+
* Pair with `acceptSwapQuote` to commit a specific value.
|
|
4861
|
+
*/
|
|
4862
|
+
async getSwapQuote(swapId) {
|
|
4951
4863
|
const { amount } = await this.swapProvider.getChainQuote(swapId);
|
|
4864
|
+
return amount;
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* Accepts a quote amount for an existing chain swap, after validating it
|
|
4868
|
+
* against the configured floor. See `quoteSwap` for floor-resolution rules.
|
|
4869
|
+
*
|
|
4870
|
+
* @throws QuoteRejectedError if `amount` ≤ 0, below the effective floor,
|
|
4871
|
+
* or no baseline is available.
|
|
4872
|
+
*/
|
|
4873
|
+
async acceptSwapQuote(swapId, amount, options) {
|
|
4874
|
+
const effectiveFloor = await this.resolveEffectiveFloor(swapId, options);
|
|
4875
|
+
this.validateQuote(amount, effectiveFloor);
|
|
4952
4876
|
await this.swapProvider.postChainQuote(swapId, { amount });
|
|
4953
4877
|
return amount;
|
|
4954
4878
|
}
|
|
4879
|
+
async resolveEffectiveFloor(swapId, options) {
|
|
4880
|
+
this.validateQuoteOptions(options);
|
|
4881
|
+
const floor = await this.resolveQuoteFloor(swapId, options);
|
|
4882
|
+
const slippageBps = options?.maxSlippageBps ?? 0;
|
|
4883
|
+
const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
|
|
4884
|
+
if (effectiveFloor < 1) {
|
|
4885
|
+
throw new TypeError(
|
|
4886
|
+
`Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
|
|
4887
|
+
);
|
|
4888
|
+
}
|
|
4889
|
+
return effectiveFloor;
|
|
4890
|
+
}
|
|
4891
|
+
async resolveQuoteFloor(swapId, options) {
|
|
4892
|
+
if (options?.minAcceptableAmount !== void 0) {
|
|
4893
|
+
return options.minAcceptableAmount;
|
|
4894
|
+
}
|
|
4895
|
+
const swaps = await this.swapRepository.getAllSwaps({
|
|
4896
|
+
id: swapId,
|
|
4897
|
+
type: "chain"
|
|
4898
|
+
});
|
|
4899
|
+
const stored = swaps[0];
|
|
4900
|
+
const amount = stored?.response?.claimDetails?.amount;
|
|
4901
|
+
if (typeof amount !== "number") {
|
|
4902
|
+
throw new QuoteRejectedError({ reason: "no_baseline" });
|
|
4903
|
+
}
|
|
4904
|
+
return amount;
|
|
4905
|
+
}
|
|
4906
|
+
validateQuoteOptions(options) {
|
|
4907
|
+
if (options?.minAcceptableAmount !== void 0) {
|
|
4908
|
+
const v = options.minAcceptableAmount;
|
|
4909
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
4910
|
+
throw new TypeError(
|
|
4911
|
+
`Invalid minAcceptableAmount: ${v} \u2014 must be a positive integer`
|
|
4912
|
+
);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
if (options?.maxSlippageBps !== void 0) {
|
|
4916
|
+
const v = options.maxSlippageBps;
|
|
4917
|
+
if (!Number.isInteger(v) || v < 0 || v > 1e4) {
|
|
4918
|
+
throw new TypeError(
|
|
4919
|
+
`Invalid maxSlippageBps: ${v} \u2014 must be an integer in [0, 10000]`
|
|
4920
|
+
);
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
validateQuote(amount, effectiveFloor) {
|
|
4925
|
+
if (!Number.isSafeInteger(amount)) {
|
|
4926
|
+
throw new QuoteRejectedError({
|
|
4927
|
+
reason: "non_safe_integer",
|
|
4928
|
+
quotedAmount: amount
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
if (amount <= 0) {
|
|
4932
|
+
throw new QuoteRejectedError({
|
|
4933
|
+
reason: "non_positive",
|
|
4934
|
+
quotedAmount: amount
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
if (amount < effectiveFloor) {
|
|
4938
|
+
throw new QuoteRejectedError({
|
|
4939
|
+
reason: "below_floor",
|
|
4940
|
+
quotedAmount: amount,
|
|
4941
|
+
floor: effectiveFloor
|
|
4942
|
+
});
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4955
4945
|
// =========================================================================
|
|
4956
4946
|
// Shared utilities
|
|
4957
4947
|
// =========================================================================
|
|
@@ -4965,14 +4955,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4965
4955
|
* @returns The commitment transaction ID.
|
|
4966
4956
|
*/
|
|
4967
4957
|
async joinBatch(identity, input, output, arkInfo, isRecoverable2 = true) {
|
|
4968
|
-
return joinBatch(
|
|
4969
|
-
this.arkProvider,
|
|
4970
|
-
identity,
|
|
4971
|
-
input,
|
|
4972
|
-
output,
|
|
4973
|
-
arkInfo,
|
|
4974
|
-
isRecoverable2
|
|
4975
|
-
);
|
|
4958
|
+
return joinBatch(this.arkProvider, identity, input, output, arkInfo, isRecoverable2);
|
|
4976
4959
|
}
|
|
4977
4960
|
/**
|
|
4978
4961
|
* Creates a VHTLC script for the swap.
|
|
@@ -5010,9 +4993,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5010
4993
|
async getPendingSubmarineSwaps() {
|
|
5011
4994
|
const swaps = await this.getPendingSubmarineSwapsFromStorage();
|
|
5012
4995
|
if (!swaps) return [];
|
|
5013
|
-
return swaps.filter(
|
|
5014
|
-
(swap) => swap.status === "invoice.set"
|
|
5015
|
-
);
|
|
4996
|
+
return swaps.filter((swap) => swap.status === "invoice.set");
|
|
5016
4997
|
}
|
|
5017
4998
|
/**
|
|
5018
4999
|
* Returns pending reverse swaps (those with status `swap.created`).
|
|
@@ -5020,9 +5001,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5020
5001
|
async getPendingReverseSwaps() {
|
|
5021
5002
|
const swaps = await this.getPendingReverseSwapsFromStorage();
|
|
5022
5003
|
if (!swaps) return [];
|
|
5023
|
-
return swaps.filter(
|
|
5024
|
-
(swap) => swap.status === "swap.created"
|
|
5025
|
-
);
|
|
5004
|
+
return swaps.filter((swap) => swap.status === "swap.created");
|
|
5026
5005
|
}
|
|
5027
5006
|
/**
|
|
5028
5007
|
* Returns pending chain swaps (those with status `swap.created`).
|
|
@@ -5061,10 +5040,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5061
5040
|
this.savePendingReverseSwap.bind(this)
|
|
5062
5041
|
)
|
|
5063
5042
|
).catch((error) => {
|
|
5064
|
-
logger.error(
|
|
5065
|
-
`Failed to refresh swap status for ${swap.id}:`,
|
|
5066
|
-
error
|
|
5067
|
-
);
|
|
5043
|
+
logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
|
|
5068
5044
|
})
|
|
5069
5045
|
);
|
|
5070
5046
|
}
|
|
@@ -5078,23 +5054,15 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5078
5054
|
this.savePendingSubmarineSwap.bind(this)
|
|
5079
5055
|
)
|
|
5080
5056
|
).catch((error) => {
|
|
5081
|
-
logger.error(
|
|
5082
|
-
`Failed to refresh swap status for ${swap.id}:`,
|
|
5083
|
-
error
|
|
5084
|
-
);
|
|
5057
|
+
logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
|
|
5085
5058
|
})
|
|
5086
5059
|
);
|
|
5087
5060
|
}
|
|
5088
5061
|
for (const swap of await this.getPendingChainSwapsFromStorage()) {
|
|
5089
5062
|
if (isChainFinalStatus(swap.status)) continue;
|
|
5090
5063
|
promises.push(
|
|
5091
|
-
this.getSwapStatus(swap.id).then(
|
|
5092
|
-
(
|
|
5093
|
-
).catch((error) => {
|
|
5094
|
-
logger.error(
|
|
5095
|
-
`Failed to refresh swap status for ${swap.id}:`,
|
|
5096
|
-
error
|
|
5097
|
-
);
|
|
5064
|
+
this.getSwapStatus(swap.id).then(({ status }) => this.savePendingChainSwap({ ...swap, status })).catch((error) => {
|
|
5065
|
+
logger.error(`Failed to refresh swap status for ${swap.id}:`, error);
|
|
5098
5066
|
})
|
|
5099
5067
|
);
|
|
5100
5068
|
}
|
|
@@ -5111,9 +5079,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5111
5079
|
* display/monitoring and are not automatically wired into the SwapManager.
|
|
5112
5080
|
*/
|
|
5113
5081
|
async restoreSwaps(boltzFees) {
|
|
5114
|
-
const publicKey = hex8.encode(
|
|
5115
|
-
await this.wallet.identity.compressedPublicKey()
|
|
5116
|
-
);
|
|
5082
|
+
const publicKey = hex8.encode(await this.wallet.identity.compressedPublicKey());
|
|
5117
5083
|
if (!publicKey) throw new Error("Failed to get public key from wallet");
|
|
5118
5084
|
const fees = boltzFees ?? await this.swapProvider.getFees();
|
|
5119
5085
|
const chainSwaps = [];
|
|
@@ -5165,25 +5131,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5165
5131
|
preimage: ""
|
|
5166
5132
|
});
|
|
5167
5133
|
} else if (isRestoredSubmarineSwap(swap)) {
|
|
5168
|
-
const {
|
|
5169
|
-
amount,
|
|
5170
|
-
lockupAddress,
|
|
5171
|
-
serverPublicKey,
|
|
5172
|
-
tree,
|
|
5173
|
-
timeoutBlockHeights
|
|
5174
|
-
} = swap.refundDetails;
|
|
5134
|
+
const { amount, lockupAddress, serverPublicKey, tree, timeoutBlockHeights } = swap.refundDetails;
|
|
5175
5135
|
let preimage = "";
|
|
5176
5136
|
if (!isSubmarineFinalStatus(status)) {
|
|
5177
5137
|
try {
|
|
5178
|
-
const data = await this.swapProvider.getSwapPreimage(
|
|
5179
|
-
swap.id
|
|
5180
|
-
);
|
|
5138
|
+
const data = await this.swapProvider.getSwapPreimage(swap.id);
|
|
5181
5139
|
preimage = data.preimage;
|
|
5182
5140
|
} catch (error) {
|
|
5183
|
-
logger.warn(
|
|
5184
|
-
`Failed to restore preimage for submarine swap ${id}`,
|
|
5185
|
-
error
|
|
5186
|
-
);
|
|
5141
|
+
logger.warn(`Failed to restore preimage for submarine swap ${id}`, error);
|
|
5187
5142
|
}
|
|
5188
5143
|
}
|
|
5189
5144
|
submarineSwaps.push({
|
|
@@ -5221,12 +5176,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5221
5176
|
} else if (isRestoredChainSwap(swap)) {
|
|
5222
5177
|
const refundDetails = swap.refundDetails;
|
|
5223
5178
|
if (!refundDetails) continue;
|
|
5224
|
-
const {
|
|
5225
|
-
amount,
|
|
5226
|
-
lockupAddress,
|
|
5227
|
-
serverPublicKey,
|
|
5228
|
-
timeoutBlockHeight
|
|
5229
|
-
} = refundDetails;
|
|
5179
|
+
const { amount, lockupAddress, serverPublicKey, timeoutBlockHeight } = refundDetails;
|
|
5230
5180
|
chainSwaps.push({
|
|
5231
5181
|
id,
|
|
5232
5182
|
type: "chain",
|
|
@@ -5285,6 +5235,7 @@ export {
|
|
|
5285
5235
|
SwapExpiredError,
|
|
5286
5236
|
TransactionFailedError,
|
|
5287
5237
|
PreimageFetchError,
|
|
5238
|
+
QuoteRejectedError,
|
|
5288
5239
|
BoltzRefundError,
|
|
5289
5240
|
isSubmarineFailedStatus,
|
|
5290
5241
|
isSubmarineFinalStatus,
|