@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.
package/dist/index.cjs CHANGED
@@ -95,7 +95,7 @@ __export(index_exports, {
95
95
  module.exports = __toCommonJS(index_exports);
96
96
 
97
97
  // package.json
98
- var version = "0.3.50";
98
+ var version = "0.3.52";
99
99
 
100
100
  // src/errors.ts
101
101
  var SwapError = class extends Error {
@@ -301,6 +301,7 @@ var BoltzRefundError = class extends Error {
301
301
  this.cause = cause;
302
302
  this.name = "BoltzRefundError";
303
303
  }
304
+ cause;
304
305
  };
305
306
 
306
307
  // src/arkade-swaps.ts
@@ -1127,6 +1128,13 @@ var MusigKeyAgg = class _MusigKeyAgg {
1127
1128
  this.internalKey = internalKey;
1128
1129
  this._tweak = _tweak;
1129
1130
  }
1131
+ privateKey;
1132
+ myPublicKey;
1133
+ publicKeys;
1134
+ myIndex;
1135
+ aggPubkey;
1136
+ internalKey;
1137
+ _tweak;
1130
1138
  xonlyTweakAdd(tweak) {
1131
1139
  if (this._tweak) throw new Error("musig key already tweaked");
1132
1140
  return new _MusigKeyAgg(
@@ -1161,6 +1169,13 @@ var MusigWithMessage = class {
1161
1169
  this.tweak = tweak;
1162
1170
  this.msg = msg;
1163
1171
  }
1172
+ privateKey;
1173
+ myPublicKey;
1174
+ publicKeys;
1175
+ myIndex;
1176
+ aggPubkey;
1177
+ tweak;
1178
+ msg;
1164
1179
  generateNonce() {
1165
1180
  const nonce = (0, import_musig2.nonceGen)(this.myPublicKey, this.privateKey, this.aggPubkey, this.msg);
1166
1181
  return new MusigWithNonce(
@@ -1184,6 +1199,13 @@ var MusigWithNonce = class {
1184
1199
  this.msg = msg;
1185
1200
  this.nonce = nonce;
1186
1201
  }
1202
+ privateKey;
1203
+ myPublicKey;
1204
+ publicKeys;
1205
+ myIndex;
1206
+ tweak;
1207
+ msg;
1208
+ nonce;
1187
1209
  get publicNonce() {
1188
1210
  return this.nonce.public;
1189
1211
  }
@@ -1232,6 +1254,14 @@ var MusigNoncesAggregated = class {
1232
1254
  this.pubNonces = pubNonces;
1233
1255
  this.aggregatedNonce = aggregatedNonce;
1234
1256
  }
1257
+ privateKey;
1258
+ publicKeys;
1259
+ myIndex;
1260
+ tweak;
1261
+ msg;
1262
+ nonce;
1263
+ pubNonces;
1264
+ aggregatedNonce;
1235
1265
  get publicNonce() {
1236
1266
  return this.nonce.public;
1237
1267
  }
@@ -1263,6 +1293,12 @@ var MusigSession = class {
1263
1293
  this.session = session;
1264
1294
  this.partialSignatures = Array(publicKeys.length).fill(null);
1265
1295
  }
1296
+ privateKey;
1297
+ publicKeys;
1298
+ myIndex;
1299
+ nonce;
1300
+ pubNonces;
1301
+ session;
1266
1302
  partialSignatures;
1267
1303
  get publicNonce() {
1268
1304
  return this.nonce.public;
@@ -1290,6 +1326,10 @@ var MusigSigned = class {
1290
1326
  this.ourPartialSignature = ourPartialSignature;
1291
1327
  this.publicNonce = publicNonce;
1292
1328
  }
1329
+ session;
1330
+ partialSignatures;
1331
+ ourPartialSignature;
1332
+ publicNonce;
1293
1333
  aggregatePartials() {
1294
1334
  if (this.partialSignatures.some((s) => s === null)) {
1295
1335
  throw new Error("not all partial signatures are set");
@@ -1571,12 +1611,20 @@ var SwapManager = class _SwapManager {
1571
1611
  */
1572
1612
  static NOT_FOUND_THRESHOLD = 10;
1573
1613
  /**
1574
- * Delay between re-attempts of a chain refund that left VTXOs deferred
1575
- * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1576
- * has elapsed). Boltz won't send another status update once the swap
1577
- * is `swap.expired`, so the manager owns the local retry cadence.
1614
+ * Floor on the delay between re-attempts of a refund that left VTXOs
1615
+ * deferred, and the delay used when the outcome names no retry time. Boltz
1616
+ * won't send another status update once the swap is refundable-and-final,
1617
+ * so the manager owns the local retry cadence.
1578
1618
  */
1579
1619
  static REFUND_RETRY_DELAY_MS = 6e4;
1620
+ /**
1621
+ * Ceiling on that delay. A deferral can be hours out (a pre-CLTV VTXO waits
1622
+ * out the whole refund locktime), and `setTimeout` fires *immediately* past
1623
+ * its 32-bit millisecond range, so long waits are broken into re-armed
1624
+ * hops. Re-attempting costs an indexer lookup and re-defers, which also lets
1625
+ * the cadence recover from a suspended device or a clock jump.
1626
+ */
1627
+ static MAX_REFUND_RETRY_DELAY_MS = 60 * 6e4;
1580
1628
  swapProvider;
1581
1629
  config;
1582
1630
  // Event listeners storage (supports multiple listeners per event)
@@ -1595,10 +1643,9 @@ var SwapManager = class _SwapManager {
1595
1643
  reconnectTimer = null;
1596
1644
  initialPollTimer = null;
1597
1645
  pollRetryTimers = /* @__PURE__ */ new Map();
1598
- // Per-swap retry timers for chain refunds that left work undone
1599
- // (refundArk returned `skipped > 0`). The swap is held in
1600
- // `monitoredSwaps` past its terminal Boltz status until the local
1601
- // refund completes or the manager stops.
1646
+ // Per-swap retry timers for deferred refunds that left local work undone.
1647
+ // The swap is held in `monitoredSwaps` past its terminal Boltz status
1648
+ // until the local refund completes or the manager stops.
1602
1649
  refundRetryTimers = /* @__PURE__ */ new Map();
1603
1650
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1604
1651
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
@@ -1770,11 +1817,12 @@ var SwapManager = class _SwapManager {
1770
1817
  this.initialSwaps.set(swap.id, swap);
1771
1818
  }
1772
1819
  for (const swap of pendingSwaps) {
1773
- if (!this.isFinalStatus(swap)) {
1820
+ if (this.shouldMonitorOnStart(swap)) {
1774
1821
  this.monitoredSwaps.set(swap.id, swap);
1775
1822
  }
1776
1823
  }
1777
1824
  await this.tryConnectWebSocket();
1825
+ this.resumePersistedRefundRetries();
1778
1826
  await this.resumeActionableSwaps();
1779
1827
  }
1780
1828
  /**
@@ -1857,6 +1905,7 @@ var SwapManager = class _SwapManager {
1857
1905
  * Remove a swap from monitoring
1858
1906
  */
1859
1907
  async removeSwap(swapId) {
1908
+ const swap = this.monitoredSwaps.get(swapId);
1860
1909
  this.monitoredSwaps.delete(swapId);
1861
1910
  this.swapSubscriptions.delete(swapId);
1862
1911
  this.chainClaimPromises.delete(swapId);
@@ -1870,6 +1919,9 @@ var SwapManager = class _SwapManager {
1870
1919
  clearTimeout(refundRetryTimer);
1871
1920
  this.refundRetryTimers.delete(swapId);
1872
1921
  }
1922
+ if (swap) {
1923
+ await this.clearRefundRetry(swap);
1924
+ }
1873
1925
  this.notFoundCounts.delete(swapId);
1874
1926
  logger.log(`Removed swap ${swapId} from monitoring`);
1875
1927
  }
@@ -2208,12 +2260,69 @@ var SwapManager = class _SwapManager {
2208
2260
  }
2209
2261
  this.swapCompletedListeners.forEach((listener) => listener(swap));
2210
2262
  }
2263
+ static isRefundRetryableSwap(swap) {
2264
+ return isPendingSubmarineSwap(swap) || isPendingChainSwap(swap);
2265
+ }
2266
+ static hasPendingRefundRetry(swap) {
2267
+ if (!_SwapManager.isRefundRetryableSwap(swap)) return false;
2268
+ return swap.refundRetry?.pending === true && Number.isFinite(swap.refundRetry.nextRetryAt);
2269
+ }
2270
+ shouldMonitorOnStart(swap) {
2271
+ if (!this.isFinalStatus(swap)) return true;
2272
+ return this.config.enableAutoActions === true && _SwapManager.hasPendingRefundRetry(swap);
2273
+ }
2211
2274
  /**
2212
- * Schedule another `executeAutonomousAction` run for a chain swap whose
2213
- * refund left VTXOs deferred. After the retry completes, if no further
2214
- * deferral was reported, finalize monitoring cleanup.
2275
+ * How long to wait before re-attempting a refund, given the `retryAt`
2276
+ * (Unix seconds) the outcome reported. Clamped at both ends: see
2277
+ * {@link SwapManager.REFUND_RETRY_DELAY_MS} and
2278
+ * {@link SwapManager.MAX_REFUND_RETRY_DELAY_MS}.
2215
2279
  */
2216
- scheduleRefundRetry(swap, delayMs) {
2280
+ static refundRetryDelayMs(retryAt) {
2281
+ if (retryAt === void 0) return _SwapManager.REFUND_RETRY_DELAY_MS;
2282
+ return Math.min(
2283
+ Math.max(retryAt * 1e3 - Date.now(), _SwapManager.REFUND_RETRY_DELAY_MS),
2284
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2285
+ );
2286
+ }
2287
+ static persistedRefundRetryDelayMs(nextRetryAt) {
2288
+ return Math.min(
2289
+ Math.max(nextRetryAt * 1e3 - Date.now(), 0),
2290
+ _SwapManager.MAX_REFUND_RETRY_DELAY_MS
2291
+ );
2292
+ }
2293
+ static nextRefundRetryAt(delayMs) {
2294
+ return Math.ceil((Date.now() + delayMs) / 1e3);
2295
+ }
2296
+ async clearRefundRetry(swap) {
2297
+ if (!_SwapManager.isRefundRetryableSwap(swap) || swap.refundRetry === void 0) return;
2298
+ delete swap.refundRetry;
2299
+ await this.saveSwap(swap);
2300
+ }
2301
+ resumePersistedRefundRetries() {
2302
+ if (!this.config.enableAutoActions) return;
2303
+ for (const swap of this.monitoredSwaps.values()) {
2304
+ if (!_SwapManager.hasPendingRefundRetry(swap)) continue;
2305
+ const delayMs = _SwapManager.persistedRefundRetryDelayMs(swap.refundRetry.nextRetryAt);
2306
+ logger.log(
2307
+ `Swap ${swap.id}: resuming deferred refund retry in ${Math.round(delayMs / 1e3)}s`
2308
+ );
2309
+ this.armRefundRetryTimer(swap, delayMs);
2310
+ }
2311
+ }
2312
+ /**
2313
+ * Schedule another `executeAutonomousAction` run for a swap whose refund
2314
+ * left VTXOs deferred. The pending retry and its next attempt time are saved
2315
+ * with the swap so stop/start and application restart can re-arm it.
2316
+ */
2317
+ async scheduleRefundRetry(swap, delayMs) {
2318
+ swap.refundRetry = {
2319
+ pending: true,
2320
+ nextRetryAt: _SwapManager.nextRefundRetryAt(delayMs)
2321
+ };
2322
+ await this.saveSwap(swap);
2323
+ this.armRefundRetryTimer(swap, delayMs);
2324
+ }
2325
+ armRefundRetryTimer(swap, delayMs) {
2217
2326
  const existing = this.refundRetryTimers.get(swap.id);
2218
2327
  if (existing) clearTimeout(existing);
2219
2328
  this.refundRetryTimers.set(
@@ -2221,12 +2330,14 @@ var SwapManager = class _SwapManager {
2221
2330
  setTimeout(async () => {
2222
2331
  this.refundRetryTimers.delete(swap.id);
2223
2332
  if (!this.isRunning) return;
2224
- if (!this.monitoredSwaps.has(swap.id)) return;
2333
+ const current = this.monitoredSwaps.get(swap.id);
2334
+ if (!current) return;
2335
+ let ran = false;
2225
2336
  try {
2226
- await this.executeAutonomousAction(swap);
2337
+ ran = await this.executeAutonomousAction(current);
2227
2338
  } finally {
2228
- if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
2229
- this.finalizeMonitoredSwap(swap);
2339
+ if (ran && !this.refundRetryTimers.has(current.id) && this.isFinalStatus(current)) {
2340
+ this.finalizeMonitoredSwap(current);
2230
2341
  }
2231
2342
  }
2232
2343
  }, delayMs)
@@ -2235,11 +2346,16 @@ var SwapManager = class _SwapManager {
2235
2346
  /**
2236
2347
  * Execute autonomous action based on swap status
2237
2348
  * Uses locking to prevent race conditions with manual operations
2349
+ *
2350
+ * @returns `false` when another action for this swap already held the lock
2351
+ * and this call did nothing, `true` when it ran. Callers that finalize on
2352
+ * the result must not act on `false`: the in-flight action owns the swap's
2353
+ * outcome and will re-arm or finalize from its own result.
2238
2354
  */
2239
2355
  async executeAutonomousAction(swap) {
2240
2356
  if (this.swapsInProgress.has(swap.id)) {
2241
2357
  logger.log(`Swap ${swap.id} is already being processed, skipping autonomous action`);
2242
- return;
2358
+ return false;
2243
2359
  }
2244
2360
  try {
2245
2361
  this.swapsInProgress.add(swap.id);
@@ -2248,7 +2364,7 @@ var SwapManager = class _SwapManager {
2248
2364
  logger.log(
2249
2365
  `Skipping claim for swap ${swap.id}: missing preimage (restored swap)`
2250
2366
  );
2251
- return;
2367
+ return true;
2252
2368
  }
2253
2369
  if (isReverseClaimableStatus(swap.status)) {
2254
2370
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
@@ -2260,11 +2376,26 @@ var SwapManager = class _SwapManager {
2260
2376
  logger.log(
2261
2377
  `Skipping refund for swap ${swap.id}: missing invoice (restored swap)`
2262
2378
  );
2263
- return;
2379
+ return true;
2264
2380
  }
2265
2381
  if (isSubmarineRefundableStatus(swap.status)) {
2266
2382
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
2267
- await this.executeRefundAction(swap);
2383
+ let outcome;
2384
+ try {
2385
+ outcome = await this.executeRefundAction(swap);
2386
+ } catch (error) {
2387
+ await this.clearRefundRetry(swap);
2388
+ throw error;
2389
+ }
2390
+ if (outcome && outcome.skipped > 0) {
2391
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2392
+ logger.log(
2393
+ `Submarine swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2394
+ );
2395
+ await this.scheduleRefundRetry(swap, delayMs);
2396
+ } else if (outcome) {
2397
+ await this.clearRefundRetry(swap);
2398
+ }
2268
2399
  this.actionExecutedListeners.forEach((listener) => listener(swap, "refund"));
2269
2400
  }
2270
2401
  } else if (isPendingChainSwap(swap)) {
@@ -2288,10 +2419,13 @@ var SwapManager = class _SwapManager {
2288
2419
  try {
2289
2420
  const outcome = await this.executeRefundArkAction(swap);
2290
2421
  if (outcome && outcome.skipped > 0) {
2422
+ const delayMs = _SwapManager.refundRetryDelayMs(outcome.retryAt);
2291
2423
  logger.log(
2292
- `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2424
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry in ${Math.round(delayMs / 1e3)}s`
2293
2425
  );
2294
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2426
+ await this.scheduleRefundRetry(swap, delayMs);
2427
+ } else if (outcome) {
2428
+ await this.clearRefundRetry(swap);
2295
2429
  }
2296
2430
  this.actionExecutedListeners.forEach(
2297
2431
  (listener) => listener(swap, "refundArk")
@@ -2304,7 +2438,7 @@ var SwapManager = class _SwapManager {
2304
2438
  this.swapFailedListeners.forEach(
2305
2439
  (listener) => listener(swap, error)
2306
2440
  );
2307
- this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2441
+ await this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2308
2442
  }
2309
2443
  }
2310
2444
  if (swap.request.from === "BTC") {
@@ -2337,6 +2471,7 @@ var SwapManager = class _SwapManager {
2337
2471
  } finally {
2338
2472
  this.swapsInProgress.delete(swap.id);
2339
2473
  }
2474
+ return true;
2340
2475
  }
2341
2476
  /**
2342
2477
  * Execute claim action for reverse swap
@@ -2350,13 +2485,15 @@ var SwapManager = class _SwapManager {
2350
2485
  }
2351
2486
  /**
2352
2487
  * Execute refund action for submarine swap
2488
+ *
2489
+ * @returns The refund outcome, or `undefined` if no callback is wired.
2353
2490
  */
2354
2491
  async executeRefundAction(swap) {
2355
2492
  if (!this.refundCallback) {
2356
2493
  logger.error("Refund callback not set");
2357
2494
  return;
2358
2495
  }
2359
- await this.refundCallback(swap);
2496
+ return this.refundCallback(swap);
2360
2497
  }
2361
2498
  /**
2362
2499
  * Execute claim action for chain swap Btc to Ark
@@ -2438,6 +2575,7 @@ var SwapManager = class _SwapManager {
2438
2575
  return;
2439
2576
  }
2440
2577
  for (const swap of this.monitoredSwaps.values()) {
2578
+ if (this.refundRetryTimers.has(swap.id)) continue;
2441
2579
  try {
2442
2580
  if (isPendingReverseSwap(swap) && isReverseClaimableStatus(swap.status)) {
2443
2581
  logger.log(`Resuming claim for swap ${swap.id}`);
@@ -2769,6 +2907,7 @@ var IndexedDbSwapRepository = class {
2769
2907
  constructor(dbName = DEFAULT_DB_NAME) {
2770
2908
  this.dbName = dbName;
2771
2909
  }
2910
+ dbName;
2772
2911
  version = 1;
2773
2912
  db = null;
2774
2913
  async getDB() {
@@ -3319,6 +3458,7 @@ var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
3319
3458
  return refundableVtxos.some((vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo));
3320
3459
  };
3321
3460
  var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
3461
+ var CLTV_IMMATURE_RETRY_SEC = 60;
3322
3462
  var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
3323
3463
  var CLAIM_VTXO_RETRY_DELAY_MS = 500;
3324
3464
  var quoteOptionsForSwap = (swap) => {
@@ -3391,9 +3531,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3391
3531
  claim: async (swap) => {
3392
3532
  await this.claimVHTLC(swap);
3393
3533
  },
3394
- refund: async (swap) => {
3395
- await this.refundVHTLC(swap);
3396
- },
3534
+ refund: (swap) => this.refundVHTLC(swap),
3397
3535
  claimArk: (swap) => this.claimArk(swap),
3398
3536
  claimBtc: (swap) => this.claimBtc(swap),
3399
3537
  refundArk: async (swap) => {
@@ -3999,7 +4137,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3999
4137
  refundWithoutReceiverLeaf,
4000
4138
  outputScript
4001
4139
  };
4002
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4140
+ const outcome = await this.refundVtxos({
4003
4141
  swapId: pendingSwap.id,
4004
4142
  vtxos: refundableVtxos,
4005
4143
  refundLocktime: vhtlcTimeouts.refund,
@@ -4009,7 +4147,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4009
4147
  refundViaBoltz: this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
4010
4148
  });
4011
4149
  if (!isSubmarineSuccessStatus(pendingSwap.status)) {
4012
- const fullyRefunded = skippedCount === 0;
4150
+ const fullyRefunded = outcome.skipped === 0;
4151
+ pendingSwap.refundable = true;
4152
+ pendingSwap.refunded = fullyRefunded;
4013
4153
  await updateSubmarineSwapStatus(
4014
4154
  pendingSwap,
4015
4155
  pendingSwap.status,
@@ -4018,7 +4158,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4018
4158
  { refundable: true, refunded: fullyRefunded }
4019
4159
  );
4020
4160
  }
4021
- return { swept: sweptCount, skipped: skippedCount };
4161
+ return outcome;
4022
4162
  }
4023
4163
  /**
4024
4164
  * Inspect a submarine swap's lockup address for recoverable funds.
@@ -4635,7 +4775,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4635
4775
  refundWithoutReceiverLeaf,
4636
4776
  outputScript
4637
4777
  };
4638
- const { swept: sweptCount, skipped: skippedCount } = await this.refundVtxos({
4778
+ const outcome = await this.refundVtxos({
4639
4779
  swapId: pendingSwap.id,
4640
4780
  vtxos: unspentVtxos,
4641
4781
  refundLocktime,
@@ -4645,11 +4785,9 @@ var ArkadeSwaps = class _ArkadeSwaps {
4645
4785
  refundViaBoltz: this.swapProvider.refundChainSwap.bind(this.swapProvider)
4646
4786
  });
4647
4787
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4648
- await this.savePendingChainSwap({
4649
- ...pendingSwap,
4650
- status: finalStatus.status
4651
- });
4652
- return { swept: sweptCount, skipped: skippedCount };
4788
+ pendingSwap.status = finalStatus.status;
4789
+ await this.savePendingChainSwap(pendingSwap);
4790
+ return outcome;
4653
4791
  }
4654
4792
  // =========================================================================
4655
4793
  // Chain swaps: BTC -> ARK
@@ -5253,18 +5391,44 @@ var ArkadeSwaps = class _ArkadeSwaps {
5253
5391
  );
5254
5392
  }
5255
5393
  }
5394
+ /**
5395
+ * {@link settleRefundWithoutReceiver}, deferring instead of failing when the server
5396
+ * rejects the spend as CLTV-immature ({@link ArkErrorName.FORFEIT_CLOSURE_LOCKED}):
5397
+ * we gate on the local wall clock, the server on the chain tip's, which lags. The
5398
+ * server-authoritative sibling of the wall-clock deferral the pre-CLTV branches
5399
+ * already implement. Anything else propagates.
5400
+ *
5401
+ * Only `submitTx` can raise it, so this is a pass-through for recoverable VTXOs,
5402
+ * which settle via a batch round.
5403
+ *
5404
+ * @returns `true` if settled, `false` if the server deferred it.
5405
+ */
5406
+ async trySettleRefundWithoutReceiver(swapId, ctx, vtxo) {
5407
+ try {
5408
+ await this.settleRefundWithoutReceiver(ctx, vtxo);
5409
+ return true;
5410
+ } catch (error) {
5411
+ if (!(0, import_sdk8.isArkError)(error, import_sdk8.ArkErrorName.FORFEIT_CLOSURE_LOCKED)) throw error;
5412
+ logger.warn(
5413
+ `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.`
5414
+ );
5415
+ return false;
5416
+ }
5417
+ }
5256
5418
  /**
5257
5419
  * Refund every VTXO at a swap's VHTLC address back to the wallet, shared by
5258
5420
  * {@link ArkadeSwaps.refundVHTLC} (submarine) and {@link ArkadeSwaps.refundArk}
5259
5421
  * (chain). Path selection per VTXO:
5260
- * - CLTV elapsed → `refundWithoutReceiver` (offchain for a live VTXO, via a
5261
- * batch round for a swept one — see {@link settleRefundWithoutReceiver}).
5422
+ * - CLTV elapsed by our wall clock → `refundWithoutReceiver` (offchain for a
5423
+ * live VTXO, via a batch round for a swept one — see
5424
+ * {@link settleRefundWithoutReceiver}), or skipped if the server defers it as
5425
+ * still immature (see {@link trySettleRefundWithoutReceiver}).
5262
5426
  * - Pre-CLTV recoverable → skipped (Boltz can't co-sign a swept-batch refund).
5263
5427
  * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz, falling
5264
5428
  * back to `refundWithoutReceiver` offchain if Boltz rejects after the
5265
5429
  * locktime has since elapsed.
5266
5430
  *
5267
- * @returns Counts of VTXOs swept vs. deferred.
5431
+ * @returns Counts of VTXOs swept vs. deferred, and when to retry the latter.
5268
5432
  */
5269
5433
  async refundVtxos(params) {
5270
5434
  const {
@@ -5280,19 +5444,27 @@ var ArkadeSwaps = class _ArkadeSwaps {
5280
5444
  let boltzCallCount = 0;
5281
5445
  let sweptCount = 0;
5282
5446
  let skippedCount = 0;
5447
+ let retryAt;
5448
+ const defer = (candidate) => {
5449
+ skippedCount++;
5450
+ retryAt = retryAt === void 0 ? candidate : Math.min(retryAt, candidate);
5451
+ };
5283
5452
  for (const vtxo of vtxos) {
5284
5453
  const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
5285
5454
  const output = { amount: BigInt(vtxo.value), script: outputScript };
5286
5455
  if (isSubmarineRefundLocktimeReached(refundLocktime)) {
5287
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5288
- sweptCount++;
5456
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5457
+ sweptCount++;
5458
+ } else {
5459
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5460
+ }
5289
5461
  continue;
5290
5462
  }
5291
5463
  if (isRecoverableVtxo) {
5292
5464
  logger.error(
5293
5465
  `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.`
5294
5466
  );
5295
- skippedCount++;
5467
+ defer(refundLocktime);
5296
5468
  continue;
5297
5469
  }
5298
5470
  const input = {
@@ -5326,17 +5498,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
5326
5498
  logger.error(
5327
5499
  `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.`
5328
5500
  );
5329
- skippedCount++;
5501
+ defer(refundLocktime);
5330
5502
  continue;
5331
5503
  }
5332
5504
  logger.warn(
5333
5505
  `Swap ${swapId}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver offchain`
5334
5506
  );
5335
- await this.settleRefundWithoutReceiver(refundContext, vtxo);
5336
- sweptCount++;
5507
+ if (await this.trySettleRefundWithoutReceiver(swapId, refundContext, vtxo)) {
5508
+ sweptCount++;
5509
+ } else {
5510
+ defer(Math.floor(Date.now() / 1e3) + CLTV_IMMATURE_RETRY_SEC);
5511
+ }
5337
5512
  }
5338
5513
  }
5339
- return { swept: sweptCount, skipped: skippedCount };
5514
+ return { swept: sweptCount, skipped: skippedCount, retryAt };
5340
5515
  }
5341
5516
  /**
5342
5517
  * Creates a VHTLC script for the swap.
@@ -5691,6 +5866,7 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
5691
5866
  constructor(swapRepository) {
5692
5867
  this.swapRepository = swapRepository;
5693
5868
  }
5869
+ swapRepository;
5694
5870
  static messageTag = DEFAULT_MESSAGE_TAG;
5695
5871
  messageTag = _ArkadeSwapsMessageHandler.messageTag;
5696
5872
  arkProvider;
@@ -6248,6 +6424,10 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
6248
6424
  this.swapRepository = swapRepository;
6249
6425
  this.withSwapManager = withSwapManager;
6250
6426
  }
6427
+ messageTag;
6428
+ serviceWorker;
6429
+ swapRepository;
6430
+ withSwapManager;
6251
6431
  eventListenerInitialized = false;
6252
6432
  swapUpdateListeners = /* @__PURE__ */ new Set();
6253
6433
  swapCompletedListeners = /* @__PURE__ */ new Set();
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-BG3xEK31.cjs';
2
- export { A as ArkadeSwaps } from './arkade-swaps-BG3xEK31.cjs';
3
- import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, O as OptimisticSendLightningPaymentResponse, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, j as ArkToBtcResponse, k as BtcToArkResponse, l as ChainArkRefundOutcome, m as SwapRepository, n as SwapManagerClient, o as SendLightningPaymentResponse, p as GetSwapsFilter } from './types-BKEkNZxK.cjs';
4
- export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineProgressionStatus, x as SubmarineRecoveryStatus, y as SwapManager, z as SwapManagerCallbacks, E as SwapManagerConfig, H as SwapManagerEvents, V as Vtxo, J as hasSubmarineStatusReached, K as isChainClaimableStatus, M as isChainFailedStatus, Q as isChainFinalStatus, R as isChainPendingStatus, T as isChainRefundableStatus, U as isChainSignableStatus, W as isChainSuccessStatus, X as isChainSwapClaimable, Y as isChainSwapRefundable, Z as isPendingChainSwap, _ as isPendingReverseSwap, $ as isPendingSubmarineSwap, a0 as isReverseClaimableStatus, a1 as isReverseFailedStatus, a2 as isReverseFinalStatus, a3 as isReversePendingStatus, a4 as isReverseSuccessStatus, a5 as isReverseSwapClaimable, a6 as isSubmarineFailedStatus, a7 as isSubmarineFinalStatus, a8 as isSubmarinePendingStatus, a9 as isSubmarineRefundableStatus, aa as isSubmarineSuccessStatus, ab as isSubmarineSwapRefundable } from './types-BKEkNZxK.cjs';
1
+ import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-qZmYvnVy.cjs';
2
+ export { A as ArkadeSwaps } from './arkade-swaps-qZmYvnVy.cjs';
3
+ import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, O as OptimisticSendLightningPaymentResponse, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, j as ArkToBtcResponse, k as BtcToArkResponse, l as ChainArkRefundOutcome, m as SwapRepository, n as SwapManagerClient, o as SendLightningPaymentResponse, p as GetSwapsFilter } from './types-Db4JSdjk.cjs';
4
+ export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineProgressionStatus, x as SubmarineRecoveryStatus, y as SwapManager, z as SwapManagerCallbacks, E as SwapManagerConfig, H as SwapManagerEvents, V as Vtxo, J as hasSubmarineStatusReached, K as isChainClaimableStatus, M as isChainFailedStatus, Q as isChainFinalStatus, R as isChainPendingStatus, T as isChainRefundableStatus, U as isChainSignableStatus, W as isChainSuccessStatus, X as isChainSwapClaimable, Y as isChainSwapRefundable, Z as isPendingChainSwap, _ as isPendingReverseSwap, $ as isPendingSubmarineSwap, a0 as isReverseClaimableStatus, a1 as isReverseFailedStatus, a2 as isReverseFinalStatus, a3 as isReversePendingStatus, a4 as isReverseSuccessStatus, a5 as isReverseSwapClaimable, a6 as isSubmarineFailedStatus, a7 as isSubmarineFinalStatus, a8 as isSubmarinePendingStatus, a9 as isSubmarineRefundableStatus, aa as isSubmarineSuccessStatus, ab as isSubmarineSwapRefundable } from './types-Db4JSdjk.cjs';
5
5
  import { Transaction } from '@scure/btc-signer';
6
6
  import { MessageHandler, RequestEnvelope, ArkInfo, ResponseEnvelope, IWallet, IReadonlyWallet, VHTLC, Identity, ArkTxInput } from '@arkade-os/sdk';
7
7
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-A180YN8g.js';
2
- export { A as ArkadeSwaps } from './arkade-swaps-A180YN8g.js';
3
- import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, O as OptimisticSendLightningPaymentResponse, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, j as ArkToBtcResponse, k as BtcToArkResponse, l as ChainArkRefundOutcome, m as SwapRepository, n as SwapManagerClient, o as SendLightningPaymentResponse, p as GetSwapsFilter } from './types-BKEkNZxK.js';
4
- export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineProgressionStatus, x as SubmarineRecoveryStatus, y as SwapManager, z as SwapManagerCallbacks, E as SwapManagerConfig, H as SwapManagerEvents, V as Vtxo, J as hasSubmarineStatusReached, K as isChainClaimableStatus, M as isChainFailedStatus, Q as isChainFinalStatus, R as isChainPendingStatus, T as isChainRefundableStatus, U as isChainSignableStatus, W as isChainSuccessStatus, X as isChainSwapClaimable, Y as isChainSwapRefundable, Z as isPendingChainSwap, _ as isPendingReverseSwap, $ as isPendingSubmarineSwap, a0 as isReverseClaimableStatus, a1 as isReverseFailedStatus, a2 as isReverseFinalStatus, a3 as isReversePendingStatus, a4 as isReverseSuccessStatus, a5 as isReverseSwapClaimable, a6 as isSubmarineFailedStatus, a7 as isSubmarineFinalStatus, a8 as isSubmarinePendingStatus, a9 as isSubmarineRefundableStatus, aa as isSubmarineSuccessStatus, ab as isSubmarineSwapRefundable } from './types-BKEkNZxK.js';
1
+ import { Q as QuoteSwapOptions, I as IArkadeSwaps, V as VhtlcTimeouts } from './arkade-swaps-CoJKHu2Y.js';
2
+ export { A as ArkadeSwaps } from './arkade-swaps-CoJKHu2Y.js';
3
+ import { B as BoltzSwap, D as DecodedInvoice, a as BoltzChainSwap, b as BoltzReverseSwap, c as BoltzSubmarineSwap, A as ArkadeSwapsConfig, N as Network, C as CreateLightningInvoiceRequest, S as SendLightningPaymentRequest, F as FeesResponse, d as Chain, e as CreateLightningInvoiceResponse, O as OptimisticSendLightningPaymentResponse, f as SubmarineRefundOutcome, g as SubmarineRecoveryInfo, h as SubmarineRecoveryResult, i as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, j as ArkToBtcResponse, k as BtcToArkResponse, l as ChainArkRefundOutcome, m as SwapRepository, n as SwapManagerClient, o as SendLightningPaymentResponse, p as GetSwapsFilter } from './types-Db4JSdjk.js';
4
+ export { q as ArkadeSwapsCreateConfig, r as BoltzSwapProvider, s as BoltzSwapStatus, I as IncomingPaymentSubscription, P as PendingChainSwap, t as PendingReverseSwap, u as PendingSubmarineSwap, v as PendingSwap, w as SubmarineProgressionStatus, x as SubmarineRecoveryStatus, y as SwapManager, z as SwapManagerCallbacks, E as SwapManagerConfig, H as SwapManagerEvents, V as Vtxo, J as hasSubmarineStatusReached, K as isChainClaimableStatus, M as isChainFailedStatus, Q as isChainFinalStatus, R as isChainPendingStatus, T as isChainRefundableStatus, U as isChainSignableStatus, W as isChainSuccessStatus, X as isChainSwapClaimable, Y as isChainSwapRefundable, Z as isPendingChainSwap, _ as isPendingReverseSwap, $ as isPendingSubmarineSwap, a0 as isReverseClaimableStatus, a1 as isReverseFailedStatus, a2 as isReverseFinalStatus, a3 as isReversePendingStatus, a4 as isReverseSuccessStatus, a5 as isReverseSwapClaimable, a6 as isSubmarineFailedStatus, a7 as isSubmarineFinalStatus, a8 as isSubmarinePendingStatus, a9 as isSubmarineRefundableStatus, aa as isSubmarineSuccessStatus, ab as isSubmarineSwapRefundable } from './types-Db4JSdjk.js';
5
5
  import { Transaction } from '@scure/btc-signer';
6
6
  import { MessageHandler, RequestEnvelope, ArkInfo, ResponseEnvelope, IWallet, IReadonlyWallet, VHTLC, Identity, ArkTxInput } from '@arkade-os/sdk';
7
7
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
package/dist/index.js CHANGED
@@ -53,14 +53,14 @@ import {
53
53
  updateReverseSwapStatus,
54
54
  updateSubmarineSwapStatus,
55
55
  verifySignatures
56
- } from "./chunk-VKHFXGKL.js";
56
+ } from "./chunk-CZTZTYZU.js";
57
57
  import {
58
58
  applyCreatedAtOrder,
59
59
  applySwapsFilter
60
60
  } from "./chunk-SJQJQO7P.js";
61
61
 
62
62
  // package.json
63
- var version = "0.3.50";
63
+ var version = "0.3.52";
64
64
 
65
65
  // src/serviceWorker/arkade-swaps-message-handler.ts
66
66
  import { RestArkProvider, RestIndexerProvider } from "@arkade-os/sdk";
@@ -102,6 +102,7 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
102
102
  constructor(swapRepository) {
103
103
  this.swapRepository = swapRepository;
104
104
  }
105
+ swapRepository;
105
106
  static messageTag = DEFAULT_MESSAGE_TAG;
106
107
  messageTag = _ArkadeSwapsMessageHandler.messageTag;
107
108
  arkProvider;
@@ -663,6 +664,10 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
663
664
  this.swapRepository = swapRepository;
664
665
  this.withSwapManager = withSwapManager;
665
666
  }
667
+ messageTag;
668
+ serviceWorker;
669
+ swapRepository;
670
+ withSwapManager;
666
671
  eventListenerInitialized = false;
667
672
  swapUpdateListeners = /* @__PURE__ */ new Set();
668
673
  swapCompletedListeners = /* @__PURE__ */ new Set();
@@ -37,6 +37,7 @@ var RealmSwapRepository = class {
37
37
  constructor(realm) {
38
38
  this.realm = realm;
39
39
  }
40
+ realm;
40
41
  version = 1;
41
42
  // ── Lifecycle ──────────────────────────────────────────────────────
42
43
  async ensureInit() {
@@ -1,5 +1,5 @@
1
1
  import { RealmLike } from '@arkade-os/sdk/repositories/realm';
2
- import { m as SwapRepository, B as BoltzSwap, p as GetSwapsFilter } from '../../types-BKEkNZxK.cjs';
2
+ import { m as SwapRepository, B as BoltzSwap, p as GetSwapsFilter } from '../../types-Db4JSdjk.cjs';
3
3
  import '@arkade-os/sdk';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { RealmLike } from '@arkade-os/sdk/repositories/realm';
2
- import { m as SwapRepository, B as BoltzSwap, p as GetSwapsFilter } from '../../types-BKEkNZxK.js';
2
+ import { m as SwapRepository, B as BoltzSwap, p as GetSwapsFilter } from '../../types-Db4JSdjk.js';
3
3
  import '@arkade-os/sdk';
4
4
 
5
5
  /**
@@ -7,6 +7,7 @@ var RealmSwapRepository = class {
7
7
  constructor(realm) {
8
8
  this.realm = realm;
9
9
  }
10
+ realm;
10
11
  version = 1;
11
12
  // ── Lifecycle ──────────────────────────────────────────────────────
12
13
  async ensureInit() {