@arkade-os/boltz-swap 0.3.49 → 0.3.51

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.
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { r as BoltzSwapProvider, y as SwapManager, m as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, j as ArkToBtcResponse, a as BoltzChainSwap, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, F as FeesResponse, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-BKEkNZxK.js';
2
+ import { r as BoltzSwapProvider, y as SwapManager, m as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, j as ArkToBtcResponse, a as BoltzChainSwap, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, F as FeesResponse, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-Db4JSdjk.js';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -485,18 +485,33 @@ declare class ArkadeSwaps {
485
485
  * reclaimed by re-registering it into a batch.
486
486
  */
487
487
  private settleRefundWithoutReceiver;
488
+ /**
489
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
490
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
491
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
492
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
493
+ * already implement. Anything else propagates.
494
+ *
495
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
496
+ * which settle via a batch round.
497
+ *
498
+ * @returns `true` if settled, `false` if the server deferred it.
499
+ */
500
+ private trySettleRefundWithoutReceiver;
488
501
  /**
489
502
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
490
503
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
491
504
  * (chain). Path selection per VTXO:
492
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
493
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
505
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
506
+ * live VTXO, via a batch round for a swept one — see
507
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
508
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
494
509
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
495
510
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
496
511
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
497
512
  * locktime has since elapsed.
498
513
  *
499
- * @returns Counts of VTXOs swept vs. deferred.
514
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
500
515
  */
501
516
  private refundVtxos;
502
517
  /**
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { r as BoltzSwapProvider, y as SwapManager, m as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, j as ArkToBtcResponse, a as BoltzChainSwap, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, F as FeesResponse, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-BKEkNZxK.cjs';
2
+ import { r as BoltzSwapProvider, y as SwapManager, m as SwapRepository, q as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, j as ArkToBtcResponse, a as BoltzChainSwap, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, F as FeesResponse, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-Db4JSdjk.cjs';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -485,18 +485,33 @@ declare class ArkadeSwaps {
485
485
  * reclaimed by re-registering it into a batch.
486
486
  */
487
487
  private settleRefundWithoutReceiver;
488
+ /**
489
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
490
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
491
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
492
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
493
+ * already implement. Anything else propagates.
494
+ *
495
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
496
+ * which settle via a batch round.
497
+ *
498
+ * @returns `true` if settled, `false` if the server deferred it.
499
+ */
500
+ private trySettleRefundWithoutReceiver;
488
501
  /**
489
502
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
490
503
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
491
504
  * (chain). Path selection per VTXO:
492
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
493
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
505
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
506
+ * live VTXO, via a batch round for a swept one — see
507
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
508
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
494
509
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
495
510
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
496
511
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
497
512
  * locktime has since elapsed.
498
513
  *
499
- * @returns Counts of VTXOs swept vs. deferred.
514
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
500
515
  */
501
516
  private refundVtxos;
502
517
  /**
@@ -7,7 +7,7 @@ import {
7
7
  isSubmarineFinalStatus,
8
8
  isSubmarineSwapRefundable,
9
9
  logger
10
- } from "./chunk-VKHFXGKL.js";
10
+ } from "./chunk-CZTZTYZU.js";
11
11
 
12
12
  // src/expo/swapsPollProcessor.ts
13
13
  var SWAP_POLL_TASK_TYPE = "swap-poll";
@@ -208,6 +208,7 @@ var BoltzRefundError = class extends Error {
208
208
  this.cause = cause;
209
209
  this.name = "BoltzRefundError";
210
210
  }
211
+ cause;
211
212
  };
212
213
 
213
214
  // src/boltz-swap-provider.ts
@@ -1077,12 +1078,20 @@ var SwapManager = class _SwapManager {
1077
1078
  */
1078
1079
  static NOT_FOUND_THRESHOLD = 10;
1079
1080
  /**
1080
- * Delay between re-attempts of a chain refund that left VTXOs deferred
1081
- * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1082
- * has elapsed). Boltz won't send another status update once the swap
1083
- * is `swap.expired`, so the manager owns the local retry cadence.
1081
+ * Floor on the delay between re-attempts of a refund that left VTXOs
1082
+ * deferred, and the delay used when the outcome names no retry time. Boltz
1083
+ * won't send another status update once the swap is refundable-and-final,
1084
+ * so the manager owns the local retry cadence.
1084
1085
  */
1085
1086
  static REFUND_RETRY_DELAY_MS = 6e4;
1087
+ /**
1088
+ * Ceiling on that delay. A deferral can be hours out (a pre-CLTV VTXO waits
1089
+ * out the whole refund locktime), and `setTimeout` fires *immediately* past
1090
+ * its 32-bit millisecond range, so long waits are broken into re-armed
1091
+ * hops. Re-attempting costs an indexer lookup and re-defers, which also lets
1092
+ * the cadence recover from a suspended device or a clock jump.
1093
+ */
1094
+ static MAX_REFUND_RETRY_DELAY_MS = 60 * 6e4;
1086
1095
  swapProvider;
1087
1096
  config;
1088
1097
  // Event listeners storage (supports multiple listeners per event)
@@ -1101,10 +1110,9 @@ var SwapManager = class _SwapManager {
1101
1110
  reconnectTimer = null;
1102
1111
  initialPollTimer = null;
1103
1112
  pollRetryTimers = /* @__PURE__ */ new Map();
1104
- // Per-swap retry timers for chain refunds that left work undone
1105
- // (refundArk returned `skipped > 0`). The swap is held in
1106
- // `monitoredSwaps` past its terminal Boltz status until the local
1107
- // refund completes or the manager stops.
1113
+ // Per-swap retry timers for deferred refunds that left local work undone.
1114
+ // The swap is held in `monitoredSwaps` past its terminal Boltz status
1115
+ // until the local refund completes or the manager stops.
1108
1116
  refundRetryTimers = /* @__PURE__ */ new Map();
1109
1117
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1110
1118
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
@@ -1276,11 +1284,12 @@ var SwapManager = class _SwapManager {
1276
1284
  this.initialSwaps.set(swap.id, swap);
1277
1285
  }
1278
1286
  for (const swap of pendingSwaps) {
1279
- if (!this.isFinalStatus(swap)) {
1287
+ if (this.shouldMonitorOnStart(swap)) {
1280
1288
  this.monitoredSwaps.set(swap.id, swap);
1281
1289
  }
1282
1290
  }
1283
1291
  await this.tryConnectWebSocket();
1292
+ this.resumePersistedRefundRetries();
1284
1293
  await this.resumeActionableSwaps();
1285
1294
  }
1286
1295
  /**
@@ -1363,6 +1372,7 @@ var SwapManager = class _SwapManager {
1363
1372
  * Remove a swap from monitoring
1364
1373
  */
1365
1374
  async removeSwap(swapId) {
1375
+ const swap = this.monitoredSwaps.get(swapId);
1366
1376
  this.monitoredSwaps.delete(swapId);
1367
1377
  this.swapSubscriptions.delete(swapId);
1368
1378
  this.chainClaimPromises.delete(swapId);
@@ -1376,6 +1386,9 @@ var SwapManager = class _SwapManager {
1376
1386
  clearTimeout(refundRetryTimer);
1377
1387
  this.refundRetryTimers.delete(swapId);
1378
1388
  }
1389
+ if (swap) {
1390
+ await this.clearRefundRetry(swap);
1391
+ }
1379
1392
  this.notFoundCounts.delete(swapId);
1380
1393
  logger.log(`Removed swap ${swapId} from monitoring`);
1381
1394
  }
@@ -1714,12 +1727,69 @@ var SwapManager = class _SwapManager {
1714
1727
  }
1715
1728
  this.swapCompletedListeners.forEach((listener) => listener(swap));
1716
1729
  }
1730
+ static isRefundRetryableSwap(swap) {
1731
+ return isPendingSubmarineSwap(swap) || isPendingChainSwap(swap);
1732
+ }
1733
+ static hasPendingRefundRetry(swap) {
1734
+ if (!_SwapManager.isRefundRetryableSwap(swap)) return false;
1735
+ return swap.refundRetry?.pending === true && Number.isFinite(swap.refundRetry.nextRetryAt);
1736
+ }
1737
+ shouldMonitorOnStart(swap) {
1738
+ if (!this.isFinalStatus(swap)) return true;
1739
+ return this.config.enableAutoActions === true && _SwapManager.hasPendingRefundRetry(swap);
1740
+ }
1741
+ /**
1742
+ * How long to wait before re-attempting a refund, given the `retryAt`
1743
+ * (Unix seconds) the outcome reported. Clamped at both ends: see
1744
+ * {@link SwapManager.REFUND_RETRY_DELAY_MS} and
1745
+ * {@link SwapManager.MAX_REFUND_RETRY_DELAY_MS}.
1746
+ */
1747
+ static refundRetryDelayMs(retryAt) {
1748
+ if (retryAt === void 0) return _SwapManager.REFUND_RETRY_DELAY_MS;
1749
+ return Math.min(
1750
+ Math.max(retryAt * 1e3 - Date.now(), _SwapManager.REFUND_RETRY_DELAY_MS),
1751
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
1752
+ );
1753
+ }
1754
+ static persistedRefundRetryDelayMs(nextRetryAt) {
1755
+ return Math.min(
1756
+ Math.max(nextRetryAt * 1e3 - Date.now(), 0),
1757
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
1758
+ );
1759
+ }
1760
+ static nextRefundRetryAt(delayMs) {
1761
+ return Math.ceil((Date.now() + delayMs) / 1e3);
1762
+ }
1763
+ async clearRefundRetry(swap) {
1764
+ if (!_SwapManager.isRefundRetryableSwap(swap) || swap.refundRetry === void 0) return;
1765
+ delete swap.refundRetry;
1766
+ await this.saveSwap(swap);
1767
+ }
1768
+ resumePersistedRefundRetries() {
1769
+ if (!this.config.enableAutoActions) return;
1770
+ for (const swap of this.monitoredSwaps.values()) {
1771
+ if (!_SwapManager.hasPendingRefundRetry(swap)) continue;
1772
+ const delayMs = _SwapManager.persistedRefundRetryDelayMs(swap.refundRetry.nextRetryAt);
1773
+ logger.log(
1774
+ `Swap ${swap.id}: resuming deferred refund retry in ${Math.round(delayMs / 1e3)}s`
1775
+ );
1776
+ this.armRefundRetryTimer(swap, delayMs);
1777
+ }
1778
+ }
1717
1779
  /**
1718
- * Schedule another `executeAutonomousAction` run for a chain swap whose
1719
- * refund left VTXOs deferred. After the retry completes, if no further
1720
- * deferral was reported, finalize monitoring cleanup.
1780
+ * Schedule another `executeAutonomousAction` run for a swap whose refund
1781
+ * left VTXOs deferred. The pending retry and its next attempt time are saved
1782
+ * with the swap so stop/start and application restart can re-arm it.
1721
1783
  */
1722
- scheduleRefundRetry(swap, delayMs) {
1784
+ async scheduleRefundRetry(swap, delayMs) {
1785
+ swap.refundRetry = {
1786
+ pending: true,
1787
+ nextRetryAt: _SwapManager.nextRefundRetryAt(delayMs)
1788
+ };
1789
+ await this.saveSwap(swap);
1790
+ this.armRefundRetryTimer(swap, delayMs);
1791
+ }
1792
+ armRefundRetryTimer(swap, delayMs) {
1723
1793
  const existing = this.refundRetryTimers.get(swap.id);
1724
1794
  if (existing) clearTimeout(existing);
1725
1795
  this.refundRetryTimers.set(
@@ -1727,12 +1797,14 @@ var SwapManager = class _SwapManager {
1727
1797
  setTimeout(async () => {
1728
1798
  this.refundRetryTimers.delete(swap.id);
1729
1799
  if (!this.isRunning) return;
1730
- if (!this.monitoredSwaps.has(swap.id)) return;
1800
+ const current = this.monitoredSwaps.get(swap.id);
1801
+ if (!current) return;
1802
+ let ran = false;
1731
1803
  try {
1732
- await this.executeAutonomousAction(swap);
1804
+ ran = await this.executeAutonomousAction(current);
1733
1805
  } finally {
1734
- if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
1735
- this.finalizeMonitoredSwap(swap);
1806
+ if (ran && !this.refundRetryTimers.has(current.id) && this.isFinalStatus(current)) {
1807
+ this.finalizeMonitoredSwap(current);
1736
1808
  }
1737
1809
  }
1738
1810
  }, delayMs)
@@ -1741,11 +1813,16 @@ var SwapManager = class _SwapManager {
1741
1813
  /**
1742
1814
  * Execute autonomous action based on swap status
1743
1815
  * Uses locking to prevent race conditions with manual operations
1816
+ *
1817
+ * @returns `false` when another action for this swap already held the lock
1818
+ * and this call did nothing, `true` when it ran. Callers that finalize on
1819
+ * the result must not act on `false`: the in-flight action owns the swap's
1820
+ * outcome and will re-arm or finalize from its own result.
1744
1821
  */
1745
1822
  async executeAutonomousAction(swap) {
1746
1823
  if (this.swapsInProgress.has(swap.id)) {
1747
1824
  logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
1748
- return;
1825
+ return false;
1749
1826
  }
1750
1827
  try {
1751
1828
  this.swapsInProgress.add(swap.id);
@@ -1754,7 +1831,7 @@ var SwapManager = class _SwapManager {
1754
1831
  logger.log(
1755
1832
  `Skipping claim for swap ${swap.id}: missing preimage (restored swap)`
1756
1833
  );
1757
- return;
1834
+ return true;
1758
1835
  }
1759
1836
  if (isReverseClaimableStatus(swap.status)) {
1760
1837
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
@@ -1766,11 +1843,26 @@ var SwapManager = class _SwapManager {
1766
1843
  logger.log(
1767
1844
  `Skipping refund for swap ${swap.id}: missing invoice (restored swap)`
1768
1845
  );
1769
- return;
1846
+ return true;
1770
1847
  }
1771
1848
  if (isSubmarineRefundableStatus(swap.status)) {
1772
1849
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
1773
- await this.executeRefundAction(swap);
1850
+ let outcome;
1851
+ try {
1852
+ outcome = await this.executeRefundAction(swap);
1853
+ } catch (error) {
1854
+ await this.clearRefundRetry(swap);
1855
+ throw error;
1856
+ }
1857
+ if (outcome && outcome.skipped > 0) {
1858
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
1859
+ logger.log(
1860
+ `Submarine swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
1861
+ );
1862
+ await this.scheduleRefundRetry(swap, delayMs);
1863
+ } else if (outcome) {
1864
+ await this.clearRefundRetry(swap);
1865
+ }
1774
1866
  this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
1775
1867
  }
1776
1868
  } else if (isPendingChainSwap(swap)) {
@@ -1794,10 +1886,13 @@ var SwapManager = class _SwapManager {
1794
1886
  try {
1795
1887
  const outcome = await this.executeRefundArkAction(swap);
1796
1888
  if (outcome && outcome.skipped > 0) {
1889
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
1797
1890
  logger.log(
1798
- `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
1891
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
1799
1892
  );
1800
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
1893
+ await this.scheduleRefundRetry(swap, delayMs);
1894
+ } else if (outcome) {
1895
+ await this.clearRefundRetry(swap);
1801
1896
  }
1802
1897
  this.actionExecutedListeners.forEach(
1803
1898
  (listener) => listener(swap, "refundArk")
@@ -1810,7 +1905,7 @@ var SwapManager = class _SwapManager {
1810
1905
  this.swapFailedListeners.forEach(
1811
1906
  (listener) => listener(swap, error)
1812
1907
  );
1813
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
1908
+ await this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
1814
1909
  }
1815
1910
  }
1816
1911
  if (swap.request.from === "BTC") {
@@ -1843,6 +1938,7 @@ var SwapManager = class _SwapManager {
1843
1938
  } finally {
1844
1939
  this.swapsInProgress.delete(swap.id);
1845
1940
  }
1941
+ return true;
1846
1942
  }
1847
1943
  /**
1848
1944
  * Execute claim action for reverse swap
@@ -1856,13 +1952,15 @@ var SwapManager = class _SwapManager {
1856
1952
  }
1857
1953
  /**
1858
1954
  * Execute refund action for submarine swap
1955
+ *
1956
+ * @returns The refund outcome, or `undefined` if no callback is wired.
1859
1957
  */
1860
1958
  async executeRefundAction(swap) {
1861
1959
  if (!this.refundCallback) {
1862
1960
  logger.error("Refund callback not set");
1863
1961
  return;
1864
1962
  }
1865
- await this.refundCallback(swap);
1963
+ return this.refundCallback(swap);
1866
1964
  }
1867
1965
  /**
1868
1966
  * Execute claim action for chain swap Btc to Ark
@@ -1944,6 +2042,7 @@ var SwapManager = class _SwapManager {
1944
2042
  return;
1945
2043
  }
1946
2044
  for (const swap of this.monitoredSwaps.values()) {
2045
+ if (this.refundRetryTimers.has(swap.id)) continue;
1947
2046
  try {
1948
2047
  if (isPendingReverseSwap(swap) && isReverseClaimableStatus(swap.status)) {
1949
2048
  logger.log(`Resuming claim for swap ${swap.id}`);
@@ -2255,6 +2354,7 @@ var IndexedDbSwapRepository = class {
2255
2354
  constructor(dbName = DEFAULT_DB_NAME) {
2256
2355
  this.dbName = dbName;
2257
2356
  }
2357
+ dbName;
2258
2358
  version = 1;
2259
2359
  db = null;
2260
2360
  async getDB() {
@@ -2379,6 +2479,8 @@ var IndexedDbSwapRepository = class {
2379
2479
  // src/arkade-swaps.ts
2380
2480
  import {
2381
2481
  ArkAddress as ArkAddress2,
2482
+ ArkErrorName,
2483
+ isArkError,
2382
2484
  isRecoverable,
2383
2485
  getNetwork as getNetwork2
2384
2486
  } from "@arkade-os/sdk";
@@ -2425,6 +2527,13 @@ var MusigKeyAgg = class _MusigKeyAgg {
2425
2527
  this.internalKey = internalKey;
2426
2528
  this._tweak = _tweak;
2427
2529
  }
2530
+ privateKey;
2531
+ myPublicKey;
2532
+ publicKeys;
2533
+ myIndex;
2534
+ aggPubkey;
2535
+ internalKey;
2536
+ _tweak;
2428
2537
  xonlyTweakAdd(tweak) {
2429
2538
  if (this._tweak) throw new Error("musig key already tweaked");
2430
2539
  return new _MusigKeyAgg(
@@ -2459,6 +2568,13 @@ var MusigWithMessage = class {
2459
2568
  this.tweak = tweak;
2460
2569
  this.msg = msg;
2461
2570
  }
2571
+ privateKey;
2572
+ myPublicKey;
2573
+ publicKeys;
2574
+ myIndex;
2575
+ aggPubkey;
2576
+ tweak;
2577
+ msg;
2462
2578
  generateNonce() {
2463
2579
  const nonce = nonceGen(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
2464
2580
  return new MusigWithNonce(
@@ -2482,6 +2598,13 @@ var MusigWithNonce = class {
2482
2598
  this.msg = msg;
2483
2599
  this.nonce = nonce;
2484
2600
  }
2601
+ privateKey;
2602
+ myPublicKey;
2603
+ publicKeys;
2604
+ myIndex;
2605
+ tweak;
2606
+ msg;
2607
+ nonce;
2485
2608
  get publicNonce() {
2486
2609
  return this.nonce.public;
2487
2610
  }
@@ -2530,6 +2653,14 @@ var MusigNoncesAggregated = class {
2530
2653
  this.pubNonces = pubNonces;
2531
2654
  this.aggregatedNonce = aggregatedNonce;
2532
2655
  }
2656
+ privateKey;
2657
+ publicKeys;
2658
+ myIndex;
2659
+ tweak;
2660
+ msg;
2661
+ nonce;
2662
+ pubNonces;
2663
+ aggregatedNonce;
2533
2664
  get publicNonce() {
2534
2665
  return this.nonce.public;
2535
2666
  }
@@ -2561,6 +2692,12 @@ var MusigSession = class {
2561
2692
  this.session = session;
2562
2693
  this.partialSignatures = Array(publicKeys.length).fill(null);
2563
2694
  }
2695
+ privateKey;
2696
+ publicKeys;
2697
+ myIndex;
2698
+ nonce;
2699
+ pubNonces;
2700
+ session;
2564
2701
  partialSignatures;
2565
2702
  get publicNonce() {
2566
2703
  return this.nonce.public;
@@ -2588,6 +2725,10 @@ var MusigSigned = class {
2588
2725
  this.ourPartialSignature = ourPartialSignature;
2589
2726
  this.publicNonce = publicNonce;
2590
2727
  }
2728
+ session;
2729
+ partialSignatures;
2730
+ ourPartialSignature;
2731
+ publicNonce;
2591
2732
  aggregatePartials() {
2592
2733
  if (this.partialSignatures.some((s) => s === null)) {
2593
2734
  throw new Error("not all partial signatures are set");
@@ -3235,6 +3376,7 @@ var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3235
3376
  return refundableVtxos.some((vtxo) => !vtxo.isSpent && !isRecoverable(vtxo));
3236
3377
  };
3237
3378
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3379
+ var CLTV_IMMATURE_RETRY_SEC = 60;
3238
3380
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3239
3381
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3240
3382
  var quoteOptionsForSwap = (swap) => {
@@ -3307,9 +3449,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3307
3449
  claim: async (swap) => {
3308
3450
  await this.claimVHTLC(swap);
3309
3451
  },
3310
- refund: async (swap) => {
3311
- await this.refundVHTLC(swap);
3312
- },
3452
+ refund: (swap) => this.refundVHTLC(swap),
3313
3453
  claimArk: (swap) => this.claimArk(swap),
3314
3454
  claimBtc: (swap) => this.claimBtc(swap),
3315
3455
  refundArk: async (swap) => {
@@ -3915,7 +4055,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3915
4055
  refundWithoutReceiverLeaf,
3916
4056
  outputScript
3917
4057
  };
3918
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4058
+ const outcome = await this.refundVtxos({
3919
4059
  swapId: pendingSwap.id,
3920
4060
  vtxos: refundableVtxos,
3921
4061
  refundLocktime: vhtlcTimeouts.refund,
@@ -3925,7 +4065,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3925
4065
  refundViaBoltz: this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3926
4066
  });
3927
4067
  if (!isSubmarineSuccessStatus(pendingSwap.status)) {
3928
- const fullyRefunded = skippedCount === 0;
4068
+ const fullyRefunded = outcome.skipped === 0;
4069
+ pendingSwap.refundable = true;
4070
+ pendingSwap.refunded = fullyRefunded;
3929
4071
  await updateSubmarineSwapStatus(
3930
4072
  pendingSwap,
3931
4073
  pendingSwap.status,
@@ -3934,7 +4076,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3934
4076
  { refundable: true, refunded: fullyRefunded }
3935
4077
  );
3936
4078
  }
3937
- return { swept: sweptCount, skipped: skippedCount };
4079
+ return outcome;
3938
4080
  }
3939
4081
  /**
3940
4082
  * Inspect a submarine swap's lockup address for recoverable funds.
@@ -4551,7 +4693,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4551
4693
  refundWithoutReceiverLeaf,
4552
4694
  outputScript
4553
4695
  };
4554
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4696
+ const outcome = await this.refundVtxos({
4555
4697
  swapId: pendingSwap.id,
4556
4698
  vtxos: unspentVtxos,
4557
4699
  refundLocktime,
@@ -4561,11 +4703,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4561
4703
  refundViaBoltz: this.swapProvider.refundChainSwap.bind(this.swapProvider)
4562
4704
  });
4563
4705
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4564
- await this.savePendingChainSwap({
4565
- ...pendingSwap,
4566
- status: finalStatus.status
4567
- });
4568
- return { swept: sweptCount, skipped: skippedCount };
4706
+ pendingSwap.status = finalStatus.status;
4707
+ await this.savePendingChainSwap(pendingSwap);
4708
+ return outcome;
4569
4709
  }
4570
4710
  // =========================================================================
4571
4711
  // Chain swaps: BTC -> ARK
@@ -5169,18 +5309,44 @@ var ArkadeSwaps = class _ArkadeSwaps {
5169
5309
  );
5170
5310
  }
5171
5311
  }
5312
+ /**
5313
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
5314
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
5315
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
5316
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
5317
+ * already implement. Anything else propagates.
5318
+ *
5319
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
5320
+ * which settle via a batch round.
5321
+ *
5322
+ * @returns `true` if settled, `false` if the server deferred it.
5323
+ */
5324
+ async trySettleRefundWithoutReceiver(swapId, ctx, vtxo) {
5325
+ try {
5326
+ await this.settleRefundWithoutReceiver(ctx, vtxo);
5327
+ return true;
5328
+ } catch (error) {
5329
+ if (!isArkError(error, ArkErrorName.FORFEIT_CLOSURE_LOCKED)) throw error;
5330
+ logger.warn(
5331
+ `Swap ${swapId}: server deferred refundWithoutReceiver for VTXO ${vtxo.txid}:${vtxo.vout} \u2014 refund locktime has not matured against the chain tip (locktime=${error.metadata?.locktime}, currentLocktime=${error.metadata?.current_locktime}, type=${error.metadata?.type}). Refund will be retried once a later block carries the locktime.`
5332
+ );
5333
+ return false;
5334
+ }
5335
+ }
5172
5336
  /**
5173
5337
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
5174
5338
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
5175
5339
  * (chain). Path selection per VTXO:
5176
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
5177
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
5340
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
5341
+ * live VTXO, via a batch round for a swept one — see
5342
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
5343
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
5178
5344
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
5179
5345
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
5180
5346
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
5181
5347
  * locktime has since elapsed.
5182
5348
  *
5183
- * @returns Counts of VTXOs swept vs. deferred.
5349
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
5184
5350
  */
5185
5351
  async refundVtxos(params) {
5186
5352
  const {
@@ -5196,19 +5362,27 @@ var ArkadeSwaps = class _ArkadeSwaps {
5196
5362
  let boltzCallCount = 0;
5197
5363
  let sweptCount = 0;
5198
5364
  let skippedCount = 0;
5365
+ let retryAt;
5366
+ const defer = (candidate) => {
5367
+ skippedCount++;
5368
+ retryAt = retryAt === void 0 ? candidate : Math.min(retryAt, candidate);
5369
+ };
5199
5370
  for (const vtxo of vtxos) {
5200
5371
  const isRecoverableVtxo = isRecoverable(vtxo);
5201
5372
  const output = { amount: BigInt(vtxo.value), script: outputScript };
5202
5373
  if (isSubmarineRefundLocktimeReached(refundLocktime)) {
5203
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5204
- sweptCount++;
5374
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5375
+ sweptCount++;
5376
+ } else {
5377
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5378
+ }
5205
5379
  continue;
5206
5380
  }
5207
5381
  if (isRecoverableVtxo) {
5208
5382
  logger.error(
5209
5383
  `Swap ${swapId}: 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.`
5210
5384
  );
5211
- skippedCount++;
5385
+ defer(refundLocktime);
5212
5386
  continue;
5213
5387
  }
5214
5388
  const input = {
@@ -5242,17 +5416,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
5242
5416
  logger.error(
5243
5417
  `Swap ${swapId}: 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.`
5244
5418
  );
5245
- skippedCount++;
5419
+ defer(refundLocktime);
5246
5420
  continue;
5247
5421
  }
5248
5422
  logger.warn(
5249
5423
  `Swap ${swapId}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver offchain`
5250
5424
  );
5251
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5252
- sweptCount++;
5425
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5426
+ sweptCount++;
5427
+ } else {
5428
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5429
+ }
5253
5430
  }
5254
5431
  }
5255
- return { swept: sweptCount, skipped: skippedCount };
5432
+ return { swept: sweptCount, skipped: skippedCount, retryAt };
5256
5433
  }
5257
5434
  /**
5258
5435
  * Creates a VHTLC script for the swap.