@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { r as BoltzSwapProvider, x as SwapManager, n as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, o as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, m as ChainArkRefundOutcome, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types--axEWA8c.js';
|
|
3
3
|
import { TransactionOutput } from '@scure/btc-signer/psbt.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -267,10 +267,19 @@ declare class ArkadeSwaps {
|
|
|
267
267
|
*/
|
|
268
268
|
claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
|
|
269
269
|
/**
|
|
270
|
-
* When an ARK to BTC swap fails, refund
|
|
270
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
271
|
+
* swap's ARK lockup address.
|
|
272
|
+
*
|
|
273
|
+
* Path selection per VTXO:
|
|
274
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
275
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
276
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
277
|
+
*
|
|
271
278
|
* @param pendingSwap - The pending chain swap to refund.
|
|
279
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
280
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
272
281
|
*/
|
|
273
|
-
refundArk(pendingSwap: BoltzChainSwap): Promise<
|
|
282
|
+
refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
|
|
274
283
|
/**
|
|
275
284
|
* Creates a chain swap from BTC to ARK.
|
|
276
285
|
* @param args.feeSatsPerByte - Fee rate for BTC transactions (default: 1).
|
|
@@ -515,7 +524,7 @@ interface IArkadeSwaps extends AsyncDisposable {
|
|
|
515
524
|
txid: string;
|
|
516
525
|
}>;
|
|
517
526
|
claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
|
|
518
|
-
refundArk(pendingSwap: BoltzChainSwap): Promise<
|
|
527
|
+
refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
|
|
519
528
|
btcToArk(args: {
|
|
520
529
|
feeSatsPerByte?: number;
|
|
521
530
|
senderLockAmount?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { r as BoltzSwapProvider, x as SwapManager, n as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, o as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, m as ChainArkRefundOutcome, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types--axEWA8c.cjs';
|
|
3
3
|
import { TransactionOutput } from '@scure/btc-signer/psbt.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -267,10 +267,19 @@ declare class ArkadeSwaps {
|
|
|
267
267
|
*/
|
|
268
268
|
claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
|
|
269
269
|
/**
|
|
270
|
-
* When an ARK to BTC swap fails, refund
|
|
270
|
+
* When an ARK to BTC swap fails, refund every unspent VTXO at the chain
|
|
271
|
+
* swap's ARK lockup address.
|
|
272
|
+
*
|
|
273
|
+
* Path selection per VTXO:
|
|
274
|
+
* - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
|
|
275
|
+
* - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
|
|
276
|
+
* - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
|
|
277
|
+
*
|
|
271
278
|
* @param pendingSwap - The pending chain swap to refund.
|
|
279
|
+
* @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
|
|
280
|
+
* the call was a no-op — callers should retry after CLTV.
|
|
272
281
|
*/
|
|
273
|
-
refundArk(pendingSwap: BoltzChainSwap): Promise<
|
|
282
|
+
refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
|
|
274
283
|
/**
|
|
275
284
|
* Creates a chain swap from BTC to ARK.
|
|
276
285
|
* @param args.feeSatsPerByte - Fee rate for BTC transactions (default: 1).
|
|
@@ -515,7 +524,7 @@ interface IArkadeSwaps extends AsyncDisposable {
|
|
|
515
524
|
txid: string;
|
|
516
525
|
}>;
|
|
517
526
|
claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
|
|
518
|
-
refundArk(pendingSwap: BoltzChainSwap): Promise<
|
|
527
|
+
refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
|
|
519
528
|
btcToArk(args: {
|
|
520
529
|
feeSatsPerByte?: number;
|
|
521
530
|
senderLockAmount?: number;
|
|
@@ -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);
|
|
@@ -3065,7 +3154,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3065
3154
|
await this.claimBtc(swap);
|
|
3066
3155
|
},
|
|
3067
3156
|
refundArk: async (swap) => {
|
|
3068
|
-
|
|
3157
|
+
return this.refundArk(swap);
|
|
3069
3158
|
},
|
|
3070
3159
|
signServerClaim: async (swap) => {
|
|
3071
3160
|
await this.signCooperativeClaimForServer(swap);
|
|
@@ -3278,51 +3367,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3278
3367
|
throw new Error(
|
|
3279
3368
|
`Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
|
|
3280
3369
|
);
|
|
3281
|
-
let
|
|
3370
|
+
let unspentVtxos = [];
|
|
3371
|
+
let rawVtxos = [];
|
|
3282
3372
|
for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
|
|
3283
|
-
const
|
|
3373
|
+
const result = await this.indexerProvider.getVtxos({
|
|
3284
3374
|
scripts: [hex8.encode(vhtlcScript.pkScript)]
|
|
3285
3375
|
});
|
|
3286
|
-
|
|
3287
|
-
|
|
3376
|
+
rawVtxos = result.vtxos;
|
|
3377
|
+
unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
|
|
3378
|
+
if (unspentVtxos.length > 0) {
|
|
3288
3379
|
break;
|
|
3289
3380
|
}
|
|
3290
3381
|
if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
|
|
3291
3382
|
await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
|
|
3292
3383
|
}
|
|
3293
3384
|
}
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3385
|
+
if (unspentVtxos.length === 0) {
|
|
3386
|
+
if (rawVtxos.length === 0) {
|
|
3387
|
+
throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
|
|
3388
|
+
}
|
|
3298
3389
|
throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
|
|
3299
3390
|
}
|
|
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
3391
|
const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
vhtlcScript
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
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}`
|
|
3323
3428
|
);
|
|
3324
|
-
finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
|
|
3325
3429
|
}
|
|
3430
|
+
const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
|
|
3326
3431
|
await updateReverseSwapStatus(
|
|
3327
3432
|
pendingSwap,
|
|
3328
3433
|
finalStatus,
|
|
@@ -3697,6 +3802,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3697
3802
|
if (boltzCallCount > 0) {
|
|
3698
3803
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
3699
3804
|
}
|
|
3805
|
+
boltzCallCount++;
|
|
3700
3806
|
await refundVHTLCwithOffchainTx(
|
|
3701
3807
|
pendingSwap.id,
|
|
3702
3808
|
this.wallet.identity,
|
|
@@ -3709,7 +3815,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3709
3815
|
arkInfo,
|
|
3710
3816
|
this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
|
|
3711
3817
|
);
|
|
3712
|
-
boltzCallCount++;
|
|
3713
3818
|
sweptCount++;
|
|
3714
3819
|
} catch (error) {
|
|
3715
3820
|
if (!(error instanceof BoltzRefundError)) {
|
|
@@ -4207,8 +4312,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4207
4312
|
await this.swapProvider.postBtcTransaction(claimTx.hex);
|
|
4208
4313
|
}
|
|
4209
4314
|
/**
|
|
4210
|
-
* 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
|
+
*
|
|
4211
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.
|
|
4212
4326
|
*/
|
|
4213
4327
|
async refundArk(pendingSwap) {
|
|
4214
4328
|
if (!pendingSwap.response.lockupDetails.serverPublicKey)
|
|
@@ -4232,21 +4346,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4232
4346
|
"boltz",
|
|
4233
4347
|
pendingSwap.id
|
|
4234
4348
|
);
|
|
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
4349
|
const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
|
|
4251
4350
|
network: arkInfo.network,
|
|
4252
4351
|
preimageHash: hex8.decode(pendingSwap.request.preimageHash),
|
|
@@ -4262,37 +4361,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4262
4361
|
message: "Unable to claim: invalid VHTLC address"
|
|
4263
4362
|
});
|
|
4264
4363
|
}
|
|
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)
|
|
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}`
|
|
4289
4370
|
);
|
|
4290
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
|
+
}
|
|
4291
4457
|
const finalStatus = await this.getSwapStatus(pendingSwap.id);
|
|
4292
4458
|
await this.savePendingChainSwap({
|
|
4293
4459
|
...pendingSwap,
|
|
4294
4460
|
status: finalStatus.status
|
|
4295
4461
|
});
|
|
4462
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
4296
4463
|
}
|
|
4297
4464
|
// =========================================================================
|
|
4298
4465
|
// Chain swaps: BTC -> ARK
|
|
@@ -4713,7 +4880,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4713
4880
|
this.validateQuoteOptions(options);
|
|
4714
4881
|
const floor = await this.resolveQuoteFloor(swapId, options);
|
|
4715
4882
|
const slippageBps = options?.maxSlippageBps ?? 0;
|
|
4716
|
-
|
|
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;
|
|
4717
4890
|
}
|
|
4718
4891
|
async resolveQuoteFloor(swapId, options) {
|
|
4719
4892
|
if (options?.minAcceptableAmount !== void 0) {
|
|
@@ -4749,7 +4922,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4749
4922
|
}
|
|
4750
4923
|
}
|
|
4751
4924
|
validateQuote(amount, effectiveFloor) {
|
|
4752
|
-
if (!(amount
|
|
4925
|
+
if (!Number.isSafeInteger(amount)) {
|
|
4926
|
+
throw new QuoteRejectedError({
|
|
4927
|
+
reason: "non_safe_integer",
|
|
4928
|
+
quotedAmount: amount
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
if (amount <= 0) {
|
|
4753
4932
|
throw new QuoteRejectedError({
|
|
4754
4933
|
reason: "non_positive",
|
|
4755
4934
|
quotedAmount: amount
|