@arkade-os/boltz-swap 0.3.33 → 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/dist/{arkade-swaps-9M7FRuq1.d.cts → arkade-swaps-BXAD1s8j.d.ts} +13 -4
- package/dist/{arkade-swaps-DNsyWeFr.d.ts → arkade-swaps-CfMets16.d.cts} +13 -4
- package/dist/{chunk-HNQDJOLM.js → chunk-5K2FS2FE.js} +1 -1
- package/dist/{chunk-SJ5SYSMK.js → chunk-TDBUZE4N.js} +269 -90
- package/dist/expo/background.cjs +270 -90
- package/dist/expo/background.d.cts +3 -3
- package/dist/expo/background.d.ts +3 -3
- package/dist/expo/background.js +3 -2
- package/dist/expo/index.cjs +269 -90
- package/dist/expo/index.d.cts +5 -5
- package/dist/expo/index.d.ts +5 -5
- package/dist/expo/index.js +2 -2
- package/dist/index.cjs +305 -94
- package/dist/index.d.cts +21 -7
- package/dist/index.d.ts +21 -7
- package/dist/index.js +40 -6
- package/dist/repositories/realm/index.d.cts +1 -1
- package/dist/repositories/realm/index.d.ts +1 -1
- package/dist/repositories/sqlite/index.d.cts +1 -1
- package/dist/repositories/sqlite/index.d.ts +1 -1
- package/dist/{swapsPollProcessor-CuITxZie.d.cts → swapsPollProcessor-BpAqG0V6.d.cts} +1 -1
- package/dist/{swapsPollProcessor-CEgeGlbP.d.ts → swapsPollProcessor-DFVOAy_-.d.ts} +1 -1
- package/dist/{types-CrKkVzBB.d.cts → types--axEWA8c.d.cts} +34 -2
- package/dist/{types-CrKkVzBB.d.ts → types--axEWA8c.d.ts} +34 -2
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
ArkadeSwapsMessageHandler: () => ArkadeSwapsMessageHandler,
|
|
36
36
|
BoltzRefundError: () => BoltzRefundError,
|
|
37
37
|
BoltzSwapProvider: () => BoltzSwapProvider,
|
|
38
|
+
InMemorySwapRepository: () => InMemorySwapRepository,
|
|
38
39
|
IndexedDbSwapRepository: () => IndexedDbSwapRepository,
|
|
39
40
|
InsufficientFundsError: () => InsufficientFundsError,
|
|
40
41
|
InvoiceExpiredError: () => InvoiceExpiredError,
|
|
@@ -215,6 +216,8 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
|
|
|
215
216
|
return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
|
|
216
217
|
case "non_positive":
|
|
217
218
|
return `Boltz quote ${options.quotedAmount} is not positive`;
|
|
219
|
+
case "non_safe_integer":
|
|
220
|
+
return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
|
|
218
221
|
case "no_baseline":
|
|
219
222
|
return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
|
|
220
223
|
}
|
|
@@ -268,6 +271,7 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
|
|
|
268
271
|
message
|
|
269
272
|
});
|
|
270
273
|
case "non_positive":
|
|
274
|
+
case "non_safe_integer":
|
|
271
275
|
if (quotedAmount === null) return null;
|
|
272
276
|
return new _QuoteRejectedError({
|
|
273
277
|
reason,
|
|
@@ -283,6 +287,7 @@ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
|
|
|
283
287
|
var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
|
|
284
288
|
"below_floor",
|
|
285
289
|
"non_positive",
|
|
290
|
+
"non_safe_integer",
|
|
286
291
|
"no_baseline"
|
|
287
292
|
]);
|
|
288
293
|
var BoltzRefundError = class extends Error {
|
|
@@ -1513,6 +1518,13 @@ var SwapManager = class _SwapManager {
|
|
|
1513
1518
|
* enough that a real "swap unknown to this provider" surfaces quickly.
|
|
1514
1519
|
*/
|
|
1515
1520
|
static NOT_FOUND_THRESHOLD = 10;
|
|
1521
|
+
/**
|
|
1522
|
+
* Delay between re-attempts of a chain refund that left VTXOs deferred
|
|
1523
|
+
* (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
|
|
1524
|
+
* has elapsed). Boltz won't send another status update once the swap
|
|
1525
|
+
* is `swap.expired`, so the manager owns the local retry cadence.
|
|
1526
|
+
*/
|
|
1527
|
+
static REFUND_RETRY_DELAY_MS = 6e4;
|
|
1516
1528
|
swapProvider;
|
|
1517
1529
|
config;
|
|
1518
1530
|
// Event listeners storage (supports multiple listeners per event)
|
|
@@ -1531,6 +1543,11 @@ var SwapManager = class _SwapManager {
|
|
|
1531
1543
|
reconnectTimer = null;
|
|
1532
1544
|
initialPollTimer = null;
|
|
1533
1545
|
pollRetryTimers = /* @__PURE__ */ new Map();
|
|
1546
|
+
// Per-swap retry timers for chain refunds that left work undone
|
|
1547
|
+
// (refundArk returned `skipped > 0`). The swap is held in
|
|
1548
|
+
// `monitoredSwaps` past its terminal Boltz status until the local
|
|
1549
|
+
// refund completes or the manager stops.
|
|
1550
|
+
refundRetryTimers = /* @__PURE__ */ new Map();
|
|
1534
1551
|
// Per-swap counter of consecutive `SwapNotFoundError` responses from
|
|
1535
1552
|
// `getSwapStatus`. Reset on any successful poll. Once a swap reaches
|
|
1536
1553
|
// `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
|
|
@@ -1727,6 +1744,10 @@ var SwapManager = class _SwapManager {
|
|
|
1727
1744
|
clearTimeout(timer);
|
|
1728
1745
|
}
|
|
1729
1746
|
this.pollRetryTimers.clear();
|
|
1747
|
+
for (const timer of this.refundRetryTimers.values()) {
|
|
1748
|
+
clearTimeout(timer);
|
|
1749
|
+
}
|
|
1750
|
+
this.refundRetryTimers.clear();
|
|
1730
1751
|
this.notFoundCounts.clear();
|
|
1731
1752
|
}
|
|
1732
1753
|
/**
|
|
@@ -1783,6 +1804,11 @@ var SwapManager = class _SwapManager {
|
|
|
1783
1804
|
clearTimeout(retryTimer);
|
|
1784
1805
|
this.pollRetryTimers.delete(swapId);
|
|
1785
1806
|
}
|
|
1807
|
+
const refundRetryTimer = this.refundRetryTimers.get(swapId);
|
|
1808
|
+
if (refundRetryTimer) {
|
|
1809
|
+
clearTimeout(refundRetryTimer);
|
|
1810
|
+
this.refundRetryTimers.delete(swapId);
|
|
1811
|
+
}
|
|
1786
1812
|
this.notFoundCounts.delete(swapId);
|
|
1787
1813
|
logger.log(`Removed swap ${swapId} from monitoring`);
|
|
1788
1814
|
}
|
|
@@ -2054,16 +2080,58 @@ var SwapManager = class _SwapManager {
|
|
|
2054
2080
|
await this.executeAutonomousAction(swap);
|
|
2055
2081
|
}
|
|
2056
2082
|
if (this.isFinalStatus(swap)) {
|
|
2057
|
-
this.
|
|
2058
|
-
|
|
2059
|
-
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
2060
|
-
if (retryTimer) {
|
|
2061
|
-
clearTimeout(retryTimer);
|
|
2062
|
-
this.pollRetryTimers.delete(swap.id);
|
|
2083
|
+
if (this.refundRetryTimers.has(swap.id)) {
|
|
2084
|
+
return;
|
|
2063
2085
|
}
|
|
2064
|
-
this.
|
|
2086
|
+
this.finalizeMonitoredSwap(swap);
|
|
2065
2087
|
}
|
|
2066
2088
|
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Drop a swap from monitoring and emit the terminal completion event.
|
|
2091
|
+
* Shared between the on-status-update finalization path and the
|
|
2092
|
+
* refund-retry finalization path (used when a previously-deferred
|
|
2093
|
+
* chain refund has finished its remaining work).
|
|
2094
|
+
*/
|
|
2095
|
+
finalizeMonitoredSwap(swap) {
|
|
2096
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
2097
|
+
this.monitoredSwaps.delete(swap.id);
|
|
2098
|
+
this.swapSubscriptions.delete(swap.id);
|
|
2099
|
+
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
2100
|
+
if (retryTimer) {
|
|
2101
|
+
clearTimeout(retryTimer);
|
|
2102
|
+
this.pollRetryTimers.delete(swap.id);
|
|
2103
|
+
}
|
|
2104
|
+
const refundRetry = this.refundRetryTimers.get(swap.id);
|
|
2105
|
+
if (refundRetry) {
|
|
2106
|
+
clearTimeout(refundRetry);
|
|
2107
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2108
|
+
}
|
|
2109
|
+
this.swapCompletedListeners.forEach((listener) => listener(swap));
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Schedule another `executeAutonomousAction` run for a chain swap whose
|
|
2113
|
+
* refund left VTXOs deferred. After the retry completes, if no further
|
|
2114
|
+
* deferral was reported, finalize monitoring cleanup.
|
|
2115
|
+
*/
|
|
2116
|
+
scheduleRefundRetry(swap, delayMs) {
|
|
2117
|
+
const existing = this.refundRetryTimers.get(swap.id);
|
|
2118
|
+
if (existing) clearTimeout(existing);
|
|
2119
|
+
this.refundRetryTimers.set(
|
|
2120
|
+
swap.id,
|
|
2121
|
+
setTimeout(async () => {
|
|
2122
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2123
|
+
if (!this.isRunning) return;
|
|
2124
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
2125
|
+
try {
|
|
2126
|
+
await this.executeAutonomousAction(swap);
|
|
2127
|
+
} finally {
|
|
2128
|
+
if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
|
|
2129
|
+
this.finalizeMonitoredSwap(swap);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}, delayMs)
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2067
2135
|
/**
|
|
2068
2136
|
* Execute autonomous action based on swap status
|
|
2069
2137
|
* Uses locking to prevent race conditions with manual operations
|
|
@@ -2117,10 +2185,27 @@ var SwapManager = class _SwapManager {
|
|
|
2117
2185
|
} else if (isChainRefundableStatus(swap.status)) {
|
|
2118
2186
|
if (swap.request.from === "ARK") {
|
|
2119
2187
|
logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
(
|
|
2123
|
-
|
|
2188
|
+
try {
|
|
2189
|
+
const outcome = await this.executeRefundArkAction(swap);
|
|
2190
|
+
if (outcome && outcome.skipped > 0) {
|
|
2191
|
+
logger.log(
|
|
2192
|
+
`Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
|
|
2193
|
+
);
|
|
2194
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
2195
|
+
}
|
|
2196
|
+
this.actionExecutedListeners.forEach(
|
|
2197
|
+
(listener) => listener(swap, "refundArk")
|
|
2198
|
+
);
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
logger.error(
|
|
2201
|
+
`Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
|
|
2202
|
+
error
|
|
2203
|
+
);
|
|
2204
|
+
this.swapFailedListeners.forEach(
|
|
2205
|
+
(listener) => listener(swap, error)
|
|
2206
|
+
);
|
|
2207
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
2208
|
+
}
|
|
2124
2209
|
}
|
|
2125
2210
|
if (swap.request.from === "BTC") {
|
|
2126
2211
|
logger.warn(
|
|
@@ -2201,7 +2286,7 @@ var SwapManager = class _SwapManager {
|
|
|
2201
2286
|
logger.error("refundArk callback not set");
|
|
2202
2287
|
return;
|
|
2203
2288
|
}
|
|
2204
|
-
|
|
2289
|
+
return this.refundArkCallback(swap);
|
|
2205
2290
|
}
|
|
2206
2291
|
/**
|
|
2207
2292
|
* Execute sign server claim action for chain swap.
|
|
@@ -2301,9 +2386,7 @@ var SwapManager = class _SwapManager {
|
|
|
2301
2386
|
*/
|
|
2302
2387
|
async pollAllSwaps() {
|
|
2303
2388
|
if (this.monitoredSwaps.size === 0) return;
|
|
2304
|
-
const pollPromises = Array.from(this.monitoredSwaps.values()).map(
|
|
2305
|
-
(swap) => this.pollSingleSwap(swap)
|
|
2306
|
-
);
|
|
2389
|
+
const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
|
|
2307
2390
|
await Promise.allSettled(pollPromises);
|
|
2308
2391
|
}
|
|
2309
2392
|
async pollSingleSwap(swap) {
|
|
@@ -2356,6 +2439,7 @@ var SwapManager = class _SwapManager {
|
|
|
2356
2439
|
* Boltz endpoint).
|
|
2357
2440
|
*/
|
|
2358
2441
|
async handleSwapNotFound(swap) {
|
|
2442
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
2359
2443
|
const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
|
|
2360
2444
|
this.notFoundCounts.set(swap.id, count);
|
|
2361
2445
|
logger.warn(
|
|
@@ -2376,6 +2460,7 @@ var SwapManager = class _SwapManager {
|
|
|
2376
2460
|
* 404s without recovering anything.
|
|
2377
2461
|
*/
|
|
2378
2462
|
async markSwapAsUnknownToProvider(swap) {
|
|
2463
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
2379
2464
|
if (!this.monitoredSwaps.has(swap.id)) {
|
|
2380
2465
|
this.notFoundCounts.delete(swap.id);
|
|
2381
2466
|
return;
|
|
@@ -2388,6 +2473,11 @@ var SwapManager = class _SwapManager {
|
|
|
2388
2473
|
clearTimeout(retryTimer);
|
|
2389
2474
|
this.pollRetryTimers.delete(swap.id);
|
|
2390
2475
|
}
|
|
2476
|
+
const refundRetryTimer = this.refundRetryTimers.get(swap.id);
|
|
2477
|
+
if (refundRetryTimer) {
|
|
2478
|
+
clearTimeout(refundRetryTimer);
|
|
2479
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2480
|
+
}
|
|
2391
2481
|
this.notFoundCounts.delete(swap.id);
|
|
2392
2482
|
this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
|
|
2393
2483
|
const subscribers = this.swapSubscriptions.get(swap.id);
|
|
@@ -3145,7 +3235,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3145
3235
|
await this.claimBtc(swap);
|
|
3146
3236
|
},
|
|
3147
3237
|
refundArk: async (swap) => {
|
|
3148
|
-
|
|
3238
|
+
return this.refundArk(swap);
|
|
3149
3239
|
},
|
|
3150
3240
|
signServerClaim: async (swap) => {
|
|
3151
3241
|
await this.signCooperativeClaimForServer(swap);
|
|
@@ -3358,51 +3448,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3358
3448
|
throw new Error(
|
|
3359
3449
|
`Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
|
|
3360
3450
|
);
|
|
3361
|
-
let
|
|
3451
|
+
let unspentVtxos = [];
|
|
3452
|
+
let rawVtxos = [];
|
|
3362
3453
|
for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
|
|
3363
|
-
const
|
|
3454
|
+
const result = await this.indexerProvider.getVtxos({
|
|
3364
3455
|
scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
|
|
3365
3456
|
});
|
|
3366
|
-
|
|
3367
|
-
|
|
3457
|
+
rawVtxos = result.vtxos;
|
|
3458
|
+
unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
3459
|
+
if (unspentVtxos.length > 0) {
|
|
3368
3460
|
break;
|
|
3369
3461
|
}
|
|
3370
3462
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
3371
3463
|
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
3372
3464
|
}
|
|
3373
3465
|
}
|
|
3374
|
-
if (
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3466
|
+
if (unspentVtxos.length === 0) {
|
|
3467
|
+
if (rawVtxos.length === 0) {
|
|
3468
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
3469
|
+
}
|
|
3378
3470
|
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3379
3471
|
}
|
|
3380
|
-
const input = {
|
|
3381
|
-
...vtxo,
|
|
3382
|
-
tapLeafScript: vhtlcScript.claim(),
|
|
3383
|
-
tapTree: vhtlcScript.encode()
|
|
3384
|
-
};
|
|
3385
|
-
const output = {
|
|
3386
|
-
amount: BigInt(vtxo.value),
|
|
3387
|
-
script: import_sdk8.ArkAddress.decode(address).pkScript
|
|
3388
|
-
};
|
|
3389
3472
|
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
vhtlcScript
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3473
|
+
const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
|
|
3474
|
+
const claimErrors = [];
|
|
3475
|
+
let usedOffchainClaim = false;
|
|
3476
|
+
for (const vtxo of unspentVtxos) {
|
|
3477
|
+
const input = {
|
|
3478
|
+
...vtxo,
|
|
3479
|
+
tapLeafScript: vhtlcScript.claim(),
|
|
3480
|
+
tapTree: vhtlcScript.encode()
|
|
3481
|
+
};
|
|
3482
|
+
const output = {
|
|
3483
|
+
amount: BigInt(vtxo.value),
|
|
3484
|
+
script: outputScript
|
|
3485
|
+
};
|
|
3486
|
+
try {
|
|
3487
|
+
if ((0, import_sdk8.isRecoverable)(vtxo)) {
|
|
3488
|
+
await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
|
|
3489
|
+
} else {
|
|
3490
|
+
await claimVHTLCwithOffchainTx(
|
|
3491
|
+
vhtlcIdentity,
|
|
3492
|
+
vhtlcScript,
|
|
3493
|
+
serverXOnly,
|
|
3494
|
+
input,
|
|
3495
|
+
output,
|
|
3496
|
+
arkInfo,
|
|
3497
|
+
this.arkProvider
|
|
3498
|
+
);
|
|
3499
|
+
usedOffchainClaim = true;
|
|
3500
|
+
}
|
|
3501
|
+
} catch (error) {
|
|
3502
|
+
claimErrors.push({ vtxo, error });
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (claimErrors.length > 0) {
|
|
3506
|
+
const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
|
|
3507
|
+
throw new Error(
|
|
3508
|
+
`Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
|
|
3403
3509
|
);
|
|
3404
|
-
finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
|
|
3405
3510
|
}
|
|
3511
|
+
const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
|
|
3406
3512
|
await updateReverseSwapStatus(
|
|
3407
3513
|
pendingSwap,
|
|
3408
3514
|
finalStatus,
|
|
@@ -3777,6 +3883,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3777
3883
|
if (boltzCallCount > 0) {
|
|
3778
3884
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
3779
3885
|
}
|
|
3886
|
+
boltzCallCount++;
|
|
3780
3887
|
await refundVHTLCwithOffchainTx(
|
|
3781
3888
|
pendingSwap.id,
|
|
3782
3889
|
this.wallet.identity,
|
|
@@ -3789,7 +3896,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3789
3896
|
arkInfo,
|
|
3790
3897
|
this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
|
|
3791
3898
|
);
|
|
3792
|
-
boltzCallCount++;
|
|
3793
3899
|
sweptCount++;
|
|
3794
3900
|
} catch (error) {
|
|
3795
3901
|
if (!(error instanceof BoltzRefundError)) {
|
|
@@ -4287,8 +4393,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4287
4393
|
await this.swapProvider.postBtcTransaction(claimTx.hex);
|
|
4288
4394
|
}
|
|
4289
4395
|
/**
|
|
4290
|
-
* When an ARK to BTC swap fails, refund
|
|
4396
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
4397
|
+
* swap's ARK lockup address.
|
|
4398
|
+
*
|
|
4399
|
+
* Path selection per VTXO:
|
|
4400
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
4401
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
4402
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
4403
|
+
*
|
|
4291
4404
|
* @param pendingSwap - The pending chain swap to refund.
|
|
4405
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
4406
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
4292
4407
|
*/
|
|
4293
4408
|
async refundArk(pendingSwap) {
|
|
4294
4409
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
@@ -4312,21 +4427,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4312
4427
|
"boltz",
|
|
4313
4428
|
pendingSwap.id
|
|
4314
4429
|
);
|
|
4315
|
-
const vhtlcPkScript = import_sdk8.ArkAddress.decode(
|
|
4316
|
-
pendingSwap.response.lockupDetails.lockupAddress
|
|
4317
|
-
).pkScript;
|
|
4318
|
-
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4319
|
-
scripts: [import_base9.hex.encode(vhtlcPkScript)]
|
|
4320
|
-
});
|
|
4321
|
-
if (vtxos.length === 0) {
|
|
4322
|
-
throw new Error(
|
|
4323
|
-
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4324
|
-
);
|
|
4325
|
-
}
|
|
4326
|
-
const vtxo = vtxos[0];
|
|
4327
|
-
if (vtxo.isSpent) {
|
|
4328
|
-
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4329
|
-
}
|
|
4330
4430
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4331
4431
|
network: arkInfo.network,
|
|
4332
4432
|
preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
|
|
@@ -4342,37 +4442,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4342
4442
|
message: "Unable to claim: invalid VHTLC address"
|
|
4343
4443
|
});
|
|
4344
4444
|
}
|
|
4345
|
-
const
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
const output = {
|
|
4352
|
-
amount: BigInt(vtxo.value),
|
|
4353
|
-
script: import_sdk8.ArkAddress.decode(address).pkScript
|
|
4354
|
-
};
|
|
4355
|
-
if (isRecoverableVtxo) {
|
|
4356
|
-
await this.joinBatch(this.wallet.identity, input, output, arkInfo);
|
|
4357
|
-
} else {
|
|
4358
|
-
await refundVHTLCwithOffchainTx(
|
|
4359
|
-
pendingSwap.id,
|
|
4360
|
-
this.wallet.identity,
|
|
4361
|
-
this.arkProvider,
|
|
4362
|
-
boltzXOnlyPublicKey,
|
|
4363
|
-
ourXOnlyPublicKey,
|
|
4364
|
-
serverXOnlyPublicKey,
|
|
4365
|
-
input,
|
|
4366
|
-
output,
|
|
4367
|
-
arkInfo,
|
|
4368
|
-
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4445
|
+
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4446
|
+
scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
|
|
4447
|
+
});
|
|
4448
|
+
if (vtxos.length === 0) {
|
|
4449
|
+
throw new Error(
|
|
4450
|
+
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4369
4451
|
);
|
|
4370
4452
|
}
|
|
4453
|
+
const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
4454
|
+
if (unspentVtxos.length === 0) {
|
|
4455
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4456
|
+
}
|
|
4457
|
+
const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
|
|
4458
|
+
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
4459
|
+
const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
|
|
4460
|
+
let boltzCallCount = 0;
|
|
4461
|
+
let sweptCount = 0;
|
|
4462
|
+
let skippedCount = 0;
|
|
4463
|
+
for (const vtxo of unspentVtxos) {
|
|
4464
|
+
const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
|
|
4465
|
+
const output = {
|
|
4466
|
+
amount: BigInt(vtxo.value),
|
|
4467
|
+
script: outputScript
|
|
4468
|
+
};
|
|
4469
|
+
if (isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4470
|
+
const input2 = {
|
|
4471
|
+
...vtxo,
|
|
4472
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4473
|
+
tapTree: vhtlcScript.encode()
|
|
4474
|
+
};
|
|
4475
|
+
await this.joinBatch(
|
|
4476
|
+
this.wallet.identity,
|
|
4477
|
+
input2,
|
|
4478
|
+
output,
|
|
4479
|
+
arkInfo,
|
|
4480
|
+
isRecoverableVtxo
|
|
4481
|
+
);
|
|
4482
|
+
sweptCount++;
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4485
|
+
if (isRecoverableVtxo) {
|
|
4486
|
+
logger.error(
|
|
4487
|
+
`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.`
|
|
4488
|
+
);
|
|
4489
|
+
skippedCount++;
|
|
4490
|
+
continue;
|
|
4491
|
+
}
|
|
4492
|
+
const input = {
|
|
4493
|
+
...vtxo,
|
|
4494
|
+
tapLeafScript: vhtlcScript.refund(),
|
|
4495
|
+
tapTree: vhtlcScript.encode()
|
|
4496
|
+
};
|
|
4497
|
+
try {
|
|
4498
|
+
if (boltzCallCount > 0) {
|
|
4499
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
4500
|
+
}
|
|
4501
|
+
boltzCallCount++;
|
|
4502
|
+
await refundVHTLCwithOffchainTx(
|
|
4503
|
+
pendingSwap.id,
|
|
4504
|
+
this.wallet.identity,
|
|
4505
|
+
this.arkProvider,
|
|
4506
|
+
boltzXOnlyPublicKey,
|
|
4507
|
+
ourXOnlyPublicKey,
|
|
4508
|
+
serverXOnlyPublicKey,
|
|
4509
|
+
input,
|
|
4510
|
+
output,
|
|
4511
|
+
arkInfo,
|
|
4512
|
+
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4513
|
+
);
|
|
4514
|
+
sweptCount++;
|
|
4515
|
+
} catch (error) {
|
|
4516
|
+
if (!(error instanceof BoltzRefundError)) {
|
|
4517
|
+
throw error;
|
|
4518
|
+
}
|
|
4519
|
+
if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4520
|
+
logger.error(
|
|
4521
|
+
`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.`
|
|
4522
|
+
);
|
|
4523
|
+
skippedCount++;
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
logger.warn(
|
|
4527
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
|
|
4528
|
+
);
|
|
4529
|
+
const fallbackInput = {
|
|
4530
|
+
...vtxo,
|
|
4531
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4532
|
+
tapTree: vhtlcScript.encode()
|
|
4533
|
+
};
|
|
4534
|
+
await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
|
|
4535
|
+
sweptCount++;
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4371
4538
|
const finalStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4372
4539
|
await this.savePendingChainSwap({
|
|
4373
4540
|
...pendingSwap,
|
|
4374
4541
|
status: finalStatus.status
|
|
4375
4542
|
});
|
|
4543
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
4376
4544
|
}
|
|
4377
4545
|
// =========================================================================
|
|
4378
4546
|
// Chain swaps: BTC -> ARK
|
|
@@ -4793,7 +4961,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4793
4961
|
this.validateQuoteOptions(options);
|
|
4794
4962
|
const floor = await this.resolveQuoteFloor(swapId, options);
|
|
4795
4963
|
const slippageBps = options?.maxSlippageBps ?? 0;
|
|
4796
|
-
|
|
4964
|
+
const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
|
|
4965
|
+
if (effectiveFloor < 1) {
|
|
4966
|
+
throw new TypeError(
|
|
4967
|
+
`Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
|
|
4968
|
+
);
|
|
4969
|
+
}
|
|
4970
|
+
return effectiveFloor;
|
|
4797
4971
|
}
|
|
4798
4972
|
async resolveQuoteFloor(swapId, options) {
|
|
4799
4973
|
if (options?.minAcceptableAmount !== void 0) {
|
|
@@ -4829,7 +5003,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4829
5003
|
}
|
|
4830
5004
|
}
|
|
4831
5005
|
validateQuote(amount, effectiveFloor) {
|
|
4832
|
-
if (!(amount
|
|
5006
|
+
if (!Number.isSafeInteger(amount)) {
|
|
5007
|
+
throw new QuoteRejectedError({
|
|
5008
|
+
reason: "non_safe_integer",
|
|
5009
|
+
quotedAmount: amount
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
if (amount <= 0) {
|
|
4833
5013
|
throw new QuoteRejectedError({
|
|
4834
5014
|
reason: "non_positive",
|
|
4835
5015
|
quotedAmount: amount
|
|
@@ -5473,9 +5653,14 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
|
|
|
5473
5653
|
case "CLAIM_BTC":
|
|
5474
5654
|
await this.handler.claimBtc(message.payload);
|
|
5475
5655
|
return this.tagged({ id, type: "BTC_CLAIM_EXECUTED" });
|
|
5476
|
-
case "REFUND_ARK":
|
|
5477
|
-
await this.handler.refundArk(message.payload);
|
|
5478
|
-
return this.tagged({
|
|
5656
|
+
case "REFUND_ARK": {
|
|
5657
|
+
const outcome = await this.handler.refundArk(message.payload);
|
|
5658
|
+
return this.tagged({
|
|
5659
|
+
id,
|
|
5660
|
+
type: "ARK_REFUND_EXECUTED",
|
|
5661
|
+
payload: outcome
|
|
5662
|
+
});
|
|
5663
|
+
}
|
|
5479
5664
|
case "SIGN_SERVER_CLAIM":
|
|
5480
5665
|
await this.handler.signCooperativeClaimForServer(message.payload);
|
|
5481
5666
|
return this.tagged({ id, type: "SERVER_CLAIM_SIGNED" });
|
|
@@ -6150,12 +6335,13 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
|
|
|
6150
6335
|
});
|
|
6151
6336
|
}
|
|
6152
6337
|
async refundArk(pendingSwap) {
|
|
6153
|
-
await this.sendMessage({
|
|
6338
|
+
const res = await this.sendMessage({
|
|
6154
6339
|
id: (0, import_sdk10.getRandomId)(),
|
|
6155
6340
|
tag: this.messageTag,
|
|
6156
6341
|
type: "REFUND_ARK",
|
|
6157
6342
|
payload: pendingSwap
|
|
6158
6343
|
});
|
|
6344
|
+
return res.payload;
|
|
6159
6345
|
}
|
|
6160
6346
|
async signCooperativeClaimForServer(pendingSwap) {
|
|
6161
6347
|
await this.sendMessage({
|
|
@@ -6565,6 +6751,30 @@ async function getContractCollection(storage, contractType) {
|
|
|
6565
6751
|
);
|
|
6566
6752
|
}
|
|
6567
6753
|
}
|
|
6754
|
+
|
|
6755
|
+
// src/repositories/inMemory/swap-repository.ts
|
|
6756
|
+
var InMemorySwapRepository = class {
|
|
6757
|
+
version = 1;
|
|
6758
|
+
swaps = /* @__PURE__ */ new Map();
|
|
6759
|
+
async saveSwap(swap) {
|
|
6760
|
+
this.swaps.set(swap.id, swap);
|
|
6761
|
+
}
|
|
6762
|
+
async deleteSwap(id) {
|
|
6763
|
+
this.swaps.delete(id);
|
|
6764
|
+
}
|
|
6765
|
+
async getAllSwaps(filter) {
|
|
6766
|
+
const swaps = [...this.swaps.values()];
|
|
6767
|
+
if (!filter || Object.keys(filter).length === 0) return swaps;
|
|
6768
|
+
const filtered = applySwapsFilter(swaps, filter);
|
|
6769
|
+
return applyCreatedAtOrder(filtered, filter);
|
|
6770
|
+
}
|
|
6771
|
+
async clear() {
|
|
6772
|
+
this.swaps.clear();
|
|
6773
|
+
}
|
|
6774
|
+
async [Symbol.asyncDispose]() {
|
|
6775
|
+
await this.clear();
|
|
6776
|
+
}
|
|
6777
|
+
};
|
|
6568
6778
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6569
6779
|
0 && (module.exports = {
|
|
6570
6780
|
ArkadeLightningMessageHandler,
|
|
@@ -6572,6 +6782,7 @@ async function getContractCollection(storage, contractType) {
|
|
|
6572
6782
|
ArkadeSwapsMessageHandler,
|
|
6573
6783
|
BoltzRefundError,
|
|
6574
6784
|
BoltzSwapProvider,
|
|
6785
|
+
InMemorySwapRepository,
|
|
6575
6786
|
IndexedDbSwapRepository,
|
|
6576
6787
|
InsufficientFundsError,
|
|
6577
6788
|
InvoiceExpiredError,
|