@arkade-os/boltz-swap 0.3.33 → 0.3.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ArkadeSwapsMessageHandler: () => ArkadeSwapsMessageHandler,
36
36
  BoltzRefundError: () => BoltzRefundError,
37
37
  BoltzSwapProvider: () => BoltzSwapProvider,
38
+ InMemorySwapRepository: () => InMemorySwapRepository,
38
39
  IndexedDbSwapRepository: () => IndexedDbSwapRepository,
39
40
  InsufficientFundsError: () => InsufficientFundsError,
40
41
  InvoiceExpiredError: () => InvoiceExpiredError,
@@ -215,6 +216,8 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
215
216
  return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
216
217
  case "non_positive":
217
218
  return `Boltz quote ${options.quotedAmount} is not positive`;
219
+ case "non_safe_integer":
220
+ return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
218
221
  case "no_baseline":
219
222
  return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
220
223
  }
@@ -268,6 +271,7 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
268
271
  message
269
272
  });
270
273
  case "non_positive":
274
+ case "non_safe_integer":
271
275
  if (quotedAmount === null) return null;
272
276
  return new _QuoteRejectedError({
273
277
  reason,
@@ -283,6 +287,7 @@ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
283
287
  var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
284
288
  "below_floor",
285
289
  "non_positive",
290
+ "non_safe_integer",
286
291
  "no_baseline"
287
292
  ]);
288
293
  var BoltzRefundError = class extends Error {
@@ -1513,6 +1518,13 @@ var SwapManager = class _SwapManager {
1513
1518
  * enough that a real "swap unknown to this provider" surfaces quickly.
1514
1519
  */
1515
1520
  static NOT_FOUND_THRESHOLD = 10;
1521
+ /**
1522
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1523
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1524
+ * has elapsed). Boltz won't send another status update once the swap
1525
+ * is `swap.expired`, so the manager owns the local retry cadence.
1526
+ */
1527
+ static REFUND_RETRY_DELAY_MS = 6e4;
1516
1528
  swapProvider;
1517
1529
  config;
1518
1530
  // Event listeners storage (supports multiple listeners per event)
@@ -1531,6 +1543,11 @@ var SwapManager = class _SwapManager {
1531
1543
  reconnectTimer = null;
1532
1544
  initialPollTimer = null;
1533
1545
  pollRetryTimers = /* @__PURE__ */ new Map();
1546
+ // Per-swap retry timers for chain refunds that left work undone
1547
+ // (refundArk returned `skipped > 0`). The swap is held in
1548
+ // `monitoredSwaps` past its terminal Boltz status until the local
1549
+ // refund completes or the manager stops.
1550
+ refundRetryTimers = /* @__PURE__ */ new Map();
1534
1551
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1535
1552
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1536
1553
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1727,6 +1744,10 @@ var SwapManager = class _SwapManager {
1727
1744
  clearTimeout(timer);
1728
1745
  }
1729
1746
  this.pollRetryTimers.clear();
1747
+ for (const timer of this.refundRetryTimers.values()) {
1748
+ clearTimeout(timer);
1749
+ }
1750
+ this.refundRetryTimers.clear();
1730
1751
  this.notFoundCounts.clear();
1731
1752
  }
1732
1753
  /**
@@ -1783,6 +1804,11 @@ var SwapManager = class _SwapManager {
1783
1804
  clearTimeout(retryTimer);
1784
1805
  this.pollRetryTimers.delete(swapId);
1785
1806
  }
1807
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1808
+ if (refundRetryTimer) {
1809
+ clearTimeout(refundRetryTimer);
1810
+ this.refundRetryTimers.delete(swapId);
1811
+ }
1786
1812
  this.notFoundCounts.delete(swapId);
1787
1813
  logger.log(`Removed swap ${swapId} from monitoring`);
1788
1814
  }
@@ -2054,16 +2080,58 @@ var SwapManager = class _SwapManager {
2054
2080
  await this.executeAutonomousAction(swap);
2055
2081
  }
2056
2082
  if (this.isFinalStatus(swap)) {
2057
- this.monitoredSwaps.delete(swap.id);
2058
- this.swapSubscriptions.delete(swap.id);
2059
- const retryTimer = this.pollRetryTimers.get(swap.id);
2060
- if (retryTimer) {
2061
- clearTimeout(retryTimer);
2062
- this.pollRetryTimers.delete(swap.id);
2083
+ if (this.refundRetryTimers.has(swap.id)) {
2084
+ return;
2063
2085
  }
2064
- this.swapCompletedListeners.forEach((listener) => listener(swap));
2086
+ this.finalizeMonitoredSwap(swap);
2065
2087
  }
2066
2088
  }
2089
+ /**
2090
+ * Drop a swap from monitoring and emit the terminal completion event.
2091
+ * Shared between the on-status-update finalization path and the
2092
+ * refund-retry finalization path (used when a previously-deferred
2093
+ * chain refund has finished its remaining work).
2094
+ */
2095
+ finalizeMonitoredSwap(swap) {
2096
+ if (!this.monitoredSwaps.has(swap.id)) return;
2097
+ this.monitoredSwaps.delete(swap.id);
2098
+ this.swapSubscriptions.delete(swap.id);
2099
+ const retryTimer = this.pollRetryTimers.get(swap.id);
2100
+ if (retryTimer) {
2101
+ clearTimeout(retryTimer);
2102
+ this.pollRetryTimers.delete(swap.id);
2103
+ }
2104
+ const refundRetry = this.refundRetryTimers.get(swap.id);
2105
+ if (refundRetry) {
2106
+ clearTimeout(refundRetry);
2107
+ this.refundRetryTimers.delete(swap.id);
2108
+ }
2109
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
2110
+ }
2111
+ /**
2112
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
2113
+ * refund left VTXOs deferred. After the retry completes, if no further
2114
+ * deferral was reported, finalize monitoring cleanup.
2115
+ */
2116
+ scheduleRefundRetry(swap, delayMs) {
2117
+ const existing = this.refundRetryTimers.get(swap.id);
2118
+ if (existing) clearTimeout(existing);
2119
+ this.refundRetryTimers.set(
2120
+ swap.id,
2121
+ setTimeout(async () => {
2122
+ this.refundRetryTimers.delete(swap.id);
2123
+ if (!this.isRunning) return;
2124
+ if (!this.monitoredSwaps.has(swap.id)) return;
2125
+ try {
2126
+ await this.executeAutonomousAction(swap);
2127
+ } finally {
2128
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
2129
+ this.finalizeMonitoredSwap(swap);
2130
+ }
2131
+ }
2132
+ }, delayMs)
2133
+ );
2134
+ }
2067
2135
  /**
2068
2136
  * Execute autonomous action based on swap status
2069
2137
  * Uses locking to prevent race conditions with manual operations
@@ -2117,10 +2185,27 @@ var SwapManager = class _SwapManager {
2117
2185
  } else if (isChainRefundableStatus(swap.status)) {
2118
2186
  if (swap.request.from === "ARK") {
2119
2187
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
2120
- await this.executeRefundArkAction(swap);
2121
- this.actionExecutedListeners.forEach(
2122
- (listener) => listener(swap, "refundArk")
2123
- );
2188
+ try {
2189
+ const outcome = await this.executeRefundArkAction(swap);
2190
+ if (outcome && outcome.skipped > 0) {
2191
+ logger.log(
2192
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2193
+ );
2194
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2195
+ }
2196
+ this.actionExecutedListeners.forEach(
2197
+ (listener) => listener(swap, "refundArk")
2198
+ );
2199
+ } catch (error) {
2200
+ logger.error(
2201
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2202
+ error
2203
+ );
2204
+ this.swapFailedListeners.forEach(
2205
+ (listener) => listener(swap, error)
2206
+ );
2207
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2208
+ }
2124
2209
  }
2125
2210
  if (swap.request.from === "BTC") {
2126
2211
  logger.warn(
@@ -2201,7 +2286,7 @@ var SwapManager = class _SwapManager {
2201
2286
  logger.error("refundArk callback not set");
2202
2287
  return;
2203
2288
  }
2204
- await this.refundArkCallback(swap);
2289
+ return this.refundArkCallback(swap);
2205
2290
  }
2206
2291
  /**
2207
2292
  * Execute sign server claim action for chain swap.
@@ -2301,9 +2386,7 @@ var SwapManager = class _SwapManager {
2301
2386
  */
2302
2387
  async pollAllSwaps() {
2303
2388
  if (this.monitoredSwaps.size === 0) return;
2304
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2305
- (swap) => this.pollSingleSwap(swap)
2306
- );
2389
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2307
2390
  await Promise.allSettled(pollPromises);
2308
2391
  }
2309
2392
  async pollSingleSwap(swap) {
@@ -2356,6 +2439,7 @@ var SwapManager = class _SwapManager {
2356
2439
  * Boltz endpoint).
2357
2440
  */
2358
2441
  async handleSwapNotFound(swap) {
2442
+ if (this.refundRetryTimers.has(swap.id)) return;
2359
2443
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2360
2444
  this.notFoundCounts.set(swap.id, count);
2361
2445
  logger.warn(
@@ -2376,6 +2460,7 @@ var SwapManager = class _SwapManager {
2376
2460
  * 404s without recovering anything.
2377
2461
  */
2378
2462
  async markSwapAsUnknownToProvider(swap) {
2463
+ if (this.refundRetryTimers.has(swap.id)) return;
2379
2464
  if (!this.monitoredSwaps.has(swap.id)) {
2380
2465
  this.notFoundCounts.delete(swap.id);
2381
2466
  return;
@@ -2388,6 +2473,11 @@ var SwapManager = class _SwapManager {
2388
2473
  clearTimeout(retryTimer);
2389
2474
  this.pollRetryTimers.delete(swap.id);
2390
2475
  }
2476
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2477
+ if (refundRetryTimer) {
2478
+ clearTimeout(refundRetryTimer);
2479
+ this.refundRetryTimers.delete(swap.id);
2480
+ }
2391
2481
  this.notFoundCounts.delete(swap.id);
2392
2482
  this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2393
2483
  const subscribers = this.swapSubscriptions.get(swap.id);
@@ -3145,7 +3235,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3145
3235
  await this.claimBtc(swap);
3146
3236
  },
3147
3237
  refundArk: async (swap) => {
3148
- await this.refundArk(swap);
3238
+ return this.refundArk(swap);
3149
3239
  },
3150
3240
  signServerClaim: async (swap) => {
3151
3241
  await this.signCooperativeClaimForServer(swap);
@@ -3358,51 +3448,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3358
3448
  throw new Error(
3359
3449
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3360
3450
  );
3361
- let vtxo;
3451
+ let unspentVtxos = [];
3452
+ let rawVtxos = [];
3362
3453
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3363
- const { vtxos } = await this.indexerProvider.getVtxos({
3454
+ const result = await this.indexerProvider.getVtxos({
3364
3455
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3365
3456
  });
3366
- if (vtxos.length > 0) {
3367
- vtxo = vtxos[0];
3457
+ rawVtxos = result.vtxos;
3458
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3459
+ if (unspentVtxos.length > 0) {
3368
3460
  break;
3369
3461
  }
3370
3462
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3371
3463
  await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3372
3464
  }
3373
3465
  }
3374
- if (!vtxo) {
3375
- throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3376
- }
3377
- if (vtxo.isSpent) {
3466
+ if (unspentVtxos.length === 0) {
3467
+ if (rawVtxos.length === 0) {
3468
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3469
+ }
3378
3470
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3379
3471
  }
3380
- const input = {
3381
- ...vtxo,
3382
- tapLeafScript: vhtlcScript.claim(),
3383
- tapTree: vhtlcScript.encode()
3384
- };
3385
- const output = {
3386
- amount: BigInt(vtxo.value),
3387
- script: import_sdk8.ArkAddress.decode(address).pkScript
3388
- };
3389
3472
  const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
3390
- let finalStatus;
3391
- if ((0, import_sdk8.isRecoverable)(vtxo)) {
3392
- await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3393
- finalStatus = "transaction.claimed";
3394
- } else {
3395
- await claimVHTLCwithOffchainTx(
3396
- vhtlcIdentity,
3397
- vhtlcScript,
3398
- serverXOnly,
3399
- input,
3400
- output,
3401
- arkInfo,
3402
- this.arkProvider
3473
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3474
+ const claimErrors = [];
3475
+ let usedOffchainClaim = false;
3476
+ for (const vtxo of unspentVtxos) {
3477
+ const input = {
3478
+ ...vtxo,
3479
+ tapLeafScript: vhtlcScript.claim(),
3480
+ tapTree: vhtlcScript.encode()
3481
+ };
3482
+ const output = {
3483
+ amount: BigInt(vtxo.value),
3484
+ script: outputScript
3485
+ };
3486
+ try {
3487
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3488
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3489
+ } else {
3490
+ await claimVHTLCwithOffchainTx(
3491
+ vhtlcIdentity,
3492
+ vhtlcScript,
3493
+ serverXOnly,
3494
+ input,
3495
+ output,
3496
+ arkInfo,
3497
+ this.arkProvider
3498
+ );
3499
+ usedOffchainClaim = true;
3500
+ }
3501
+ } catch (error) {
3502
+ claimErrors.push({ vtxo, error });
3503
+ }
3504
+ }
3505
+ if (claimErrors.length > 0) {
3506
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3507
+ throw new Error(
3508
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3403
3509
  );
3404
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3405
3510
  }
3511
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3406
3512
  await updateReverseSwapStatus(
3407
3513
  pendingSwap,
3408
3514
  finalStatus,
@@ -3777,6 +3883,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3777
3883
  if (boltzCallCount > 0) {
3778
3884
  await new Promise((r) => setTimeout(r, 2e3));
3779
3885
  }
3886
+ boltzCallCount++;
3780
3887
  await refundVHTLCwithOffchainTx(
3781
3888
  pendingSwap.id,
3782
3889
  this.wallet.identity,
@@ -3789,7 +3896,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3789
3896
  arkInfo,
3790
3897
  this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3791
3898
  );
3792
- boltzCallCount++;
3793
3899
  sweptCount++;
3794
3900
  } catch (error) {
3795
3901
  if (!(error instanceof BoltzRefundError)) {
@@ -4287,8 +4393,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4287
4393
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4288
4394
  }
4289
4395
  /**
4290
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4396
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4397
+ * swap's ARK lockup address.
4398
+ *
4399
+ * Path selection per VTXO:
4400
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4401
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4402
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4403
+ *
4291
4404
  * @param pendingSwap - The pending chain swap to refund.
4405
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4406
+ * the call was a no-op — callers should retry after CLTV.
4292
4407
  */
4293
4408
  async refundArk(pendingSwap) {
4294
4409
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
@@ -4312,21 +4427,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4312
4427
  "boltz",
4313
4428
  pendingSwap.id
4314
4429
  );
4315
- const vhtlcPkScript = import_sdk8.ArkAddress.decode(
4316
- pendingSwap.response.lockupDetails.lockupAddress
4317
- ).pkScript;
4318
- const { vtxos } = await this.indexerProvider.getVtxos({
4319
- scripts: [import_base9.hex.encode(vhtlcPkScript)]
4320
- });
4321
- if (vtxos.length === 0) {
4322
- throw new Error(
4323
- `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4324
- );
4325
- }
4326
- const vtxo = vtxos[0];
4327
- if (vtxo.isSpent) {
4328
- throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4329
- }
4330
4430
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4331
4431
  network: arkInfo.network,
4332
4432
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4342,37 +4442,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
4342
4442
  message: "Unable to claim: invalid VHTLC address"
4343
4443
  });
4344
4444
  }
4345
- const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4346
- const input = {
4347
- ...vtxo,
4348
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
4349
- tapTree: vhtlcScript.encode()
4350
- };
4351
- const output = {
4352
- amount: BigInt(vtxo.value),
4353
- script: import_sdk8.ArkAddress.decode(address).pkScript
4354
- };
4355
- if (isRecoverableVtxo) {
4356
- await this.joinBatch(this.wallet.identity, input, output, arkInfo);
4357
- } else {
4358
- await refundVHTLCwithOffchainTx(
4359
- pendingSwap.id,
4360
- this.wallet.identity,
4361
- this.arkProvider,
4362
- boltzXOnlyPublicKey,
4363
- ourXOnlyPublicKey,
4364
- serverXOnlyPublicKey,
4365
- input,
4366
- output,
4367
- arkInfo,
4368
- this.swapProvider.refundChainSwap.bind(this.swapProvider)
4445
+ const { vtxos } = await this.indexerProvider.getVtxos({
4446
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4447
+ });
4448
+ if (vtxos.length === 0) {
4449
+ throw new Error(
4450
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4369
4451
  );
4370
4452
  }
4453
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4454
+ if (unspentVtxos.length === 0) {
4455
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4456
+ }
4457
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4458
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4459
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4460
+ let boltzCallCount = 0;
4461
+ let sweptCount = 0;
4462
+ let skippedCount = 0;
4463
+ for (const vtxo of unspentVtxos) {
4464
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4465
+ const output = {
4466
+ amount: BigInt(vtxo.value),
4467
+ script: outputScript
4468
+ };
4469
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4470
+ const input2 = {
4471
+ ...vtxo,
4472
+ tapLeafScript: refundWithoutReceiverLeaf,
4473
+ tapTree: vhtlcScript.encode()
4474
+ };
4475
+ await this.joinBatch(
4476
+ this.wallet.identity,
4477
+ input2,
4478
+ output,
4479
+ arkInfo,
4480
+ isRecoverableVtxo
4481
+ );
4482
+ sweptCount++;
4483
+ continue;
4484
+ }
4485
+ if (isRecoverableVtxo) {
4486
+ logger.error(
4487
+ `Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${refundLocktime}, currentTimestamp=${Math.floor(Date.now() / 1e3)}). Refund will be retried after locktime.`
4488
+ );
4489
+ skippedCount++;
4490
+ continue;
4491
+ }
4492
+ const input = {
4493
+ ...vtxo,
4494
+ tapLeafScript: vhtlcScript.refund(),
4495
+ tapTree: vhtlcScript.encode()
4496
+ };
4497
+ try {
4498
+ if (boltzCallCount > 0) {
4499
+ await new Promise((r) => setTimeout(r, 2e3));
4500
+ }
4501
+ boltzCallCount++;
4502
+ await refundVHTLCwithOffchainTx(
4503
+ pendingSwap.id,
4504
+ this.wallet.identity,
4505
+ this.arkProvider,
4506
+ boltzXOnlyPublicKey,
4507
+ ourXOnlyPublicKey,
4508
+ serverXOnlyPublicKey,
4509
+ input,
4510
+ output,
4511
+ arkInfo,
4512
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4513
+ );
4514
+ sweptCount++;
4515
+ } catch (error) {
4516
+ if (!(error instanceof BoltzRefundError)) {
4517
+ throw error;
4518
+ }
4519
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4520
+ logger.error(
4521
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (currentTimestamp=${Math.floor(Date.now() / 1e3)}, locktime=${refundLocktime}). Refund will be retried after locktime.`
4522
+ );
4523
+ skippedCount++;
4524
+ continue;
4525
+ }
4526
+ logger.warn(
4527
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4528
+ );
4529
+ const fallbackInput = {
4530
+ ...vtxo,
4531
+ tapLeafScript: refundWithoutReceiverLeaf,
4532
+ tapTree: vhtlcScript.encode()
4533
+ };
4534
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4535
+ sweptCount++;
4536
+ }
4537
+ }
4371
4538
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4372
4539
  await this.savePendingChainSwap({
4373
4540
  ...pendingSwap,
4374
4541
  status: finalStatus.status
4375
4542
  });
4543
+ return { swept: sweptCount, skipped: skippedCount };
4376
4544
  }
4377
4545
  // =========================================================================
4378
4546
  // Chain swaps: BTC -> ARK
@@ -4793,7 +4961,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4793
4961
  this.validateQuoteOptions(options);
4794
4962
  const floor = await this.resolveQuoteFloor(swapId, options);
4795
4963
  const slippageBps = options?.maxSlippageBps ?? 0;
4796
- return Math.floor(floor - floor * slippageBps / 1e4);
4964
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4965
+ if (effectiveFloor < 1) {
4966
+ throw new TypeError(
4967
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4968
+ );
4969
+ }
4970
+ return effectiveFloor;
4797
4971
  }
4798
4972
  async resolveQuoteFloor(swapId, options) {
4799
4973
  if (options?.minAcceptableAmount !== void 0) {
@@ -4829,7 +5003,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4829
5003
  }
4830
5004
  }
4831
5005
  validateQuote(amount, effectiveFloor) {
4832
- if (!(amount > 0)) {
5006
+ if (!Number.isSafeInteger(amount)) {
5007
+ throw new QuoteRejectedError({
5008
+ reason: "non_safe_integer",
5009
+ quotedAmount: amount
5010
+ });
5011
+ }
5012
+ if (amount <= 0) {
4833
5013
  throw new QuoteRejectedError({
4834
5014
  reason: "non_positive",
4835
5015
  quotedAmount: amount
@@ -5473,9 +5653,14 @@ var ArkadeSwapsMessageHandler = class _ArkadeSwapsMessageHandler {
5473
5653
  case "CLAIM_BTC":
5474
5654
  await this.handler.claimBtc(message.payload);
5475
5655
  return this.tagged({ id, type: "BTC_CLAIM_EXECUTED" });
5476
- case "REFUND_ARK":
5477
- await this.handler.refundArk(message.payload);
5478
- return this.tagged({ id, type: "ARK_REFUND_EXECUTED" });
5656
+ case "REFUND_ARK": {
5657
+ const outcome = await this.handler.refundArk(message.payload);
5658
+ return this.tagged({
5659
+ id,
5660
+ type: "ARK_REFUND_EXECUTED",
5661
+ payload: outcome
5662
+ });
5663
+ }
5479
5664
  case "SIGN_SERVER_CLAIM":
5480
5665
  await this.handler.signCooperativeClaimForServer(message.payload);
5481
5666
  return this.tagged({ id, type: "SERVER_CLAIM_SIGNED" });
@@ -6150,12 +6335,13 @@ var ServiceWorkerArkadeSwaps = class _ServiceWorkerArkadeSwaps {
6150
6335
  });
6151
6336
  }
6152
6337
  async refundArk(pendingSwap) {
6153
- await this.sendMessage({
6338
+ const res = await this.sendMessage({
6154
6339
  id: (0, import_sdk10.getRandomId)(),
6155
6340
  tag: this.messageTag,
6156
6341
  type: "REFUND_ARK",
6157
6342
  payload: pendingSwap
6158
6343
  });
6344
+ return res.payload;
6159
6345
  }
6160
6346
  async signCooperativeClaimForServer(pendingSwap) {
6161
6347
  await this.sendMessage({
@@ -6565,6 +6751,30 @@ async function getContractCollection(storage, contractType) {
6565
6751
  );
6566
6752
  }
6567
6753
  }
6754
+
6755
+ // src/repositories/inMemory/swap-repository.ts
6756
+ var InMemorySwapRepository = class {
6757
+ version = 1;
6758
+ swaps = /* @__PURE__ */ new Map();
6759
+ async saveSwap(swap) {
6760
+ this.swaps.set(swap.id, swap);
6761
+ }
6762
+ async deleteSwap(id) {
6763
+ this.swaps.delete(id);
6764
+ }
6765
+ async getAllSwaps(filter) {
6766
+ const swaps = [...this.swaps.values()];
6767
+ if (!filter || Object.keys(filter).length === 0) return swaps;
6768
+ const filtered = applySwapsFilter(swaps, filter);
6769
+ return applyCreatedAtOrder(filtered, filter);
6770
+ }
6771
+ async clear() {
6772
+ this.swaps.clear();
6773
+ }
6774
+ async [Symbol.asyncDispose]() {
6775
+ await this.clear();
6776
+ }
6777
+ };
6568
6778
  // Annotate the CommonJS export names for ESM import in node:
6569
6779
  0 && (module.exports = {
6570
6780
  ArkadeLightningMessageHandler,
@@ -6572,6 +6782,7 @@ async function getContractCollection(storage, contractType) {
6572
6782
  ArkadeSwapsMessageHandler,
6573
6783
  BoltzRefundError,
6574
6784
  BoltzSwapProvider,
6785
+ InMemorySwapRepository,
6575
6786
  IndexedDbSwapRepository,
6576
6787
  InsufficientFundsError,
6577
6788
  InvoiceExpiredError,