@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.
@@ -225,6 +225,7 @@ var BoltzRefundError = class extends Error {
225
225
  this.cause = cause;
226
226
  this.name = "BoltzRefundError";
227
227
  }
228
+ cause;
228
229
  };
229
230
 
230
231
  // src/arkade-swaps.ts
@@ -995,6 +996,13 @@ var MusigKeyAgg = class _MusigKeyAgg {
995
996
  this.internalKey = internalKey;
996
997
  this._tweak = _tweak;
997
998
  }
999
+ privateKey;
1000
+ myPublicKey;
1001
+ publicKeys;
1002
+ myIndex;
1003
+ aggPubkey;
1004
+ internalKey;
1005
+ _tweak;
998
1006
  xonlyTweakAdd(tweak) {
999
1007
  if (this._tweak) throw new Error("musig key already tweaked");
1000
1008
  return new _MusigKeyAgg(
@@ -1029,6 +1037,13 @@ var MusigWithMessage = class {
1029
1037
  this.tweak = tweak;
1030
1038
  this.msg = msg;
1031
1039
  }
1040
+ privateKey;
1041
+ myPublicKey;
1042
+ publicKeys;
1043
+ myIndex;
1044
+ aggPubkey;
1045
+ tweak;
1046
+ msg;
1032
1047
  generateNonce() {
1033
1048
  const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
1034
1049
  return new MusigWithNonce(
@@ -1052,6 +1067,13 @@ var MusigWithNonce = class {
1052
1067
  this.msg = msg;
1053
1068
  this.nonce = nonce;
1054
1069
  }
1070
+ privateKey;
1071
+ myPublicKey;
1072
+ publicKeys;
1073
+ myIndex;
1074
+ tweak;
1075
+ msg;
1076
+ nonce;
1055
1077
  get publicNonce() {
1056
1078
  return this.nonce.public;
1057
1079
  }
@@ -1100,6 +1122,14 @@ var MusigNoncesAggregated = class {
1100
1122
  this.pubNonces = pubNonces;
1101
1123
  this.aggregatedNonce = aggregatedNonce;
1102
1124
  }
1125
+ privateKey;
1126
+ publicKeys;
1127
+ myIndex;
1128
+ tweak;
1129
+ msg;
1130
+ nonce;
1131
+ pubNonces;
1132
+ aggregatedNonce;
1103
1133
  get publicNonce() {
1104
1134
  return this.nonce.public;
1105
1135
  }
@@ -1131,6 +1161,12 @@ var MusigSession = class {
1131
1161
  this.session = session;
1132
1162
  this.partialSignatures = Array(publicKeys.length).fill(null);
1133
1163
  }
1164
+ privateKey;
1165
+ publicKeys;
1166
+ myIndex;
1167
+ nonce;
1168
+ pubNonces;
1169
+ session;
1134
1170
  partialSignatures;
1135
1171
  get publicNonce() {
1136
1172
  return this.nonce.public;
@@ -1158,6 +1194,10 @@ var MusigSigned = class {
1158
1194
  this.ourPartialSignature = ourPartialSignature;
1159
1195
  this.publicNonce = publicNonce;
1160
1196
  }
1197
+ session;
1198
+ partialSignatures;
1199
+ ourPartialSignature;
1200
+ publicNonce;
1161
1201
  aggregatePartials() {
1162
1202
  if (this.partialSignatures.some((s) => s === null)) {
1163
1203
  throw new Error("not all partial signatures are set");
@@ -1425,12 +1465,20 @@ var SwapManager = class _SwapManager {
1425
1465
  */
1426
1466
  static NOT_FOUND_THRESHOLD = 10;
1427
1467
  /**
1428
- * Delay between re-attempts of a chain refund that left VTXOs deferred
1429
- * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1430
- * has elapsed). Boltz won't send another status update once the swap
1431
- * is `swap.expired`, so the manager owns the local retry cadence.
1468
+ * Floor on the delay between re-attempts of a refund that left VTXOs
1469
+ * deferred, and the delay used when the outcome names no retry time. Boltz
1470
+ * won't send another status update once the swap is refundable-and-final,
1471
+ * so the manager owns the local retry cadence.
1432
1472
  */
1433
1473
  static REFUND_RETRY_DELAY_MS = 6e4;
1474
+ /**
1475
+ * Ceiling on that delay. A deferral can be hours out (a pre-CLTV VTXO waits
1476
+ * out the whole refund locktime), and `setTimeout` fires *immediately* past
1477
+ * its 32-bit millisecond range, so long waits are broken into re-armed
1478
+ * hops. Re-attempting costs an indexer lookup and re-defers, which also lets
1479
+ * the cadence recover from a suspended device or a clock jump.
1480
+ */
1481
+ static MAX_REFUND_RETRY_DELAY_MS = 60 * 6e4;
1434
1482
  swapProvider;
1435
1483
  config;
1436
1484
  // Event listeners storage (supports multiple listeners per event)
@@ -1449,10 +1497,9 @@ var SwapManager = class _SwapManager {
1449
1497
  reconnectTimer = null;
1450
1498
  initialPollTimer = null;
1451
1499
  pollRetryTimers = /* @__PURE__ */ new Map();
1452
- // Per-swap retry timers for chain refunds that left work undone
1453
- // (refundArk returned `skipped > 0`). The swap is held in
1454
- // `monitoredSwaps` past its terminal Boltz status until the local
1455
- // refund completes or the manager stops.
1500
+ // Per-swap retry timers for deferred refunds that left local work undone.
1501
+ // The swap is held in `monitoredSwaps` past its terminal Boltz status
1502
+ // until the local refund completes or the manager stops.
1456
1503
  refundRetryTimers = /* @__PURE__ */ new Map();
1457
1504
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1458
1505
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
@@ -1624,11 +1671,12 @@ var SwapManager = class _SwapManager {
1624
1671
  this.initialSwaps.set(swap.id, swap);
1625
1672
  }
1626
1673
  for (const swap of pendingSwaps) {
1627
- if (!this.isFinalStatus(swap)) {
1674
+ if (this.shouldMonitorOnStart(swap)) {
1628
1675
  this.monitoredSwaps.set(swap.id, swap);
1629
1676
  }
1630
1677
  }
1631
1678
  await this.tryConnectWebSocket();
1679
+ this.resumePersistedRefundRetries();
1632
1680
  await this.resumeActionableSwaps();
1633
1681
  }
1634
1682
  /**
@@ -1711,6 +1759,7 @@ var SwapManager = class _SwapManager {
1711
1759
  * Remove a swap from monitoring
1712
1760
  */
1713
1761
  async removeSwap(swapId) {
1762
+ const swap = this.monitoredSwaps.get(swapId);
1714
1763
  this.monitoredSwaps.delete(swapId);
1715
1764
  this.swapSubscriptions.delete(swapId);
1716
1765
  this.chainClaimPromises.delete(swapId);
@@ -1724,6 +1773,9 @@ var SwapManager = class _SwapManager {
1724
1773
  clearTimeout(refundRetryTimer);
1725
1774
  this.refundRetryTimers.delete(swapId);
1726
1775
  }
1776
+ if (swap) {
1777
+ await this.clearRefundRetry(swap);
1778
+ }
1727
1779
  this.notFoundCounts.delete(swapId);
1728
1780
  logger.log(`Removed swap ${swapId} from monitoring`);
1729
1781
  }
@@ -2062,12 +2114,69 @@ var SwapManager = class _SwapManager {
2062
2114
  }
2063
2115
  this.swapCompletedListeners.forEach((listener) => listener(swap));
2064
2116
  }
2117
+ static isRefundRetryableSwap(swap) {
2118
+ return isPendingSubmarineSwap(swap) || isPendingChainSwap(swap);
2119
+ }
2120
+ static hasPendingRefundRetry(swap) {
2121
+ if (!_SwapManager.isRefundRetryableSwap(swap)) return false;
2122
+ return swap.refundRetry?.pending === true && Number.isFinite(swap.refundRetry.nextRetryAt);
2123
+ }
2124
+ shouldMonitorOnStart(swap) {
2125
+ if (!this.isFinalStatus(swap)) return true;
2126
+ return this.config.enableAutoActions === true && _SwapManager.hasPendingRefundRetry(swap);
2127
+ }
2065
2128
  /**
2066
- * Schedule another `executeAutonomousAction` run for a chain swap whose
2067
- * refund left VTXOs deferred. After the retry completes, if no further
2068
- * deferral was reported, finalize monitoring cleanup.
2129
+ * How long to wait before re-attempting a refund, given the `retryAt`
2130
+ * (Unix seconds) the outcome reported. Clamped at both ends: see
2131
+ * {@link SwapManager.REFUND_RETRY_DELAY_MS} and
2132
+ * {@link SwapManager.MAX_REFUND_RETRY_DELAY_MS}.
2069
2133
  */
2070
- scheduleRefundRetry(swap, delayMs) {
2134
+ static refundRetryDelayMs(retryAt) {
2135
+ if (retryAt === void 0) return _SwapManager.REFUND_RETRY_DELAY_MS;
2136
+ return Math.min(
2137
+ Math.max(retryAt * 1e3 - Date.now(), _SwapManager.REFUND_RETRY_DELAY_MS),
2138
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2139
+ );
2140
+ }
2141
+ static persistedRefundRetryDelayMs(nextRetryAt) {
2142
+ return Math.min(
2143
+ Math.max(nextRetryAt * 1e3 - Date.now(), 0),
2144
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2145
+ );
2146
+ }
2147
+ static nextRefundRetryAt(delayMs) {
2148
+ return Math.ceil((Date.now() + delayMs) / 1e3);
2149
+ }
2150
+ async clearRefundRetry(swap) {
2151
+ if (!_SwapManager.isRefundRetryableSwap(swap) || swap.refundRetry === void 0) return;
2152
+ delete swap.refundRetry;
2153
+ await this.saveSwap(swap);
2154
+ }
2155
+ resumePersistedRefundRetries() {
2156
+ if (!this.config.enableAutoActions) return;
2157
+ for (const swap of this.monitoredSwaps.values()) {
2158
+ if (!_SwapManager.hasPendingRefundRetry(swap)) continue;
2159
+ const delayMs = _SwapManager.persistedRefundRetryDelayMs(swap.refundRetry.nextRetryAt);
2160
+ logger.log(
2161
+ `Swap ${swap.id}: resuming deferred refund retry in ${Math.round(delayMs / 1e3)}s`
2162
+ );
2163
+ this.armRefundRetryTimer(swap, delayMs);
2164
+ }
2165
+ }
2166
+ /**
2167
+ * Schedule another `executeAutonomousAction` run for a swap whose refund
2168
+ * left VTXOs deferred. The pending retry and its next attempt time are saved
2169
+ * with the swap so stop/start and application restart can re-arm it.
2170
+ */
2171
+ async scheduleRefundRetry(swap, delayMs) {
2172
+ swap.refundRetry = {
2173
+ pending: true,
2174
+ nextRetryAt: _SwapManager.nextRefundRetryAt(delayMs)
2175
+ };
2176
+ await this.saveSwap(swap);
2177
+ this.armRefundRetryTimer(swap, delayMs);
2178
+ }
2179
+ armRefundRetryTimer(swap, delayMs) {
2071
2180
  const existing = this.refundRetryTimers.get(swap.id);
2072
2181
  if (existing) clearTimeout(existing);
2073
2182
  this.refundRetryTimers.set(
@@ -2075,12 +2184,14 @@ var SwapManager = class _SwapManager {
2075
2184
  setTimeout(async () => {
2076
2185
  this.refundRetryTimers.delete(swap.id);
2077
2186
  if (!this.isRunning) return;
2078
- if (!this.monitoredSwaps.has(swap.id)) return;
2187
+ const current = this.monitoredSwaps.get(swap.id);
2188
+ if (!current) return;
2189
+ let ran = false;
2079
2190
  try {
2080
- await this.executeAutonomousAction(swap);
2191
+ ran = await this.executeAutonomousAction(current);
2081
2192
  } finally {
2082
- if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
2083
- this.finalizeMonitoredSwap(swap);
2193
+ if (ran && !this.refundRetryTimers.has(current.id) && this.isFinalStatus(current)) {
2194
+ this.finalizeMonitoredSwap(current);
2084
2195
  }
2085
2196
  }
2086
2197
  }, delayMs)
@@ -2089,11 +2200,16 @@ var SwapManager = class _SwapManager {
2089
2200
  /**
2090
2201
  * Execute autonomous action based on swap status
2091
2202
  * Uses locking to prevent race conditions with manual operations
2203
+ *
2204
+ * @returns `false` when another action for this swap already held the lock
2205
+ * and this call did nothing, `true` when it ran. Callers that finalize on
2206
+ * the result must not act on `false`: the in-flight action owns the swap's
2207
+ * outcome and will re-arm or finalize from its own result.
2092
2208
  */
2093
2209
  async executeAutonomousAction(swap) {
2094
2210
  if (this.swapsInProgress.has(swap.id)) {
2095
2211
  logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
2096
- return;
2212
+ return false;
2097
2213
  }
2098
2214
  try {
2099
2215
  this.swapsInProgress.add(swap.id);
@@ -2102,7 +2218,7 @@ var SwapManager = class _SwapManager {
2102
2218
  logger.log(
2103
2219
  `Skipping claim for swap ${swap.id}: missing preimage (restored swap)`
2104
2220
  );
2105
- return;
2221
+ return true;
2106
2222
  }
2107
2223
  if (isReverseClaimableStatus(swap.status)) {
2108
2224
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
@@ -2114,11 +2230,26 @@ var SwapManager = class _SwapManager {
2114
2230
  logger.log(
2115
2231
  `Skipping refund for swap ${swap.id}: missing invoice (restored swap)`
2116
2232
  );
2117
- return;
2233
+ return true;
2118
2234
  }
2119
2235
  if (isSubmarineRefundableStatus(swap.status)) {
2120
2236
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
2121
- await this.executeRefundAction(swap);
2237
+ let outcome;
2238
+ try {
2239
+ outcome = await this.executeRefundAction(swap);
2240
+ } catch (error) {
2241
+ await this.clearRefundRetry(swap);
2242
+ throw error;
2243
+ }
2244
+ if (outcome && outcome.skipped > 0) {
2245
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2246
+ logger.log(
2247
+ `Submarine swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2248
+ );
2249
+ await this.scheduleRefundRetry(swap, delayMs);
2250
+ } else if (outcome) {
2251
+ await this.clearRefundRetry(swap);
2252
+ }
2122
2253
  this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
2123
2254
  }
2124
2255
  } else if (isPendingChainSwap(swap)) {
@@ -2142,10 +2273,13 @@ var SwapManager = class _SwapManager {
2142
2273
  try {
2143
2274
  const outcome = await this.executeRefundArkAction(swap);
2144
2275
  if (outcome && outcome.skipped > 0) {
2276
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2145
2277
  logger.log(
2146
- `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2278
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2147
2279
  );
2148
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2280
+ await this.scheduleRefundRetry(swap, delayMs);
2281
+ } else if (outcome) {
2282
+ await this.clearRefundRetry(swap);
2149
2283
  }
2150
2284
  this.actionExecutedListeners.forEach(
2151
2285
  (listener) => listener(swap, "refundArk")
@@ -2158,7 +2292,7 @@ var SwapManager = class _SwapManager {
2158
2292
  this.swapFailedListeners.forEach(
2159
2293
  (listener) => listener(swap, error)
2160
2294
  );
2161
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2295
+ await this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2162
2296
  }
2163
2297
  }
2164
2298
  if (swap.request.from === "BTC") {
@@ -2191,6 +2325,7 @@ var SwapManager = class _SwapManager {
2191
2325
  } finally {
2192
2326
  this.swapsInProgress.delete(swap.id);
2193
2327
  }
2328
+ return true;
2194
2329
  }
2195
2330
  /**
2196
2331
  * Execute claim action for reverse swap
@@ -2204,13 +2339,15 @@ var SwapManager = class _SwapManager {
2204
2339
  }
2205
2340
  /**
2206
2341
  * Execute refund action for submarine swap
2342
+ *
2343
+ * @returns The refund outcome, or `undefined` if no callback is wired.
2207
2344
  */
2208
2345
  async executeRefundAction(swap) {
2209
2346
  if (!this.refundCallback) {
2210
2347
  logger.error("Refund callback not set");
2211
2348
  return;
2212
2349
  }
2213
- await this.refundCallback(swap);
2350
+ return this.refundCallback(swap);
2214
2351
  }
2215
2352
  /**
2216
2353
  * Execute claim action for chain swap Btc to Ark
@@ -2292,6 +2429,7 @@ var SwapManager = class _SwapManager {
2292
2429
  return;
2293
2430
  }
2294
2431
  for (const swap of this.monitoredSwaps.values()) {
2432
+ if (this.refundRetryTimers.has(swap.id)) continue;
2295
2433
  try {
2296
2434
  if (isPendingReverseSwap(swap) && isReverseClaimableStatus(swap.status)) {
2297
2435
  logger.log(`Resuming claim for swap ${swap.id}`);
@@ -2616,6 +2754,7 @@ var IndexedDbSwapRepository = class {
2616
2754
  constructor(dbName = DEFAULT_DB_NAME) {
2617
2755
  this.dbName = dbName;
2618
2756
  }
2757
+ dbName;
2619
2758
  version = 1;
2620
2759
  db = null;
2621
2760
  async getDB() {
@@ -3166,6 +3305,7 @@ var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3166
3305
  return refundableVtxos.some((vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo));
3167
3306
  };
3168
3307
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3308
+ var CLTV_IMMATURE_RETRY_SEC = 60;
3169
3309
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3170
3310
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3171
3311
  var quoteOptionsForSwap = (swap) => {
@@ -3238,9 +3378,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3238
3378
  claim: async (swap) => {
3239
3379
  await this.claimVHTLC(swap);
3240
3380
  },
3241
- refund: async (swap) => {
3242
- await this.refundVHTLC(swap);
3243
- },
3381
+ refund: (swap) => this.refundVHTLC(swap),
3244
3382
  claimArk: (swap) => this.claimArk(swap),
3245
3383
  claimBtc: (swap) => this.claimBtc(swap),
3246
3384
  refundArk: async (swap) => {
@@ -3846,7 +3984,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3846
3984
  refundWithoutReceiverLeaf,
3847
3985
  outputScript
3848
3986
  };
3849
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
3987
+ const outcome = await this.refundVtxos({
3850
3988
  swapId: pendingSwap.id,
3851
3989
  vtxos: refundableVtxos,
3852
3990
  refundLocktime: vhtlcTimeouts.refund,
@@ -3856,7 +3994,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
3856
3994
  refundViaBoltz: this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3857
3995
  });
3858
3996
  if (!isSubmarineSuccessStatus(pendingSwap.status)) {
3859
- const fullyRefunded = skippedCount === 0;
3997
+ const fullyRefunded = outcome.skipped === 0;
3998
+ pendingSwap.refundable = true;
3999
+ pendingSwap.refunded = fullyRefunded;
3860
4000
  await updateSubmarineSwapStatus(
3861
4001
  pendingSwap,
3862
4002
  pendingSwap.status,
@@ -3865,7 +4005,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3865
4005
  { refundable: true, refunded: fullyRefunded }
3866
4006
  );
3867
4007
  }
3868
- return { swept: sweptCount, skipped: skippedCount };
4008
+ return outcome;
3869
4009
  }
3870
4010
  /**
3871
4011
  * Inspect a submarine swap's lockup address for recoverable funds.
@@ -4482,7 +4622,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4482
4622
  refundWithoutReceiverLeaf,
4483
4623
  outputScript
4484
4624
  };
4485
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4625
+ const outcome = await this.refundVtxos({
4486
4626
  swapId: pendingSwap.id,
4487
4627
  vtxos: unspentVtxos,
4488
4628
  refundLocktime,
@@ -4492,11 +4632,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4492
4632
  refundViaBoltz: this.swapProvider.refundChainSwap.bind(this.swapProvider)
4493
4633
  });
4494
4634
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4495
- await this.savePendingChainSwap({
4496
- ...pendingSwap,
4497
- status: finalStatus.status
4498
- });
4499
- return { swept: sweptCount, skipped: skippedCount };
4635
+ pendingSwap.status = finalStatus.status;
4636
+ await this.savePendingChainSwap(pendingSwap);
4637
+ return outcome;
4500
4638
  }
4501
4639
  // =========================================================================
4502
4640
  // Chain swaps: BTC -> ARK
@@ -5100,18 +5238,44 @@ var ArkadeSwaps = class _ArkadeSwaps {
5100
5238
  );
5101
5239
  }
5102
5240
  }
5241
+ /**
5242
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
5243
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
5244
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
5245
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
5246
+ * already implement. Anything else propagates.
5247
+ *
5248
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
5249
+ * which settle via a batch round.
5250
+ *
5251
+ * @returns `true` if settled, `false` if the server deferred it.
5252
+ */
5253
+ async trySettleRefundWithoutReceiver(swapId, ctx, vtxo) {
5254
+ try {
5255
+ await this.settleRefundWithoutReceiver(ctx, vtxo);
5256
+ return true;
5257
+ } catch (error) {
5258
+ if (!(0, import_sdk8.isArkError)(error, import_sdk8.ArkErrorName.FORFEIT_CLOSURE_LOCKED)) throw error;
5259
+ logger.warn(
5260
+ `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.`
5261
+ );
5262
+ return false;
5263
+ }
5264
+ }
5103
5265
  /**
5104
5266
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
5105
5267
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
5106
5268
  * (chain). Path selection per VTXO:
5107
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
5108
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
5269
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
5270
+ * live VTXO, via a batch round for a swept one — see
5271
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
5272
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
5109
5273
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
5110
5274
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
5111
5275
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
5112
5276
  * locktime has since elapsed.
5113
5277
  *
5114
- * @returns Counts of VTXOs swept vs. deferred.
5278
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
5115
5279
  */
5116
5280
  async refundVtxos(params) {
5117
5281
  const {
@@ -5127,19 +5291,27 @@ var ArkadeSwaps = class _ArkadeSwaps {
5127
5291
  let boltzCallCount = 0;
5128
5292
  let sweptCount = 0;
5129
5293
  let skippedCount = 0;
5294
+ let retryAt;
5295
+ const defer = (candidate) => {
5296
+ skippedCount++;
5297
+ retryAt = retryAt === void 0 ? candidate : Math.min(retryAt, candidate);
5298
+ };
5130
5299
  for (const vtxo of vtxos) {
5131
5300
  const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
5132
5301
  const output = { amount: BigInt(vtxo.value), script: outputScript };
5133
5302
  if (isSubmarineRefundLocktimeReached(refundLocktime)) {
5134
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5135
- sweptCount++;
5303
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5304
+ sweptCount++;
5305
+ } else {
5306
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5307
+ }
5136
5308
  continue;
5137
5309
  }
5138
5310
  if (isRecoverableVtxo) {
5139
5311
  logger.error(
5140
5312
  `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.`
5141
5313
  );
5142
- skippedCount++;
5314
+ defer(refundLocktime);
5143
5315
  continue;
5144
5316
  }
5145
5317
  const input = {
@@ -5173,17 +5345,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
5173
5345
  logger.error(
5174
5346
  `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.`
5175
5347
  );
5176
- skippedCount++;
5348
+ defer(refundLocktime);
5177
5349
  continue;
5178
5350
  }
5179
5351
  logger.warn(
5180
5352
  `Swap ${swapId}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver offchain`
5181
5353
  );
5182
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5183
- sweptCount++;
5354
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5355
+ sweptCount++;
5356
+ } else {
5357
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5358
+ }
5184
5359
  }
5185
5360
  }
5186
- return { swept: sweptCount, skipped: skippedCount };
5361
+ return { swept: sweptCount, skipped: skippedCount, retryAt };
5187
5362
  }
5188
5363
  /**
5189
5364
  * Creates a VHTLC script for the swap.
@@ -5521,6 +5696,8 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
5521
5696
  this.config = config;
5522
5697
  this.swapRepository = inner.swapRepository;
5523
5698
  }
5699
+ inner;
5700
+ config;
5524
5701
  swapRepository;
5525
5702
  foregroundIntervalId;
5526
5703
  /**
@@ -1,7 +1,7 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-BG3xEK31.cjs';
2
- import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, j as ArkToBtcResponse, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-BKEkNZxK.cjs';
3
- import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-CIqTFkV1.cjs';
4
- export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-CIqTFkV1.cjs';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-qZmYvnVy.cjs';
2
+ import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, j as ArkToBtcResponse, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-Db4JSdjk.cjs';
3
+ import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-DCtJYSbJ.cjs';
4
+ export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-DCtJYSbJ.cjs';
5
5
  import { ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
6
6
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
7
7
  import '@arkade-os/sdk/worker/expo';
@@ -1,7 +1,7 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-A180YN8g.js';
2
- import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, j as ArkToBtcResponse, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-BKEkNZxK.js';
3
- import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-C4HQHP9Z.js';
4
- export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-C4HQHP9Z.js';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-CoJKHu2Y.js';
2
+ import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, o as SendLightningPaymentResponse, O as OptimisticSendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, j as ArkToBtcResponse, l as ChainArkRefundOutcome, k as BtcToArkResponse, d as Chain, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-Db4JSdjk.js';
3
+ import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor--BPV48QB.js';
4
+ export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor--BPV48QB.js';
5
5
  import { ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
6
6
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
7
7
  import '@arkade-os/sdk/worker/expo';
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SWAP_POLL_TASK_TYPE
3
- } from "../chunk-HIG2OOAN.js";
3
+ } from "../chunk-76V3TKDL.js";
4
4
  import {
5
5
  ArkadeSwaps
6
- } from "../chunk-VKHFXGKL.js";
6
+ } from "../chunk-CZTZTYZU.js";
7
7
  import "../chunk-SJQJQO7P.js";
8
8
 
9
9
  // src/expo/arkade-lightning.ts
@@ -28,6 +28,8 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
28
28
  this.config = config;
29
29
  this.swapRepository = inner.swapRepository;
30
30
  }
31
+ inner;
32
+ config;
31
33
  swapRepository;
32
34
  foregroundIntervalId;
33
35
  /**