@arkade-os/boltz-swap 0.3.33 → 0.3.35
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-SJ5SYSMK.js → chunk-B4CYBKFJ.js} +295 -120
- package/dist/{chunk-HNQDJOLM.js → chunk-H6F67K2A.js} +1 -1
- package/dist/expo/background.cjs +296 -120
- 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 +295 -120
- 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 +331 -124
- 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 +3 -3
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 {
|
|
@@ -1496,6 +1501,20 @@ function extractInvoiceAmount(amountSats, fees) {
|
|
|
1496
1501
|
if (miner >= amountSats) return 0;
|
|
1497
1502
|
return Math.ceil((amountSats - miner) / (1 - percentage / 100));
|
|
1498
1503
|
}
|
|
1504
|
+
function resolveVhtlcTimeouts(tree, timeoutBlockHeights) {
|
|
1505
|
+
const resolved = timeoutBlockHeights ?? {
|
|
1506
|
+
refund: extractTimeLockFromLeafOutput(tree.refundWithoutBoltzLeaf?.output ?? ""),
|
|
1507
|
+
unilateralClaim: extractTimeLockFromLeafOutput(tree.unilateralClaimLeaf?.output ?? ""),
|
|
1508
|
+
unilateralRefund: extractTimeLockFromLeafOutput(tree.unilateralRefundLeaf?.output ?? ""),
|
|
1509
|
+
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
1510
|
+
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
1511
|
+
)
|
|
1512
|
+
};
|
|
1513
|
+
if (!resolved.refund || !resolved.unilateralClaim || !resolved.unilateralRefund || !resolved.unilateralRefundWithoutReceiver) {
|
|
1514
|
+
return void 0;
|
|
1515
|
+
}
|
|
1516
|
+
return resolved;
|
|
1517
|
+
}
|
|
1499
1518
|
|
|
1500
1519
|
// src/logger.ts
|
|
1501
1520
|
var logger = console;
|
|
@@ -1513,6 +1532,13 @@ var SwapManager = class _SwapManager {
|
|
|
1513
1532
|
* enough that a real "swap unknown to this provider" surfaces quickly.
|
|
1514
1533
|
*/
|
|
1515
1534
|
static NOT_FOUND_THRESHOLD = 10;
|
|
1535
|
+
/**
|
|
1536
|
+
* Delay between re-attempts of a chain refund that left VTXOs deferred
|
|
1537
|
+
* (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
|
|
1538
|
+
* has elapsed). Boltz won't send another status update once the swap
|
|
1539
|
+
* is `swap.expired`, so the manager owns the local retry cadence.
|
|
1540
|
+
*/
|
|
1541
|
+
static REFUND_RETRY_DELAY_MS = 6e4;
|
|
1516
1542
|
swapProvider;
|
|
1517
1543
|
config;
|
|
1518
1544
|
// Event listeners storage (supports multiple listeners per event)
|
|
@@ -1531,6 +1557,11 @@ var SwapManager = class _SwapManager {
|
|
|
1531
1557
|
reconnectTimer = null;
|
|
1532
1558
|
initialPollTimer = null;
|
|
1533
1559
|
pollRetryTimers = /* @__PURE__ */ new Map();
|
|
1560
|
+
// Per-swap retry timers for chain refunds that left work undone
|
|
1561
|
+
// (refundArk returned `skipped > 0`). The swap is held in
|
|
1562
|
+
// `monitoredSwaps` past its terminal Boltz status until the local
|
|
1563
|
+
// refund completes or the manager stops.
|
|
1564
|
+
refundRetryTimers = /* @__PURE__ */ new Map();
|
|
1534
1565
|
// Per-swap counter of consecutive `SwapNotFoundError` responses from
|
|
1535
1566
|
// `getSwapStatus`. Reset on any successful poll. Once a swap reaches
|
|
1536
1567
|
// `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
|
|
@@ -1727,6 +1758,10 @@ var SwapManager = class _SwapManager {
|
|
|
1727
1758
|
clearTimeout(timer);
|
|
1728
1759
|
}
|
|
1729
1760
|
this.pollRetryTimers.clear();
|
|
1761
|
+
for (const timer of this.refundRetryTimers.values()) {
|
|
1762
|
+
clearTimeout(timer);
|
|
1763
|
+
}
|
|
1764
|
+
this.refundRetryTimers.clear();
|
|
1730
1765
|
this.notFoundCounts.clear();
|
|
1731
1766
|
}
|
|
1732
1767
|
/**
|
|
@@ -1783,6 +1818,11 @@ var SwapManager = class _SwapManager {
|
|
|
1783
1818
|
clearTimeout(retryTimer);
|
|
1784
1819
|
this.pollRetryTimers.delete(swapId);
|
|
1785
1820
|
}
|
|
1821
|
+
const refundRetryTimer = this.refundRetryTimers.get(swapId);
|
|
1822
|
+
if (refundRetryTimer) {
|
|
1823
|
+
clearTimeout(refundRetryTimer);
|
|
1824
|
+
this.refundRetryTimers.delete(swapId);
|
|
1825
|
+
}
|
|
1786
1826
|
this.notFoundCounts.delete(swapId);
|
|
1787
1827
|
logger.log(`Removed swap ${swapId} from monitoring`);
|
|
1788
1828
|
}
|
|
@@ -2054,16 +2094,58 @@ var SwapManager = class _SwapManager {
|
|
|
2054
2094
|
await this.executeAutonomousAction(swap);
|
|
2055
2095
|
}
|
|
2056
2096
|
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);
|
|
2097
|
+
if (this.refundRetryTimers.has(swap.id)) {
|
|
2098
|
+
return;
|
|
2063
2099
|
}
|
|
2064
|
-
this.
|
|
2100
|
+
this.finalizeMonitoredSwap(swap);
|
|
2065
2101
|
}
|
|
2066
2102
|
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Drop a swap from monitoring and emit the terminal completion event.
|
|
2105
|
+
* Shared between the on-status-update finalization path and the
|
|
2106
|
+
* refund-retry finalization path (used when a previously-deferred
|
|
2107
|
+
* chain refund has finished its remaining work).
|
|
2108
|
+
*/
|
|
2109
|
+
finalizeMonitoredSwap(swap) {
|
|
2110
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
2111
|
+
this.monitoredSwaps.delete(swap.id);
|
|
2112
|
+
this.swapSubscriptions.delete(swap.id);
|
|
2113
|
+
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
2114
|
+
if (retryTimer) {
|
|
2115
|
+
clearTimeout(retryTimer);
|
|
2116
|
+
this.pollRetryTimers.delete(swap.id);
|
|
2117
|
+
}
|
|
2118
|
+
const refundRetry = this.refundRetryTimers.get(swap.id);
|
|
2119
|
+
if (refundRetry) {
|
|
2120
|
+
clearTimeout(refundRetry);
|
|
2121
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2122
|
+
}
|
|
2123
|
+
this.swapCompletedListeners.forEach((listener) => listener(swap));
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Schedule another `executeAutonomousAction` run for a chain swap whose
|
|
2127
|
+
* refund left VTXOs deferred. After the retry completes, if no further
|
|
2128
|
+
* deferral was reported, finalize monitoring cleanup.
|
|
2129
|
+
*/
|
|
2130
|
+
scheduleRefundRetry(swap, delayMs) {
|
|
2131
|
+
const existing = this.refundRetryTimers.get(swap.id);
|
|
2132
|
+
if (existing) clearTimeout(existing);
|
|
2133
|
+
this.refundRetryTimers.set(
|
|
2134
|
+
swap.id,
|
|
2135
|
+
setTimeout(async () => {
|
|
2136
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2137
|
+
if (!this.isRunning) return;
|
|
2138
|
+
if (!this.monitoredSwaps.has(swap.id)) return;
|
|
2139
|
+
try {
|
|
2140
|
+
await this.executeAutonomousAction(swap);
|
|
2141
|
+
} finally {
|
|
2142
|
+
if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
|
|
2143
|
+
this.finalizeMonitoredSwap(swap);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}, delayMs)
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2067
2149
|
/**
|
|
2068
2150
|
* Execute autonomous action based on swap status
|
|
2069
2151
|
* Uses locking to prevent race conditions with manual operations
|
|
@@ -2117,10 +2199,27 @@ var SwapManager = class _SwapManager {
|
|
|
2117
2199
|
} else if (isChainRefundableStatus(swap.status)) {
|
|
2118
2200
|
if (swap.request.from === "ARK") {
|
|
2119
2201
|
logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
(
|
|
2123
|
-
|
|
2202
|
+
try {
|
|
2203
|
+
const outcome = await this.executeRefundArkAction(swap);
|
|
2204
|
+
if (outcome && outcome.skipped > 0) {
|
|
2205
|
+
logger.log(
|
|
2206
|
+
`Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
|
|
2207
|
+
);
|
|
2208
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
2209
|
+
}
|
|
2210
|
+
this.actionExecutedListeners.forEach(
|
|
2211
|
+
(listener) => listener(swap, "refundArk")
|
|
2212
|
+
);
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
logger.error(
|
|
2215
|
+
`Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
|
|
2216
|
+
error
|
|
2217
|
+
);
|
|
2218
|
+
this.swapFailedListeners.forEach(
|
|
2219
|
+
(listener) => listener(swap, error)
|
|
2220
|
+
);
|
|
2221
|
+
this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
|
|
2222
|
+
}
|
|
2124
2223
|
}
|
|
2125
2224
|
if (swap.request.from === "BTC") {
|
|
2126
2225
|
logger.warn(
|
|
@@ -2201,7 +2300,7 @@ var SwapManager = class _SwapManager {
|
|
|
2201
2300
|
logger.error("refundArk callback not set");
|
|
2202
2301
|
return;
|
|
2203
2302
|
}
|
|
2204
|
-
|
|
2303
|
+
return this.refundArkCallback(swap);
|
|
2205
2304
|
}
|
|
2206
2305
|
/**
|
|
2207
2306
|
* Execute sign server claim action for chain swap.
|
|
@@ -2301,9 +2400,7 @@ var SwapManager = class _SwapManager {
|
|
|
2301
2400
|
*/
|
|
2302
2401
|
async pollAllSwaps() {
|
|
2303
2402
|
if (this.monitoredSwaps.size === 0) return;
|
|
2304
|
-
const pollPromises = Array.from(this.monitoredSwaps.values()).map(
|
|
2305
|
-
(swap) => this.pollSingleSwap(swap)
|
|
2306
|
-
);
|
|
2403
|
+
const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
|
|
2307
2404
|
await Promise.allSettled(pollPromises);
|
|
2308
2405
|
}
|
|
2309
2406
|
async pollSingleSwap(swap) {
|
|
@@ -2356,6 +2453,7 @@ var SwapManager = class _SwapManager {
|
|
|
2356
2453
|
* Boltz endpoint).
|
|
2357
2454
|
*/
|
|
2358
2455
|
async handleSwapNotFound(swap) {
|
|
2456
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
2359
2457
|
const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
|
|
2360
2458
|
this.notFoundCounts.set(swap.id, count);
|
|
2361
2459
|
logger.warn(
|
|
@@ -2376,6 +2474,7 @@ var SwapManager = class _SwapManager {
|
|
|
2376
2474
|
* 404s without recovering anything.
|
|
2377
2475
|
*/
|
|
2378
2476
|
async markSwapAsUnknownToProvider(swap) {
|
|
2477
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
2379
2478
|
if (!this.monitoredSwaps.has(swap.id)) {
|
|
2380
2479
|
this.notFoundCounts.delete(swap.id);
|
|
2381
2480
|
return;
|
|
@@ -2388,6 +2487,11 @@ var SwapManager = class _SwapManager {
|
|
|
2388
2487
|
clearTimeout(retryTimer);
|
|
2389
2488
|
this.pollRetryTimers.delete(swap.id);
|
|
2390
2489
|
}
|
|
2490
|
+
const refundRetryTimer = this.refundRetryTimers.get(swap.id);
|
|
2491
|
+
if (refundRetryTimer) {
|
|
2492
|
+
clearTimeout(refundRetryTimer);
|
|
2493
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2494
|
+
}
|
|
2391
2495
|
this.notFoundCounts.delete(swap.id);
|
|
2392
2496
|
this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
|
|
2393
2497
|
const subscribers = this.swapSubscriptions.get(swap.id);
|
|
@@ -3145,7 +3249,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3145
3249
|
await this.claimBtc(swap);
|
|
3146
3250
|
},
|
|
3147
3251
|
refundArk: async (swap) => {
|
|
3148
|
-
|
|
3252
|
+
return this.refundArk(swap);
|
|
3149
3253
|
},
|
|
3150
3254
|
signServerClaim: async (swap) => {
|
|
3151
3255
|
await this.signCooperativeClaimForServer(swap);
|
|
@@ -3358,51 +3462,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3358
3462
|
throw new Error(
|
|
3359
3463
|
`Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
|
|
3360
3464
|
);
|
|
3361
|
-
let
|
|
3465
|
+
let unspentVtxos = [];
|
|
3466
|
+
let rawVtxos = [];
|
|
3362
3467
|
for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
|
|
3363
|
-
const
|
|
3468
|
+
const result = await this.indexerProvider.getVtxos({
|
|
3364
3469
|
scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
|
|
3365
3470
|
});
|
|
3366
|
-
|
|
3367
|
-
|
|
3471
|
+
rawVtxos = result.vtxos;
|
|
3472
|
+
unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
3473
|
+
if (unspentVtxos.length > 0) {
|
|
3368
3474
|
break;
|
|
3369
3475
|
}
|
|
3370
3476
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
3371
3477
|
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
3372
3478
|
}
|
|
3373
3479
|
}
|
|
3374
|
-
if (
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3480
|
+
if (unspentVtxos.length === 0) {
|
|
3481
|
+
if (rawVtxos.length === 0) {
|
|
3482
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
3483
|
+
}
|
|
3378
3484
|
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3379
3485
|
}
|
|
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
3486
|
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
vhtlcScript
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3487
|
+
const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
|
|
3488
|
+
const claimErrors = [];
|
|
3489
|
+
let usedOffchainClaim = false;
|
|
3490
|
+
for (const vtxo of unspentVtxos) {
|
|
3491
|
+
const input = {
|
|
3492
|
+
...vtxo,
|
|
3493
|
+
tapLeafScript: vhtlcScript.claim(),
|
|
3494
|
+
tapTree: vhtlcScript.encode()
|
|
3495
|
+
};
|
|
3496
|
+
const output = {
|
|
3497
|
+
amount: BigInt(vtxo.value),
|
|
3498
|
+
script: outputScript
|
|
3499
|
+
};
|
|
3500
|
+
try {
|
|
3501
|
+
if ((0, import_sdk8.isRecoverable)(vtxo)) {
|
|
3502
|
+
await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
|
|
3503
|
+
} else {
|
|
3504
|
+
await claimVHTLCwithOffchainTx(
|
|
3505
|
+
vhtlcIdentity,
|
|
3506
|
+
vhtlcScript,
|
|
3507
|
+
serverXOnly,
|
|
3508
|
+
input,
|
|
3509
|
+
output,
|
|
3510
|
+
arkInfo,
|
|
3511
|
+
this.arkProvider
|
|
3512
|
+
);
|
|
3513
|
+
usedOffchainClaim = true;
|
|
3514
|
+
}
|
|
3515
|
+
} catch (error) {
|
|
3516
|
+
claimErrors.push({ vtxo, error });
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
if (claimErrors.length > 0) {
|
|
3520
|
+
const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
|
|
3521
|
+
throw new Error(
|
|
3522
|
+
`Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
|
|
3403
3523
|
);
|
|
3404
|
-
finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
|
|
3405
3524
|
}
|
|
3525
|
+
const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
|
|
3406
3526
|
await updateReverseSwapStatus(
|
|
3407
3527
|
pendingSwap,
|
|
3408
3528
|
finalStatus,
|
|
@@ -3777,6 +3897,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3777
3897
|
if (boltzCallCount > 0) {
|
|
3778
3898
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
3779
3899
|
}
|
|
3900
|
+
boltzCallCount++;
|
|
3780
3901
|
await refundVHTLCwithOffchainTx(
|
|
3781
3902
|
pendingSwap.id,
|
|
3782
3903
|
this.wallet.identity,
|
|
@@ -3789,7 +3910,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3789
3910
|
arkInfo,
|
|
3790
3911
|
this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
|
|
3791
3912
|
);
|
|
3792
|
-
boltzCallCount++;
|
|
3793
3913
|
sweptCount++;
|
|
3794
3914
|
} catch (error) {
|
|
3795
3915
|
if (!(error instanceof BoltzRefundError)) {
|
|
@@ -4287,8 +4407,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4287
4407
|
await this.swapProvider.postBtcTransaction(claimTx.hex);
|
|
4288
4408
|
}
|
|
4289
4409
|
/**
|
|
4290
|
-
* When an ARK to BTC swap fails, refund
|
|
4410
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
4411
|
+
* swap's ARK lockup address.
|
|
4412
|
+
*
|
|
4413
|
+
* Path selection per VTXO:
|
|
4414
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
4415
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
4416
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
4417
|
+
*
|
|
4291
4418
|
* @param pendingSwap - The pending chain swap to refund.
|
|
4419
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
4420
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
4292
4421
|
*/
|
|
4293
4422
|
async refundArk(pendingSwap) {
|
|
4294
4423
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
@@ -4312,21 +4441,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4312
4441
|
"boltz",
|
|
4313
4442
|
pendingSwap.id
|
|
4314
4443
|
);
|
|
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
4444
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4331
4445
|
network: arkInfo.network,
|
|
4332
4446
|
preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
|
|
@@ -4342,37 +4456,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4342
4456
|
message: "Unable to claim: invalid VHTLC address"
|
|
4343
4457
|
});
|
|
4344
4458
|
}
|
|
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)
|
|
4459
|
+
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4460
|
+
scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
|
|
4461
|
+
});
|
|
4462
|
+
if (vtxos.length === 0) {
|
|
4463
|
+
throw new Error(
|
|
4464
|
+
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4369
4465
|
);
|
|
4370
4466
|
}
|
|
4467
|
+
const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
4468
|
+
if (unspentVtxos.length === 0) {
|
|
4469
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4470
|
+
}
|
|
4471
|
+
const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
|
|
4472
|
+
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
4473
|
+
const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
|
|
4474
|
+
let boltzCallCount = 0;
|
|
4475
|
+
let sweptCount = 0;
|
|
4476
|
+
let skippedCount = 0;
|
|
4477
|
+
for (const vtxo of unspentVtxos) {
|
|
4478
|
+
const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
|
|
4479
|
+
const output = {
|
|
4480
|
+
amount: BigInt(vtxo.value),
|
|
4481
|
+
script: outputScript
|
|
4482
|
+
};
|
|
4483
|
+
if (isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4484
|
+
const input2 = {
|
|
4485
|
+
...vtxo,
|
|
4486
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4487
|
+
tapTree: vhtlcScript.encode()
|
|
4488
|
+
};
|
|
4489
|
+
await this.joinBatch(
|
|
4490
|
+
this.wallet.identity,
|
|
4491
|
+
input2,
|
|
4492
|
+
output,
|
|
4493
|
+
arkInfo,
|
|
4494
|
+
isRecoverableVtxo
|
|
4495
|
+
);
|
|
4496
|
+
sweptCount++;
|
|
4497
|
+
continue;
|
|
4498
|
+
}
|
|
4499
|
+
if (isRecoverableVtxo) {
|
|
4500
|
+
logger.error(
|
|
4501
|
+
`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.`
|
|
4502
|
+
);
|
|
4503
|
+
skippedCount++;
|
|
4504
|
+
continue;
|
|
4505
|
+
}
|
|
4506
|
+
const input = {
|
|
4507
|
+
...vtxo,
|
|
4508
|
+
tapLeafScript: vhtlcScript.refund(),
|
|
4509
|
+
tapTree: vhtlcScript.encode()
|
|
4510
|
+
};
|
|
4511
|
+
try {
|
|
4512
|
+
if (boltzCallCount > 0) {
|
|
4513
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
4514
|
+
}
|
|
4515
|
+
boltzCallCount++;
|
|
4516
|
+
await refundVHTLCwithOffchainTx(
|
|
4517
|
+
pendingSwap.id,
|
|
4518
|
+
this.wallet.identity,
|
|
4519
|
+
this.arkProvider,
|
|
4520
|
+
boltzXOnlyPublicKey,
|
|
4521
|
+
ourXOnlyPublicKey,
|
|
4522
|
+
serverXOnlyPublicKey,
|
|
4523
|
+
input,
|
|
4524
|
+
output,
|
|
4525
|
+
arkInfo,
|
|
4526
|
+
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4527
|
+
);
|
|
4528
|
+
sweptCount++;
|
|
4529
|
+
} catch (error) {
|
|
4530
|
+
if (!(error instanceof BoltzRefundError)) {
|
|
4531
|
+
throw error;
|
|
4532
|
+
}
|
|
4533
|
+
if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4534
|
+
logger.error(
|
|
4535
|
+
`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.`
|
|
4536
|
+
);
|
|
4537
|
+
skippedCount++;
|
|
4538
|
+
continue;
|
|
4539
|
+
}
|
|
4540
|
+
logger.warn(
|
|
4541
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
|
|
4542
|
+
);
|
|
4543
|
+
const fallbackInput = {
|
|
4544
|
+
...vtxo,
|
|
4545
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4546
|
+
tapTree: vhtlcScript.encode()
|
|
4547
|
+
};
|
|
4548
|
+
await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
|
|
4549
|
+
sweptCount++;
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4371
4552
|
const finalStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4372
4553
|
await this.savePendingChainSwap({
|
|
4373
4554
|
...pendingSwap,
|
|
4374
4555
|
status: finalStatus.status
|
|
4375
4556
|
});
|
|
4557
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
4376
4558
|
}
|
|
4377
4559
|
// =========================================================================
|
|
4378
4560
|
// Chain swaps: BTC -> ARK
|
|
@@ -4793,7 +4975,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4793
4975
|
this.validateQuoteOptions(options);
|
|
4794
4976
|
const floor = await this.resolveQuoteFloor(swapId, options);
|
|
4795
4977
|
const slippageBps = options?.maxSlippageBps ?? 0;
|
|
4796
|
-
|
|
4978
|
+
const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
|
|
4979
|
+
if (effectiveFloor < 1) {
|
|
4980
|
+
throw new TypeError(
|
|
4981
|
+
`Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
|
|
4982
|
+
);
|
|
4983
|
+
}
|
|
4984
|
+
return effectiveFloor;
|
|
4797
4985
|
}
|
|
4798
4986
|
async resolveQuoteFloor(swapId, options) {
|
|
4799
4987
|
if (options?.minAcceptableAmount !== void 0) {
|
|
@@ -4829,7 +5017,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4829
5017
|
}
|
|
4830
5018
|
}
|
|
4831
5019
|
validateQuote(amount, effectiveFloor) {
|
|
4832
|
-
if (!(amount
|
|
5020
|
+
if (!Number.isSafeInteger(amount)) {
|
|
5021
|
+
throw new QuoteRejectedError({
|
|
5022
|
+
reason: "non_safe_integer",
|
|
5023
|
+
quotedAmount: amount
|
|
5024
|
+
});
|
|
5025
|
+
}
|
|
5026
|
+
if (amount <= 0) {
|
|
4833
5027
|
throw new QuoteRejectedError({
|
|
4834
5028
|
reason: "non_positive",
|
|
4835
5029
|
quotedAmount: amount
|
|
@@ -5012,20 +5206,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5012
5206
|
onchainAmount: amount,
|
|
5013
5207
|
lockupAddress,
|
|
5014
5208
|
refundPublicKey: serverPublicKey,
|
|
5015
|
-
timeoutBlockHeights: timeoutBlockHeights
|
|
5016
|
-
refund: extractTimeLockFromLeafOutput(
|
|
5017
|
-
tree.refundWithoutBoltzLeaf?.output ?? ""
|
|
5018
|
-
),
|
|
5019
|
-
unilateralClaim: extractTimeLockFromLeafOutput(
|
|
5020
|
-
tree.unilateralClaimLeaf?.output ?? ""
|
|
5021
|
-
),
|
|
5022
|
-
unilateralRefund: extractTimeLockFromLeafOutput(
|
|
5023
|
-
tree.unilateralRefundLeaf?.output ?? ""
|
|
5024
|
-
),
|
|
5025
|
-
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
5026
|
-
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
5027
|
-
)
|
|
5028
|
-
}
|
|
5209
|
+
timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
5029
5210
|
},
|
|
5030
5211
|
status,
|
|
5031
5212
|
type: "reverse",
|
|
@@ -5058,26 +5239,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5058
5239
|
address: lockupAddress,
|
|
5059
5240
|
expectedAmount: amount,
|
|
5060
5241
|
claimPublicKey: serverPublicKey,
|
|
5061
|
-
timeoutBlockHeights: timeoutBlockHeights
|
|
5062
|
-
refund: extractTimeLockFromLeafOutput(
|
|
5063
|
-
tree.refundWithoutBoltzLeaf?.output ?? ""
|
|
5064
|
-
),
|
|
5065
|
-
unilateralClaim: extractTimeLockFromLeafOutput(
|
|
5066
|
-
tree.unilateralClaimLeaf?.output ?? ""
|
|
5067
|
-
),
|
|
5068
|
-
unilateralRefund: extractTimeLockFromLeafOutput(
|
|
5069
|
-
tree.unilateralRefundLeaf?.output ?? ""
|
|
5070
|
-
),
|
|
5071
|
-
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
5072
|
-
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
5073
|
-
)
|
|
5074
|
-
}
|
|
5242
|
+
timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
5075
5243
|
}
|
|
5076
5244
|
});
|
|
5077
5245
|
} else if (isRestoredChainSwap(swap)) {
|
|
5078
5246
|
const refundDetails = swap.refundDetails;
|
|
5079
5247
|
if (!refundDetails) continue;
|
|
5080
|
-
const {
|
|
5248
|
+
const {
|
|
5249
|
+
amount,
|
|
5250
|
+
lockupAddress,
|
|
5251
|
+
serverPublicKey,
|
|
5252
|
+
timeoutBlockHeight,
|
|
5253
|
+
tree,
|
|
5254
|
+
timeoutBlockHeights
|
|
5255
|
+
} = refundDetails;
|
|
5081
5256
|
chainSwaps.push({
|
|
5082
5257
|
id,
|
|
5083
5258
|
type: "chain",
|
|
@@ -5103,7 +5278,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5103
5278
|
amount,
|
|
5104
5279
|
lockupAddress,
|
|
5105
5280
|
serverPublicKey,
|
|
5106
|
-
timeoutBlockHeight
|
|
5281
|
+
timeoutBlockHeight,
|
|
5282
|
+
timeouts: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
5107
5283
|
}
|
|
5108
5284
|
}
|
|
5109
5285
|
});
|
|
@@ -5473,9 +5649,14 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
|
|
|
5473
5649
|
case "CLAIM_BTC":
|
|
5474
5650
|
await this.handler.claimBtc(message.payload);
|
|
5475
5651
|
return this.tagged({ id, type: "BTC_CLAIM_EXECUTED" });
|
|
5476
|
-
case "REFUND_ARK":
|
|
5477
|
-
await this.handler.refundArk(message.payload);
|
|
5478
|
-
return this.tagged({
|
|
5652
|
+
case "REFUND_ARK": {
|
|
5653
|
+
const outcome = await this.handler.refundArk(message.payload);
|
|
5654
|
+
return this.tagged({
|
|
5655
|
+
id,
|
|
5656
|
+
type: "ARK_REFUND_EXECUTED",
|
|
5657
|
+
payload: outcome
|
|
5658
|
+
});
|
|
5659
|
+
}
|
|
5479
5660
|
case "SIGN_SERVER_CLAIM":
|
|
5480
5661
|
await this.handler.signCooperativeClaimForServer(message.payload);
|
|
5481
5662
|
return this.tagged({ id, type: "SERVER_CLAIM_SIGNED" });
|
|
@@ -6150,12 +6331,13 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
|
|
|
6150
6331
|
});
|
|
6151
6332
|
}
|
|
6152
6333
|
async refundArk(pendingSwap) {
|
|
6153
|
-
await this.sendMessage({
|
|
6334
|
+
const res = await this.sendMessage({
|
|
6154
6335
|
id: (0, import_sdk10.getRandomId)(),
|
|
6155
6336
|
tag: this.messageTag,
|
|
6156
6337
|
type: "REFUND_ARK",
|
|
6157
6338
|
payload: pendingSwap
|
|
6158
6339
|
});
|
|
6340
|
+
return res.payload;
|
|
6159
6341
|
}
|
|
6160
6342
|
async signCooperativeClaimForServer(pendingSwap) {
|
|
6161
6343
|
await this.sendMessage({
|
|
@@ -6565,6 +6747,30 @@ async function getContractCollection(storage, contractType) {
|
|
|
6565
6747
|
);
|
|
6566
6748
|
}
|
|
6567
6749
|
}
|
|
6750
|
+
|
|
6751
|
+
// src/repositories/inMemory/swap-repository.ts
|
|
6752
|
+
var InMemorySwapRepository = class {
|
|
6753
|
+
version = 1;
|
|
6754
|
+
swaps = /* @__PURE__ */ new Map();
|
|
6755
|
+
async saveSwap(swap) {
|
|
6756
|
+
this.swaps.set(swap.id, swap);
|
|
6757
|
+
}
|
|
6758
|
+
async deleteSwap(id) {
|
|
6759
|
+
this.swaps.delete(id);
|
|
6760
|
+
}
|
|
6761
|
+
async getAllSwaps(filter) {
|
|
6762
|
+
const swaps = [...this.swaps.values()];
|
|
6763
|
+
if (!filter || Object.keys(filter).length === 0) return swaps;
|
|
6764
|
+
const filtered = applySwapsFilter(swaps, filter);
|
|
6765
|
+
return applyCreatedAtOrder(filtered, filter);
|
|
6766
|
+
}
|
|
6767
|
+
async clear() {
|
|
6768
|
+
this.swaps.clear();
|
|
6769
|
+
}
|
|
6770
|
+
async [Symbol.asyncDispose]() {
|
|
6771
|
+
await this.clear();
|
|
6772
|
+
}
|
|
6773
|
+
};
|
|
6568
6774
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6569
6775
|
0 && (module.exports = {
|
|
6570
6776
|
ArkadeLightningMessageHandler,
|
|
@@ -6572,6 +6778,7 @@ async function getContractCollection(storage, contractType) {
|
|
|
6572
6778
|
ArkadeSwapsMessageHandler,
|
|
6573
6779
|
BoltzRefundError,
|
|
6574
6780
|
BoltzSwapProvider,
|
|
6781
|
+
InMemorySwapRepository,
|
|
6575
6782
|
IndexedDbSwapRepository,
|
|
6576
6783
|
InsufficientFundsError,
|
|
6577
6784
|
InvoiceExpiredError,
|