@arkade-os/boltz-swap 0.3.50 → 0.3.52

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.
@@ -235,6 +235,7 @@ var BoltzRefundError = class extends Error {
235
235
  this.cause = cause;
236
236
  this.name = "BoltzRefundError";
237
237
  }
238
+ cause;
238
239
  };
239
240
 
240
241
  // src/boltz-swap-provider.ts
@@ -1005,6 +1006,13 @@ var MusigKeyAgg = class _MusigKeyAgg {
1005
1006
  this.internalKey = internalKey;
1006
1007
  this._tweak = _tweak;
1007
1008
  }
1009
+ privateKey;
1010
+ myPublicKey;
1011
+ publicKeys;
1012
+ myIndex;
1013
+ aggPubkey;
1014
+ internalKey;
1015
+ _tweak;
1008
1016
  xonlyTweakAdd(tweak) {
1009
1017
  if (this._tweak) throw new Error("musig key already tweaked");
1010
1018
  return new _MusigKeyAgg(
@@ -1039,6 +1047,13 @@ var MusigWithMessage = class {
1039
1047
  this.tweak = tweak;
1040
1048
  this.msg = msg;
1041
1049
  }
1050
+ privateKey;
1051
+ myPublicKey;
1052
+ publicKeys;
1053
+ myIndex;
1054
+ aggPubkey;
1055
+ tweak;
1056
+ msg;
1042
1057
  generateNonce() {
1043
1058
  const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
1044
1059
  return new MusigWithNonce(
@@ -1062,6 +1077,13 @@ var MusigWithNonce = class {
1062
1077
  this.msg = msg;
1063
1078
  this.nonce = nonce;
1064
1079
  }
1080
+ privateKey;
1081
+ myPublicKey;
1082
+ publicKeys;
1083
+ myIndex;
1084
+ tweak;
1085
+ msg;
1086
+ nonce;
1065
1087
  get publicNonce() {
1066
1088
  return this.nonce.public;
1067
1089
  }
@@ -1110,6 +1132,14 @@ var MusigNoncesAggregated = class {
1110
1132
  this.pubNonces = pubNonces;
1111
1133
  this.aggregatedNonce = aggregatedNonce;
1112
1134
  }
1135
+ privateKey;
1136
+ publicKeys;
1137
+ myIndex;
1138
+ tweak;
1139
+ msg;
1140
+ nonce;
1141
+ pubNonces;
1142
+ aggregatedNonce;
1113
1143
  get publicNonce() {
1114
1144
  return this.nonce.public;
1115
1145
  }
@@ -1141,6 +1171,12 @@ var MusigSession = class {
1141
1171
  this.session = session;
1142
1172
  this.partialSignatures = Array(publicKeys.length).fill(null);
1143
1173
  }
1174
+ privateKey;
1175
+ publicKeys;
1176
+ myIndex;
1177
+ nonce;
1178
+ pubNonces;
1179
+ session;
1144
1180
  partialSignatures;
1145
1181
  get publicNonce() {
1146
1182
  return this.nonce.public;
@@ -1168,6 +1204,10 @@ var MusigSigned = class {
1168
1204
  this.ourPartialSignature = ourPartialSignature;
1169
1205
  this.publicNonce = publicNonce;
1170
1206
  }
1207
+ session;
1208
+ partialSignatures;
1209
+ ourPartialSignature;
1210
+ publicNonce;
1171
1211
  aggregatePartials() {
1172
1212
  if (this.partialSignatures.some((s) => s === null)) {
1173
1213
  throw new Error("not all partial signatures are set");
@@ -1435,12 +1475,20 @@ var SwapManager = class _SwapManager {
1435
1475
  */
1436
1476
  static NOT_FOUND_THRESHOLD = 10;
1437
1477
  /**
1438
- * Delay between re-attempts of a chain refund that left VTXOs deferred
1439
- * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1440
- * has elapsed). Boltz won't send another status update once the swap
1441
- * is `swap.expired`, so the manager owns the local retry cadence.
1478
+ * Floor on the delay between re-attempts of a refund that left VTXOs
1479
+ * deferred, and the delay used when the outcome names no retry time. Boltz
1480
+ * won't send another status update once the swap is refundable-and-final,
1481
+ * so the manager owns the local retry cadence.
1442
1482
  */
1443
1483
  static REFUND_RETRY_DELAY_MS = 6e4;
1484
+ /**
1485
+ * Ceiling on that delay. A deferral can be hours out (a pre-CLTV VTXO waits
1486
+ * out the whole refund locktime), and `setTimeout` fires *immediately* past
1487
+ * its 32-bit millisecond range, so long waits are broken into re-armed
1488
+ * hops. Re-attempting costs an indexer lookup and re-defers, which also lets
1489
+ * the cadence recover from a suspended device or a clock jump.
1490
+ */
1491
+ static MAX_REFUND_RETRY_DELAY_MS = 60 * 6e4;
1444
1492
  swapProvider;
1445
1493
  config;
1446
1494
  // Event listeners storage (supports multiple listeners per event)
@@ -1459,10 +1507,9 @@ var SwapManager = class _SwapManager {
1459
1507
  reconnectTimer = null;
1460
1508
  initialPollTimer = null;
1461
1509
  pollRetryTimers = /* @__PURE__ */ new Map();
1462
- // Per-swap retry timers for chain refunds that left work undone
1463
- // (refundArk returned `skipped > 0`). The swap is held in
1464
- // `monitoredSwaps` past its terminal Boltz status until the local
1465
- // refund completes or the manager stops.
1510
+ // Per-swap retry timers for deferred refunds that left local work undone.
1511
+ // The swap is held in `monitoredSwaps` past its terminal Boltz status
1512
+ // until the local refund completes or the manager stops.
1466
1513
  refundRetryTimers = /* @__PURE__ */ new Map();
1467
1514
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1468
1515
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
@@ -1634,11 +1681,12 @@ var SwapManager = class _SwapManager {
1634
1681
  this.initialSwaps.set(swap.id, swap);
1635
1682
  }
1636
1683
  for (const swap of pendingSwaps) {
1637
- if (!this.isFinalStatus(swap)) {
1684
+ if (this.shouldMonitorOnStart(swap)) {
1638
1685
  this.monitoredSwaps.set(swap.id, swap);
1639
1686
  }
1640
1687
  }
1641
1688
  await this.tryConnectWebSocket();
1689
+ this.resumePersistedRefundRetries();
1642
1690
  await this.resumeActionableSwaps();
1643
1691
  }
1644
1692
  /**
@@ -1721,6 +1769,7 @@ var SwapManager = class _SwapManager {
1721
1769
  * Remove a swap from monitoring
1722
1770
  */
1723
1771
  async removeSwap(swapId) {
1772
+ const swap = this.monitoredSwaps.get(swapId);
1724
1773
  this.monitoredSwaps.delete(swapId);
1725
1774
  this.swapSubscriptions.delete(swapId);
1726
1775
  this.chainClaimPromises.delete(swapId);
@@ -1734,6 +1783,9 @@ var SwapManager = class _SwapManager {
1734
1783
  clearTimeout(refundRetryTimer);
1735
1784
  this.refundRetryTimers.delete(swapId);
1736
1785
  }
1786
+ if (swap) {
1787
+ await this.clearRefundRetry(swap);
1788
+ }
1737
1789
  this.notFoundCounts.delete(swapId);
1738
1790
  logger.log(`Removed swap ${swapId} from monitoring`);
1739
1791
  }
@@ -2072,12 +2124,69 @@ var SwapManager = class _SwapManager {
2072
2124
  }
2073
2125
  this.swapCompletedListeners.forEach((listener) => listener(swap));
2074
2126
  }
2127
+ static isRefundRetryableSwap(swap) {
2128
+ return isPendingSubmarineSwap(swap) || isPendingChainSwap(swap);
2129
+ }
2130
+ static hasPendingRefundRetry(swap) {
2131
+ if (!_SwapManager.isRefundRetryableSwap(swap)) return false;
2132
+ return swap.refundRetry?.pending === true && Number.isFinite(swap.refundRetry.nextRetryAt);
2133
+ }
2134
+ shouldMonitorOnStart(swap) {
2135
+ if (!this.isFinalStatus(swap)) return true;
2136
+ return this.config.enableAutoActions === true && _SwapManager.hasPendingRefundRetry(swap);
2137
+ }
2138
+ /**
2139
+ * How long to wait before re-attempting a refund, given the `retryAt`
2140
+ * (Unix seconds) the outcome reported. Clamped at both ends: see
2141
+ * {@link SwapManager.REFUND_RETRY_DELAY_MS} and
2142
+ * {@link SwapManager.MAX_REFUND_RETRY_DELAY_MS}.
2143
+ */
2144
+ static refundRetryDelayMs(retryAt) {
2145
+ if (retryAt === void 0) return _SwapManager.REFUND_RETRY_DELAY_MS;
2146
+ return Math.min(
2147
+ Math.max(retryAt * 1e3 - Date.now(), _SwapManager.REFUND_RETRY_DELAY_MS),
2148
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2149
+ );
2150
+ }
2151
+ static persistedRefundRetryDelayMs(nextRetryAt) {
2152
+ return Math.min(
2153
+ Math.max(nextRetryAt * 1e3 - Date.now(), 0),
2154
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2155
+ );
2156
+ }
2157
+ static nextRefundRetryAt(delayMs) {
2158
+ return Math.ceil((Date.now() + delayMs) / 1e3);
2159
+ }
2160
+ async clearRefundRetry(swap) {
2161
+ if (!_SwapManager.isRefundRetryableSwap(swap) || swap.refundRetry === void 0) return;
2162
+ delete swap.refundRetry;
2163
+ await this.saveSwap(swap);
2164
+ }
2165
+ resumePersistedRefundRetries() {
2166
+ if (!this.config.enableAutoActions) return;
2167
+ for (const swap of this.monitoredSwaps.values()) {
2168
+ if (!_SwapManager.hasPendingRefundRetry(swap)) continue;
2169
+ const delayMs = _SwapManager.persistedRefundRetryDelayMs(swap.refundRetry.nextRetryAt);
2170
+ logger.log(
2171
+ `Swap ${swap.id}: resuming deferred refund retry in ${Math.round(delayMs / 1e3)}s`
2172
+ );
2173
+ this.armRefundRetryTimer(swap, delayMs);
2174
+ }
2175
+ }
2075
2176
  /**
2076
- * Schedule another `executeAutonomousAction` run for a chain swap whose
2077
- * refund left VTXOs deferred. After the retry completes, if no further
2078
- * deferral was reported, finalize monitoring cleanup.
2177
+ * Schedule another `executeAutonomousAction` run for a swap whose refund
2178
+ * left VTXOs deferred. The pending retry and its next attempt time are saved
2179
+ * with the swap so stop/start and application restart can re-arm it.
2079
2180
  */
2080
- scheduleRefundRetry(swap, delayMs) {
2181
+ async scheduleRefundRetry(swap, delayMs) {
2182
+ swap.refundRetry = {
2183
+ pending: true,
2184
+ nextRetryAt: _SwapManager.nextRefundRetryAt(delayMs)
2185
+ };
2186
+ await this.saveSwap(swap);
2187
+ this.armRefundRetryTimer(swap, delayMs);
2188
+ }
2189
+ armRefundRetryTimer(swap, delayMs) {
2081
2190
  const existing = this.refundRetryTimers.get(swap.id);
2082
2191
  if (existing) clearTimeout(existing);
2083
2192
  this.refundRetryTimers.set(
@@ -2085,12 +2194,14 @@ var SwapManager = class _SwapManager {
2085
2194
  setTimeout(async () => {
2086
2195
  this.refundRetryTimers.delete(swap.id);
2087
2196
  if (!this.isRunning) return;
2088
- if (!this.monitoredSwaps.has(swap.id)) return;
2197
+ const current = this.monitoredSwaps.get(swap.id);
2198
+ if (!current) return;
2199
+ let ran = false;
2089
2200
  try {
2090
- await this.executeAutonomousAction(swap);
2201
+ ran = await this.executeAutonomousAction(current);
2091
2202
  } finally {
2092
- if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
2093
- this.finalizeMonitoredSwap(swap);
2203
+ if (ran && !this.refundRetryTimers.has(current.id) && this.isFinalStatus(current)) {
2204
+ this.finalizeMonitoredSwap(current);
2094
2205
  }
2095
2206
  }
2096
2207
  }, delayMs)
@@ -2099,11 +2210,16 @@ var SwapManager = class _SwapManager {
2099
2210
  /**
2100
2211
  * Execute autonomous action based on swap status
2101
2212
  * Uses locking to prevent race conditions with manual operations
2213
+ *
2214
+ * @returns `false` when another action for this swap already held the lock
2215
+ * and this call did nothing, `true` when it ran. Callers that finalize on
2216
+ * the result must not act on `false`: the in-flight action owns the swap's
2217
+ * outcome and will re-arm or finalize from its own result.
2102
2218
  */
2103
2219
  async executeAutonomousAction(swap) {
2104
2220
  if (this.swapsInProgress.has(swap.id)) {
2105
2221
  logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
2106
- return;
2222
+ return false;
2107
2223
  }
2108
2224
  try {
2109
2225
  this.swapsInProgress.add(swap.id);
@@ -2112,7 +2228,7 @@ var SwapManager = class _SwapManager {
2112
2228
  logger.log(
2113
2229
  `Skipping claim for swap ${swap.id}: missing preimage (restored swap)`
2114
2230
  );
2115
- return;
2231
+ return true;
2116
2232
  }
2117
2233
  if (isReverseClaimableStatus(swap.status)) {
2118
2234
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
@@ -2124,11 +2240,26 @@ var SwapManager = class _SwapManager {
2124
2240
  logger.log(
2125
2241
  `Skipping refund for swap ${swap.id}: missing invoice (restored swap)`
2126
2242
  );
2127
- return;
2243
+ return true;
2128
2244
  }
2129
2245
  if (isSubmarineRefundableStatus(swap.status)) {
2130
2246
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
2131
- await this.executeRefundAction(swap);
2247
+ let outcome;
2248
+ try {
2249
+ outcome = await this.executeRefundAction(swap);
2250
+ } catch (error) {
2251
+ await this.clearRefundRetry(swap);
2252
+ throw error;
2253
+ }
2254
+ if (outcome && outcome.skipped > 0) {
2255
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2256
+ logger.log(
2257
+ `Submarine swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2258
+ );
2259
+ await this.scheduleRefundRetry(swap, delayMs);
2260
+ } else if (outcome) {
2261
+ await this.clearRefundRetry(swap);
2262
+ }
2132
2263
  this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
2133
2264
  }
2134
2265
  } else if (isPendingChainSwap(swap)) {
@@ -2152,10 +2283,13 @@ var SwapManager = class _SwapManager {
2152
2283
  try {
2153
2284
  const outcome = await this.executeRefundArkAction(swap);
2154
2285
  if (outcome && outcome.skipped > 0) {
2286
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2155
2287
  logger.log(
2156
- `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2288
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2157
2289
  );
2158
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2290
+ await this.scheduleRefundRetry(swap, delayMs);
2291
+ } else if (outcome) {
2292
+ await this.clearRefundRetry(swap);
2159
2293
  }
2160
2294
  this.actionExecutedListeners.forEach(
2161
2295
  (listener) => listener(swap, "refundArk")
@@ -2168,7 +2302,7 @@ var SwapManager = class _SwapManager {
2168
2302
  this.swapFailedListeners.forEach(
2169
2303
  (listener) => listener(swap, error)
2170
2304
  );
2171
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2305
+ await this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2172
2306
  }
2173
2307
  }
2174
2308
  if (swap.request.from === "BTC") {
@@ -2201,6 +2335,7 @@ var SwapManager = class _SwapManager {
2201
2335
  } finally {
2202
2336
  this.swapsInProgress.delete(swap.id);
2203
2337
  }
2338
+ return true;
2204
2339
  }
2205
2340
  /**
2206
2341
  * Execute claim action for reverse swap
@@ -2214,13 +2349,15 @@ var SwapManager = class _SwapManager {
2214
2349
  }
2215
2350
  /**
2216
2351
  * Execute refund action for submarine swap
2352
+ *
2353
+ * @returns The refund outcome, or `undefined` if no callback is wired.
2217
2354
  */
2218
2355
  async executeRefundAction(swap) {
2219
2356
  if (!this.refundCallback) {
2220
2357
  logger.error("Refund callback not set");
2221
2358
  return;
2222
2359
  }
2223
- await this.refundCallback(swap);
2360
+ return this.refundCallback(swap);
2224
2361
  }
2225
2362
  /**
2226
2363
  * Execute claim action for chain swap Btc to Ark
@@ -2302,6 +2439,7 @@ var SwapManager = class _SwapManager {
2302
2439
  return;
2303
2440
  }
2304
2441
  for (const swap of this.monitoredSwaps.values()) {
2442
+ if (this.refundRetryTimers.has(swap.id)) continue;
2305
2443
  try {
2306
2444
  if (isPendingReverseSwap(swap) && isReverseClaimableStatus(swap.status)) {
2307
2445
  logger.log(`Resuming claim for swap ${swap.id}`);
@@ -2626,6 +2764,7 @@ var IndexedDbSwapRepository = class {
2626
2764
  constructor(dbName = DEFAULT_DB_NAME) {
2627
2765
  this.dbName = dbName;
2628
2766
  }
2767
+ dbName;
2629
2768
  version = 1;
2630
2769
  db = null;
2631
2770
  async getDB() {
@@ -3176,6 +3315,7 @@ var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3176
3315
  return refundableVtxos.some((vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo));
3177
3316
  };
3178
3317
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3318
+ var CLTV_IMMATURE_RETRY_SEC = 60;
3179
3319
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3180
3320
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3181
3321
  var quoteOptionsForSwap = (swap) => {
@@ -3248,9 +3388,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3248
3388
  claim: async (swap) => {
3249
3389
  await this.claimVHTLC(swap);
3250
3390
  },
3251
- refund: async (swap) => {
3252
- await this.refundVHTLC(swap);
3253
- },
3391
+ refund: (swap) => this.refundVHTLC(swap),
3254
3392
  claimArk: (swap) => this.claimArk(swap),
3255
3393
  claimBtc: (swap) => this.claimBtc(swap),
3256
3394
  refundArk: async (swap) => {
@@ -3856,7 +3994,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3856
3994
  refundWithoutReceiverLeaf,
3857
3995
  outputScript
3858
3996
  };
3859
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
3997
+ const outcome = await this.refundVtxos({
3860
3998
  swapId: pendingSwap.id,
3861
3999
  vtxos: refundableVtxos,
3862
4000
  refundLocktime: vhtlcTimeouts.refund,
@@ -3866,7 +4004,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3866
4004
  refundViaBoltz: this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3867
4005
  });
3868
4006
  if (!isSubmarineSuccessStatus(pendingSwap.status)) {
3869
- const fullyRefunded = skippedCount === 0;
4007
+ const fullyRefunded = outcome.skipped === 0;
4008
+ pendingSwap.refundable = true;
4009
+ pendingSwap.refunded = fullyRefunded;
3870
4010
  await updateSubmarineSwapStatus(
3871
4011
  pendingSwap,
3872
4012
  pendingSwap.status,
@@ -3875,7 +4015,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3875
4015
  { refundable: true, refunded: fullyRefunded }
3876
4016
  );
3877
4017
  }
3878
- return { swept: sweptCount, skipped: skippedCount };
4018
+ return outcome;
3879
4019
  }
3880
4020
  /**
3881
4021
  * Inspect a submarine swap's lockup address for recoverable funds.
@@ -4492,7 +4632,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4492
4632
  refundWithoutReceiverLeaf,
4493
4633
  outputScript
4494
4634
  };
4495
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4635
+ const outcome = await this.refundVtxos({
4496
4636
  swapId: pendingSwap.id,
4497
4637
  vtxos: unspentVtxos,
4498
4638
  refundLocktime,
@@ -4502,11 +4642,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4502
4642
  refundViaBoltz: this.swapProvider.refundChainSwap.bind(this.swapProvider)
4503
4643
  });
4504
4644
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4505
- await this.savePendingChainSwap({
4506
- ...pendingSwap,
4507
- status: finalStatus.status
4508
- });
4509
- return { swept: sweptCount, skipped: skippedCount };
4645
+ pendingSwap.status = finalStatus.status;
4646
+ await this.savePendingChainSwap(pendingSwap);
4647
+ return outcome;
4510
4648
  }
4511
4649
  // =========================================================================
4512
4650
  // Chain swaps: BTC -> ARK
@@ -5110,18 +5248,44 @@ var ArkadeSwaps = class _ArkadeSwaps {
5110
5248
  );
5111
5249
  }
5112
5250
  }
5251
+ /**
5252
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
5253
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
5254
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
5255
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
5256
+ * already implement. Anything else propagates.
5257
+ *
5258
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
5259
+ * which settle via a batch round.
5260
+ *
5261
+ * @returns `true` if settled, `false` if the server deferred it.
5262
+ */
5263
+ async trySettleRefundWithoutReceiver(swapId, ctx, vtxo) {
5264
+ try {
5265
+ await this.settleRefundWithoutReceiver(ctx, vtxo);
5266
+ return true;
5267
+ } catch (error) {
5268
+ if (!(0, import_sdk8.isArkError)(error, import_sdk8.ArkErrorName.FORFEIT_CLOSURE_LOCKED)) throw error;
5269
+ logger.warn(
5270
+ `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.`
5271
+ );
5272
+ return false;
5273
+ }
5274
+ }
5113
5275
  /**
5114
5276
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
5115
5277
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
5116
5278
  * (chain). Path selection per VTXO:
5117
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
5118
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
5279
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
5280
+ * live VTXO, via a batch round for a swept one — see
5281
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
5282
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
5119
5283
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
5120
5284
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
5121
5285
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
5122
5286
  * locktime has since elapsed.
5123
5287
  *
5124
- * @returns Counts of VTXOs swept vs. deferred.
5288
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
5125
5289
  */
5126
5290
  async refundVtxos(params) {
5127
5291
  const {
@@ -5137,19 +5301,27 @@ var ArkadeSwaps = class _ArkadeSwaps {
5137
5301
  let boltzCallCount = 0;
5138
5302
  let sweptCount = 0;
5139
5303
  let skippedCount = 0;
5304
+ let retryAt;
5305
+ const defer = (candidate) => {
5306
+ skippedCount++;
5307
+ retryAt = retryAt === void 0 ? candidate : Math.min(retryAt, candidate);
5308
+ };
5140
5309
  for (const vtxo of vtxos) {
5141
5310
  const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
5142
5311
  const output = { amount: BigInt(vtxo.value), script: outputScript };
5143
5312
  if (isSubmarineRefundLocktimeReached(refundLocktime)) {
5144
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5145
- sweptCount++;
5313
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5314
+ sweptCount++;
5315
+ } else {
5316
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5317
+ }
5146
5318
  continue;
5147
5319
  }
5148
5320
  if (isRecoverableVtxo) {
5149
5321
  logger.error(
5150
5322
  `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.`
5151
5323
  );
5152
- skippedCount++;
5324
+ defer(refundLocktime);
5153
5325
  continue;
5154
5326
  }
5155
5327
  const input = {
@@ -5183,17 +5355,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
5183
5355
  logger.error(
5184
5356
  `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.`
5185
5357
  );
5186
- skippedCount++;
5358
+ defer(refundLocktime);
5187
5359
  continue;
5188
5360
  }
5189
5361
  logger.warn(
5190
5362
  `Swap ${swapId}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver offchain`
5191
5363
  );
5192
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5193
- sweptCount++;
5364
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5365
+ sweptCount++;
5366
+ } else {
5367
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5368
+ }
5194
5369
  }
5195
5370
  }
5196
- return { swept: sweptCount, skipped: skippedCount };
5371
+ return { swept: sweptCount, skipped: skippedCount, retryAt };
5197
5372
  }
5198
5373
  /**
5199
5374
  * Creates a VHTLC script for the swap.
@@ -1,8 +1,8 @@
1
- import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-CIqTFkV1.cjs';
2
- export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-CIqTFkV1.cjs';
1
+ import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-DCtJYSbJ.cjs';
2
+ export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-DCtJYSbJ.cjs';
3
3
  import '@arkade-os/sdk/worker/expo';
4
4
  import '@arkade-os/sdk';
5
- import '../types-BKEkNZxK.cjs';
5
+ import '../types-Db4JSdjk.cjs';
6
6
 
7
7
  /**
8
8
  * Define the Expo background task handler for swap polling.
@@ -1,8 +1,8 @@
1
- import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-C4HQHP9Z.js';
2
- export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-C4HQHP9Z.js';
1
+ import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor--BPV48QB.js';
2
+ export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor--BPV48QB.js';
3
3
  import '@arkade-os/sdk/worker/expo';
4
4
  import '@arkade-os/sdk';
5
- import '../types-BKEkNZxK.js';
5
+ import '../types-Db4JSdjk.js';
6
6
 
7
7
  /**
8
8
  * Define the Expo background task handler for swap polling.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  SWAP_POLL_TASK_TYPE,
3
3
  swapsPollProcessor
4
- } from "../chunk-HIG2OOAN.js";
4
+ } from "../chunk-76V3TKDL.js";
5
5
  import {
6
6
  BoltzSwapProvider
7
- } from "../chunk-VKHFXGKL.js";
7
+ } from "../chunk-CZTZTYZU.js";
8
8
  import "../chunk-SJQJQO7P.js";
9
9
 
10
10
  // src/expo/background.ts