@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
|
@@ -128,6 +128,8 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
|
|
|
128
128
|
return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
|
|
129
129
|
case "non_positive":
|
|
130
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`;
|
|
131
133
|
case "no_baseline":
|
|
132
134
|
return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
|
|
133
135
|
}
|
|
@@ -181,6 +183,7 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
|
|
|
181
183
|
message
|
|
182
184
|
});
|
|
183
185
|
case "non_positive":
|
|
186
|
+
case "non_safe_integer":
|
|
184
187
|
if (quotedAmount === null) return null;
|
|
185
188
|
return new _QuoteRejectedError({
|
|
186
189
|
reason,
|
|
@@ -196,6 +199,7 @@ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
|
|
|
196
199
|
var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
|
|
197
200
|
"below_floor",
|
|
198
201
|
"non_positive",
|
|
202
|
+
"non_safe_integer",
|
|
199
203
|
"no_baseline"
|
|
200
204
|
]);
|
|
201
205
|
var BoltzRefundError = class extends Error {
|
|
@@ -1041,6 +1045,13 @@ var SwapManager = class _SwapManager {
|
|
|
1041
1045
|
* enough that a real "swap unknown to this provider" surfaces quickly.
|
|
1042
1046
|
*/
|
|
1043
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;
|
|
1044
1055
|
swapProvider;
|
|
1045
1056
|
config;
|
|
1046
1057
|
// Event listeners storage (supports multiple listeners per event)
|
|
@@ -1059,6 +1070,11 @@ var SwapManager = class _SwapManager {
|
|
|
1059
1070
|
reconnectTimer = null;
|
|
1060
1071
|
initialPollTimer = null;
|
|
1061
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();
|
|
1062
1078
|
// Per-swap counter of consecutive `SwapNotFoundError` responses from
|
|
1063
1079
|
// `getSwapStatus`. Reset on any successful poll. Once a swap reaches
|
|
1064
1080
|
// `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
|
|
@@ -1255,6 +1271,10 @@ var SwapManager = class _SwapManager {
|
|
|
1255
1271
|
clearTimeout(timer);
|
|
1256
1272
|
}
|
|
1257
1273
|
this.pollRetryTimers.clear();
|
|
1274
|
+
for (const timer of this.refundRetryTimers.values()) {
|
|
1275
|
+
clearTimeout(timer);
|
|
1276
|
+
}
|
|
1277
|
+
this.refundRetryTimers.clear();
|
|
1258
1278
|
this.notFoundCounts.clear();
|
|
1259
1279
|
}
|
|
1260
1280
|
/**
|
|
@@ -1311,6 +1331,11 @@ var SwapManager = class _SwapManager {
|
|
|
1311
1331
|
clearTimeout(retryTimer);
|
|
1312
1332
|
this.pollRetryTimers.delete(swapId);
|
|
1313
1333
|
}
|
|
1334
|
+
const refundRetryTimer = this.refundRetryTimers.get(swapId);
|
|
1335
|
+
if (refundRetryTimer) {
|
|
1336
|
+
clearTimeout(refundRetryTimer);
|
|
1337
|
+
this.refundRetryTimers.delete(swapId);
|
|
1338
|
+
}
|
|
1314
1339
|
this.notFoundCounts.delete(swapId);
|
|
1315
1340
|
logger.log(`Removed swap ${swapId} from monitoring`);
|
|
1316
1341
|
}
|
|
@@ -1582,15 +1607,57 @@ var SwapManager = class _SwapManager {
|
|
|
1582
1607
|
await this.executeAutonomousAction(swap);
|
|
1583
1608
|
}
|
|
1584
1609
|
if (this.isFinalStatus(swap)) {
|
|
1585
|
-
this.
|
|
1586
|
-
|
|
1587
|
-
const retryTimer = this.pollRetryTimers.get(swap.id);
|
|
1588
|
-
if (retryTimer) {
|
|
1589
|
-
clearTimeout(retryTimer);
|
|
1590
|
-
this.pollRetryTimers.delete(swap.id);
|
|
1610
|
+
if (this.refundRetryTimers.has(swap.id)) {
|
|
1611
|
+
return;
|
|
1591
1612
|
}
|
|
1592
|
-
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);
|
|
1630
|
+
}
|
|
1631
|
+
const refundRetry = this.refundRetryTimers.get(swap.id);
|
|
1632
|
+
if (refundRetry) {
|
|
1633
|
+
clearTimeout(refundRetry);
|
|
1634
|
+
this.refundRetryTimers.delete(swap.id);
|
|
1593
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
|
+
);
|
|
1594
1661
|
}
|
|
1595
1662
|
/**
|
|
1596
1663
|
* Execute autonomous action based on swap status
|
|
@@ -1645,10 +1712,27 @@ var SwapManager = class _SwapManager {
|
|
|
1645
1712
|
} else if (isChainRefundableStatus(swap.status)) {
|
|
1646
1713
|
if (swap.request.from === "ARK") {
|
|
1647
1714
|
logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
(
|
|
1651
|
-
|
|
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
|
+
}
|
|
1652
1736
|
}
|
|
1653
1737
|
if (swap.request.from === "BTC") {
|
|
1654
1738
|
logger.warn(
|
|
@@ -1729,7 +1813,7 @@ var SwapManager = class _SwapManager {
|
|
|
1729
1813
|
logger.error("refundArk callback not set");
|
|
1730
1814
|
return;
|
|
1731
1815
|
}
|
|
1732
|
-
|
|
1816
|
+
return this.refundArkCallback(swap);
|
|
1733
1817
|
}
|
|
1734
1818
|
/**
|
|
1735
1819
|
* Execute sign server claim action for chain swap.
|
|
@@ -1829,9 +1913,7 @@ var SwapManager = class _SwapManager {
|
|
|
1829
1913
|
*/
|
|
1830
1914
|
async pollAllSwaps() {
|
|
1831
1915
|
if (this.monitoredSwaps.size === 0) return;
|
|
1832
|
-
const pollPromises = Array.from(this.monitoredSwaps.values()).map(
|
|
1833
|
-
(swap) => this.pollSingleSwap(swap)
|
|
1834
|
-
);
|
|
1916
|
+
const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
|
|
1835
1917
|
await Promise.allSettled(pollPromises);
|
|
1836
1918
|
}
|
|
1837
1919
|
async pollSingleSwap(swap) {
|
|
@@ -1884,6 +1966,7 @@ var SwapManager = class _SwapManager {
|
|
|
1884
1966
|
* Boltz endpoint).
|
|
1885
1967
|
*/
|
|
1886
1968
|
async handleSwapNotFound(swap) {
|
|
1969
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
1887
1970
|
const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
|
|
1888
1971
|
this.notFoundCounts.set(swap.id, count);
|
|
1889
1972
|
logger.warn(
|
|
@@ -1904,6 +1987,7 @@ var SwapManager = class _SwapManager {
|
|
|
1904
1987
|
* 404s without recovering anything.
|
|
1905
1988
|
*/
|
|
1906
1989
|
async markSwapAsUnknownToProvider(swap) {
|
|
1990
|
+
if (this.refundRetryTimers.has(swap.id)) return;
|
|
1907
1991
|
if (!this.monitoredSwaps.has(swap.id)) {
|
|
1908
1992
|
this.notFoundCounts.delete(swap.id);
|
|
1909
1993
|
return;
|
|
@@ -1916,6 +2000,11 @@ var SwapManager = class _SwapManager {
|
|
|
1916
2000
|
clearTimeout(retryTimer);
|
|
1917
2001
|
this.pollRetryTimers.delete(swap.id);
|
|
1918
2002
|
}
|
|
2003
|
+
const refundRetryTimer = this.refundRetryTimers.get(swap.id);
|
|
2004
|
+
if (refundRetryTimer) {
|
|
2005
|
+
clearTimeout(refundRetryTimer);
|
|
2006
|
+
this.refundRetryTimers.delete(swap.id);
|
|
2007
|
+
}
|
|
1919
2008
|
this.notFoundCounts.delete(swap.id);
|
|
1920
2009
|
this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
|
|
1921
2010
|
const subscribers = this.swapSubscriptions.get(swap.id);
|
|
@@ -2581,6 +2670,20 @@ function extractInvoiceAmount(amountSats, fees) {
|
|
|
2581
2670
|
if (miner >= amountSats) return 0;
|
|
2582
2671
|
return Math.ceil((amountSats - miner) / (1 - percentage / 100));
|
|
2583
2672
|
}
|
|
2673
|
+
function resolveVhtlcTimeouts(tree, timeoutBlockHeights) {
|
|
2674
|
+
const resolved = timeoutBlockHeights ?? {
|
|
2675
|
+
refund: extractTimeLockFromLeafOutput(tree.refundWithoutBoltzLeaf?.output ?? ""),
|
|
2676
|
+
unilateralClaim: extractTimeLockFromLeafOutput(tree.unilateralClaimLeaf?.output ?? ""),
|
|
2677
|
+
unilateralRefund: extractTimeLockFromLeafOutput(tree.unilateralRefundLeaf?.output ?? ""),
|
|
2678
|
+
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
2679
|
+
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
2680
|
+
)
|
|
2681
|
+
};
|
|
2682
|
+
if (!resolved.refund || !resolved.unilateralClaim || !resolved.unilateralRefund || !resolved.unilateralRefundWithoutReceiver) {
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
return resolved;
|
|
2686
|
+
}
|
|
2584
2687
|
|
|
2585
2688
|
// src/utils/identity.ts
|
|
2586
2689
|
import { ConditionWitness, setArkPsbtField, Transaction as Transaction3 } from "@arkade-os/sdk";
|
|
@@ -3065,7 +3168,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3065
3168
|
await this.claimBtc(swap);
|
|
3066
3169
|
},
|
|
3067
3170
|
refundArk: async (swap) => {
|
|
3068
|
-
|
|
3171
|
+
return this.refundArk(swap);
|
|
3069
3172
|
},
|
|
3070
3173
|
signServerClaim: async (swap) => {
|
|
3071
3174
|
await this.signCooperativeClaimForServer(swap);
|
|
@@ -3278,51 +3381,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3278
3381
|
throw new Error(
|
|
3279
3382
|
`Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
|
|
3280
3383
|
);
|
|
3281
|
-
let
|
|
3384
|
+
let unspentVtxos = [];
|
|
3385
|
+
let rawVtxos = [];
|
|
3282
3386
|
for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
|
|
3283
|
-
const
|
|
3387
|
+
const result = await this.indexerProvider.getVtxos({
|
|
3284
3388
|
scripts: [hex8.encode(vhtlcScript.pkScript)]
|
|
3285
3389
|
});
|
|
3286
|
-
|
|
3287
|
-
|
|
3390
|
+
rawVtxos = result.vtxos;
|
|
3391
|
+
unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
3392
|
+
if (unspentVtxos.length > 0) {
|
|
3288
3393
|
break;
|
|
3289
3394
|
}
|
|
3290
3395
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
3291
3396
|
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
3292
3397
|
}
|
|
3293
3398
|
}
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3399
|
+
if (unspentVtxos.length === 0) {
|
|
3400
|
+
if (rawVtxos.length === 0) {
|
|
3401
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
3402
|
+
}
|
|
3298
3403
|
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3299
3404
|
}
|
|
3300
|
-
const input = {
|
|
3301
|
-
...vtxo,
|
|
3302
|
-
tapLeafScript: vhtlcScript.claim(),
|
|
3303
|
-
tapTree: vhtlcScript.encode()
|
|
3304
|
-
};
|
|
3305
|
-
const output = {
|
|
3306
|
-
amount: BigInt(vtxo.value),
|
|
3307
|
-
script: ArkAddress2.decode(address).pkScript
|
|
3308
|
-
};
|
|
3309
3405
|
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
vhtlcScript
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3406
|
+
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
3407
|
+
const claimErrors = [];
|
|
3408
|
+
let usedOffchainClaim = false;
|
|
3409
|
+
for (const vtxo of unspentVtxos) {
|
|
3410
|
+
const input = {
|
|
3411
|
+
...vtxo,
|
|
3412
|
+
tapLeafScript: vhtlcScript.claim(),
|
|
3413
|
+
tapTree: vhtlcScript.encode()
|
|
3414
|
+
};
|
|
3415
|
+
const output = {
|
|
3416
|
+
amount: BigInt(vtxo.value),
|
|
3417
|
+
script: outputScript
|
|
3418
|
+
};
|
|
3419
|
+
try {
|
|
3420
|
+
if (isRecoverable(vtxo)) {
|
|
3421
|
+
await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
|
|
3422
|
+
} else {
|
|
3423
|
+
await claimVHTLCwithOffchainTx(
|
|
3424
|
+
vhtlcIdentity,
|
|
3425
|
+
vhtlcScript,
|
|
3426
|
+
serverXOnly,
|
|
3427
|
+
input,
|
|
3428
|
+
output,
|
|
3429
|
+
arkInfo,
|
|
3430
|
+
this.arkProvider
|
|
3431
|
+
);
|
|
3432
|
+
usedOffchainClaim = true;
|
|
3433
|
+
}
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
claimErrors.push({ vtxo, error });
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
if (claimErrors.length > 0) {
|
|
3439
|
+
const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
|
|
3440
|
+
throw new Error(
|
|
3441
|
+
`Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
|
|
3323
3442
|
);
|
|
3324
|
-
finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
|
|
3325
3443
|
}
|
|
3444
|
+
const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
|
|
3326
3445
|
await updateReverseSwapStatus(
|
|
3327
3446
|
pendingSwap,
|
|
3328
3447
|
finalStatus,
|
|
@@ -3697,6 +3816,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3697
3816
|
if (boltzCallCount > 0) {
|
|
3698
3817
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
3699
3818
|
}
|
|
3819
|
+
boltzCallCount++;
|
|
3700
3820
|
await refundVHTLCwithOffchainTx(
|
|
3701
3821
|
pendingSwap.id,
|
|
3702
3822
|
this.wallet.identity,
|
|
@@ -3709,7 +3829,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3709
3829
|
arkInfo,
|
|
3710
3830
|
this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
|
|
3711
3831
|
);
|
|
3712
|
-
boltzCallCount++;
|
|
3713
3832
|
sweptCount++;
|
|
3714
3833
|
} catch (error) {
|
|
3715
3834
|
if (!(error instanceof BoltzRefundError)) {
|
|
@@ -4207,8 +4326,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4207
4326
|
await this.swapProvider.postBtcTransaction(claimTx.hex);
|
|
4208
4327
|
}
|
|
4209
4328
|
/**
|
|
4210
|
-
* When an ARK to BTC swap fails, refund
|
|
4329
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
4330
|
+
* swap's ARK lockup address.
|
|
4331
|
+
*
|
|
4332
|
+
* Path selection per VTXO:
|
|
4333
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
4334
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
4335
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
4336
|
+
*
|
|
4211
4337
|
* @param pendingSwap - The pending chain swap to refund.
|
|
4338
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
4339
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
4212
4340
|
*/
|
|
4213
4341
|
async refundArk(pendingSwap) {
|
|
4214
4342
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
@@ -4232,21 +4360,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4232
4360
|
"boltz",
|
|
4233
4361
|
pendingSwap.id
|
|
4234
4362
|
);
|
|
4235
|
-
const vhtlcPkScript = ArkAddress2.decode(
|
|
4236
|
-
pendingSwap.response.lockupDetails.lockupAddress
|
|
4237
|
-
).pkScript;
|
|
4238
|
-
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4239
|
-
scripts: [hex8.encode(vhtlcPkScript)]
|
|
4240
|
-
});
|
|
4241
|
-
if (vtxos.length === 0) {
|
|
4242
|
-
throw new Error(
|
|
4243
|
-
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4244
|
-
);
|
|
4245
|
-
}
|
|
4246
|
-
const vtxo = vtxos[0];
|
|
4247
|
-
if (vtxo.isSpent) {
|
|
4248
|
-
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4249
|
-
}
|
|
4250
4363
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4251
4364
|
network: arkInfo.network,
|
|
4252
4365
|
preimageHash: hex8.decode(pendingSwap.request.preimageHash),
|
|
@@ -4262,37 +4375,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4262
4375
|
message: "Unable to claim: invalid VHTLC address"
|
|
4263
4376
|
});
|
|
4264
4377
|
}
|
|
4265
|
-
const
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
const output = {
|
|
4272
|
-
amount: BigInt(vtxo.value),
|
|
4273
|
-
script: ArkAddress2.decode(address).pkScript
|
|
4274
|
-
};
|
|
4275
|
-
if (isRecoverableVtxo) {
|
|
4276
|
-
await this.joinBatch(this.wallet.identity, input, output, arkInfo);
|
|
4277
|
-
} else {
|
|
4278
|
-
await refundVHTLCwithOffchainTx(
|
|
4279
|
-
pendingSwap.id,
|
|
4280
|
-
this.wallet.identity,
|
|
4281
|
-
this.arkProvider,
|
|
4282
|
-
boltzXOnlyPublicKey,
|
|
4283
|
-
ourXOnlyPublicKey,
|
|
4284
|
-
serverXOnlyPublicKey,
|
|
4285
|
-
input,
|
|
4286
|
-
output,
|
|
4287
|
-
arkInfo,
|
|
4288
|
-
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4378
|
+
const { vtxos } = await this.indexerProvider.getVtxos({
|
|
4379
|
+
scripts: [hex8.encode(vhtlcScript.pkScript)]
|
|
4380
|
+
});
|
|
4381
|
+
if (vtxos.length === 0) {
|
|
4382
|
+
throw new Error(
|
|
4383
|
+
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
|
|
4289
4384
|
);
|
|
4290
4385
|
}
|
|
4386
|
+
const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
4387
|
+
if (unspentVtxos.length === 0) {
|
|
4388
|
+
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
4389
|
+
}
|
|
4390
|
+
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
4391
|
+
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
4392
|
+
const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
|
|
4393
|
+
let boltzCallCount = 0;
|
|
4394
|
+
let sweptCount = 0;
|
|
4395
|
+
let skippedCount = 0;
|
|
4396
|
+
for (const vtxo of unspentVtxos) {
|
|
4397
|
+
const isRecoverableVtxo = isRecoverable(vtxo);
|
|
4398
|
+
const output = {
|
|
4399
|
+
amount: BigInt(vtxo.value),
|
|
4400
|
+
script: outputScript
|
|
4401
|
+
};
|
|
4402
|
+
if (isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4403
|
+
const input2 = {
|
|
4404
|
+
...vtxo,
|
|
4405
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4406
|
+
tapTree: vhtlcScript.encode()
|
|
4407
|
+
};
|
|
4408
|
+
await this.joinBatch(
|
|
4409
|
+
this.wallet.identity,
|
|
4410
|
+
input2,
|
|
4411
|
+
output,
|
|
4412
|
+
arkInfo,
|
|
4413
|
+
isRecoverableVtxo
|
|
4414
|
+
);
|
|
4415
|
+
sweptCount++;
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4418
|
+
if (isRecoverableVtxo) {
|
|
4419
|
+
logger.error(
|
|
4420
|
+
`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.`
|
|
4421
|
+
);
|
|
4422
|
+
skippedCount++;
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4425
|
+
const input = {
|
|
4426
|
+
...vtxo,
|
|
4427
|
+
tapLeafScript: vhtlcScript.refund(),
|
|
4428
|
+
tapTree: vhtlcScript.encode()
|
|
4429
|
+
};
|
|
4430
|
+
try {
|
|
4431
|
+
if (boltzCallCount > 0) {
|
|
4432
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
4433
|
+
}
|
|
4434
|
+
boltzCallCount++;
|
|
4435
|
+
await refundVHTLCwithOffchainTx(
|
|
4436
|
+
pendingSwap.id,
|
|
4437
|
+
this.wallet.identity,
|
|
4438
|
+
this.arkProvider,
|
|
4439
|
+
boltzXOnlyPublicKey,
|
|
4440
|
+
ourXOnlyPublicKey,
|
|
4441
|
+
serverXOnlyPublicKey,
|
|
4442
|
+
input,
|
|
4443
|
+
output,
|
|
4444
|
+
arkInfo,
|
|
4445
|
+
this.swapProvider.refundChainSwap.bind(this.swapProvider)
|
|
4446
|
+
);
|
|
4447
|
+
sweptCount++;
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
if (!(error instanceof BoltzRefundError)) {
|
|
4450
|
+
throw error;
|
|
4451
|
+
}
|
|
4452
|
+
if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
|
|
4453
|
+
logger.error(
|
|
4454
|
+
`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.`
|
|
4455
|
+
);
|
|
4456
|
+
skippedCount++;
|
|
4457
|
+
continue;
|
|
4458
|
+
}
|
|
4459
|
+
logger.warn(
|
|
4460
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
|
|
4461
|
+
);
|
|
4462
|
+
const fallbackInput = {
|
|
4463
|
+
...vtxo,
|
|
4464
|
+
tapLeafScript: refundWithoutReceiverLeaf,
|
|
4465
|
+
tapTree: vhtlcScript.encode()
|
|
4466
|
+
};
|
|
4467
|
+
await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
|
|
4468
|
+
sweptCount++;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4291
4471
|
const finalStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4292
4472
|
await this.savePendingChainSwap({
|
|
4293
4473
|
...pendingSwap,
|
|
4294
4474
|
status: finalStatus.status
|
|
4295
4475
|
});
|
|
4476
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
4296
4477
|
}
|
|
4297
4478
|
// =========================================================================
|
|
4298
4479
|
// Chain swaps: BTC -> ARK
|
|
@@ -4713,7 +4894,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4713
4894
|
this.validateQuoteOptions(options);
|
|
4714
4895
|
const floor = await this.resolveQuoteFloor(swapId, options);
|
|
4715
4896
|
const slippageBps = options?.maxSlippageBps ?? 0;
|
|
4716
|
-
|
|
4897
|
+
const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
|
|
4898
|
+
if (effectiveFloor < 1) {
|
|
4899
|
+
throw new TypeError(
|
|
4900
|
+
`Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
|
|
4901
|
+
);
|
|
4902
|
+
}
|
|
4903
|
+
return effectiveFloor;
|
|
4717
4904
|
}
|
|
4718
4905
|
async resolveQuoteFloor(swapId, options) {
|
|
4719
4906
|
if (options?.minAcceptableAmount !== void 0) {
|
|
@@ -4749,7 +4936,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4749
4936
|
}
|
|
4750
4937
|
}
|
|
4751
4938
|
validateQuote(amount, effectiveFloor) {
|
|
4752
|
-
if (!(amount
|
|
4939
|
+
if (!Number.isSafeInteger(amount)) {
|
|
4940
|
+
throw new QuoteRejectedError({
|
|
4941
|
+
reason: "non_safe_integer",
|
|
4942
|
+
quotedAmount: amount
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
if (amount <= 0) {
|
|
4753
4946
|
throw new QuoteRejectedError({
|
|
4754
4947
|
reason: "non_positive",
|
|
4755
4948
|
quotedAmount: amount
|
|
@@ -4932,20 +5125,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4932
5125
|
onchainAmount: amount,
|
|
4933
5126
|
lockupAddress,
|
|
4934
5127
|
refundPublicKey: serverPublicKey,
|
|
4935
|
-
timeoutBlockHeights: timeoutBlockHeights
|
|
4936
|
-
refund: extractTimeLockFromLeafOutput(
|
|
4937
|
-
tree.refundWithoutBoltzLeaf?.output ?? ""
|
|
4938
|
-
),
|
|
4939
|
-
unilateralClaim: extractTimeLockFromLeafOutput(
|
|
4940
|
-
tree.unilateralClaimLeaf?.output ?? ""
|
|
4941
|
-
),
|
|
4942
|
-
unilateralRefund: extractTimeLockFromLeafOutput(
|
|
4943
|
-
tree.unilateralRefundLeaf?.output ?? ""
|
|
4944
|
-
),
|
|
4945
|
-
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
4946
|
-
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
4947
|
-
)
|
|
4948
|
-
}
|
|
5128
|
+
timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
4949
5129
|
},
|
|
4950
5130
|
status,
|
|
4951
5131
|
type: "reverse",
|
|
@@ -4978,26 +5158,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4978
5158
|
address: lockupAddress,
|
|
4979
5159
|
expectedAmount: amount,
|
|
4980
5160
|
claimPublicKey: serverPublicKey,
|
|
4981
|
-
timeoutBlockHeights: timeoutBlockHeights
|
|
4982
|
-
refund: extractTimeLockFromLeafOutput(
|
|
4983
|
-
tree.refundWithoutBoltzLeaf?.output ?? ""
|
|
4984
|
-
),
|
|
4985
|
-
unilateralClaim: extractTimeLockFromLeafOutput(
|
|
4986
|
-
tree.unilateralClaimLeaf?.output ?? ""
|
|
4987
|
-
),
|
|
4988
|
-
unilateralRefund: extractTimeLockFromLeafOutput(
|
|
4989
|
-
tree.unilateralRefundLeaf?.output ?? ""
|
|
4990
|
-
),
|
|
4991
|
-
unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
|
|
4992
|
-
tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
|
|
4993
|
-
)
|
|
4994
|
-
}
|
|
5161
|
+
timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
4995
5162
|
}
|
|
4996
5163
|
});
|
|
4997
5164
|
} else if (isRestoredChainSwap(swap)) {
|
|
4998
5165
|
const refundDetails = swap.refundDetails;
|
|
4999
5166
|
if (!refundDetails) continue;
|
|
5000
|
-
const {
|
|
5167
|
+
const {
|
|
5168
|
+
amount,
|
|
5169
|
+
lockupAddress,
|
|
5170
|
+
serverPublicKey,
|
|
5171
|
+
timeoutBlockHeight,
|
|
5172
|
+
tree,
|
|
5173
|
+
timeoutBlockHeights
|
|
5174
|
+
} = refundDetails;
|
|
5001
5175
|
chainSwaps.push({
|
|
5002
5176
|
id,
|
|
5003
5177
|
type: "chain",
|
|
@@ -5023,7 +5197,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
5023
5197
|
amount,
|
|
5024
5198
|
lockupAddress,
|
|
5025
5199
|
serverPublicKey,
|
|
5026
|
-
timeoutBlockHeight
|
|
5200
|
+
timeoutBlockHeight,
|
|
5201
|
+
timeouts: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
|
|
5027
5202
|
}
|
|
5028
5203
|
}
|
|
5029
5204
|
});
|