@arkade-os/boltz-swap 0.3.33 → 0.3.35

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.
@@ -155,6 +155,8 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
155
155
  return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
156
156
  case "non_positive":
157
157
  return `Boltz quote ${options.quotedAmount} is not positive`;
158
+ case "non_safe_integer":
159
+ return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
158
160
  case "no_baseline":
159
161
  return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
160
162
  }
@@ -208,6 +210,7 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
208
210
  message
209
211
  });
210
212
  case "non_positive":
213
+ case "non_safe_integer":
211
214
  if (quotedAmount === null) return null;
212
215
  return new _QuoteRejectedError({
213
216
  reason,
@@ -223,6 +226,7 @@ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
223
226
  var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
224
227
  "below_floor",
225
228
  "non_positive",
229
+ "non_safe_integer",
226
230
  "no_baseline"
227
231
  ]);
228
232
  var BoltzRefundError = class extends Error {
@@ -1369,6 +1373,20 @@ function extractInvoiceAmount(amountSats, fees) {
1369
1373
  if (miner >= amountSats) return 0;
1370
1374
  return Math.ceil((amountSats - miner) / (1 - percentage / 100));
1371
1375
  }
1376
+ function resolveVhtlcTimeouts(tree, timeoutBlockHeights) {
1377
+ const resolved = timeoutBlockHeights ?? {
1378
+ refund: extractTimeLockFromLeafOutput(tree.refundWithoutBoltzLeaf?.output ?? ""),
1379
+ unilateralClaim: extractTimeLockFromLeafOutput(tree.unilateralClaimLeaf?.output ?? ""),
1380
+ unilateralRefund: extractTimeLockFromLeafOutput(tree.unilateralRefundLeaf?.output ?? ""),
1381
+ unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
1382
+ tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
1383
+ )
1384
+ };
1385
+ if (!resolved.refund || !resolved.unilateralClaim || !resolved.unilateralRefund || !resolved.unilateralRefundWithoutReceiver) {
1386
+ return void 0;
1387
+ }
1388
+ return resolved;
1389
+ }
1372
1390
 
1373
1391
  // src/logger.ts
1374
1392
  var logger = console;
@@ -1383,6 +1401,13 @@ var SwapManager = class _SwapManager {
1383
1401
  * enough that a real "swap unknown to this provider" surfaces quickly.
1384
1402
  */
1385
1403
  static NOT_FOUND_THRESHOLD = 10;
1404
+ /**
1405
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1406
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1407
+ * has elapsed). Boltz won't send another status update once the swap
1408
+ * is `swap.expired`, so the manager owns the local retry cadence.
1409
+ */
1410
+ static REFUND_RETRY_DELAY_MS = 6e4;
1386
1411
  swapProvider;
1387
1412
  config;
1388
1413
  // Event listeners storage (supports multiple listeners per event)
@@ -1401,6 +1426,11 @@ var SwapManager = class _SwapManager {
1401
1426
  reconnectTimer = null;
1402
1427
  initialPollTimer = null;
1403
1428
  pollRetryTimers = /* @__PURE__ */ new Map();
1429
+ // Per-swap retry timers for chain refunds that left work undone
1430
+ // (refundArk returned `skipped > 0`). The swap is held in
1431
+ // `monitoredSwaps` past its terminal Boltz status until the local
1432
+ // refund completes or the manager stops.
1433
+ refundRetryTimers = /* @__PURE__ */ new Map();
1404
1434
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1405
1435
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1406
1436
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1597,6 +1627,10 @@ var SwapManager = class _SwapManager {
1597
1627
  clearTimeout(timer);
1598
1628
  }
1599
1629
  this.pollRetryTimers.clear();
1630
+ for (const timer of this.refundRetryTimers.values()) {
1631
+ clearTimeout(timer);
1632
+ }
1633
+ this.refundRetryTimers.clear();
1600
1634
  this.notFoundCounts.clear();
1601
1635
  }
1602
1636
  /**
@@ -1653,6 +1687,11 @@ var SwapManager = class _SwapManager {
1653
1687
  clearTimeout(retryTimer);
1654
1688
  this.pollRetryTimers.delete(swapId);
1655
1689
  }
1690
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1691
+ if (refundRetryTimer) {
1692
+ clearTimeout(refundRetryTimer);
1693
+ this.refundRetryTimers.delete(swapId);
1694
+ }
1656
1695
  this.notFoundCounts.delete(swapId);
1657
1696
  logger.log(`Removed swap ${swapId} from monitoring`);
1658
1697
  }
@@ -1924,15 +1963,57 @@ var SwapManager = class _SwapManager {
1924
1963
  await this.executeAutonomousAction(swap);
1925
1964
  }
1926
1965
  if (this.isFinalStatus(swap)) {
1927
- this.monitoredSwaps.delete(swap.id);
1928
- this.swapSubscriptions.delete(swap.id);
1929
- const retryTimer = this.pollRetryTimers.get(swap.id);
1930
- if (retryTimer) {
1931
- clearTimeout(retryTimer);
1932
- this.pollRetryTimers.delete(swap.id);
1966
+ if (this.refundRetryTimers.has(swap.id)) {
1967
+ return;
1933
1968
  }
1934
- this.swapCompletedListeners.forEach((listener) => listener(swap));
1969
+ this.finalizeMonitoredSwap(swap);
1970
+ }
1971
+ }
1972
+ /**
1973
+ * Drop a swap from monitoring and emit the terminal completion event.
1974
+ * Shared between the on-status-update finalization path and the
1975
+ * refund-retry finalization path (used when a previously-deferred
1976
+ * chain refund has finished its remaining work).
1977
+ */
1978
+ finalizeMonitoredSwap(swap) {
1979
+ if (!this.monitoredSwaps.has(swap.id)) return;
1980
+ this.monitoredSwaps.delete(swap.id);
1981
+ this.swapSubscriptions.delete(swap.id);
1982
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1983
+ if (retryTimer) {
1984
+ clearTimeout(retryTimer);
1985
+ this.pollRetryTimers.delete(swap.id);
1935
1986
  }
1987
+ const refundRetry = this.refundRetryTimers.get(swap.id);
1988
+ if (refundRetry) {
1989
+ clearTimeout(refundRetry);
1990
+ this.refundRetryTimers.delete(swap.id);
1991
+ }
1992
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
1993
+ }
1994
+ /**
1995
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
1996
+ * refund left VTXOs deferred. After the retry completes, if no further
1997
+ * deferral was reported, finalize monitoring cleanup.
1998
+ */
1999
+ scheduleRefundRetry(swap, delayMs) {
2000
+ const existing = this.refundRetryTimers.get(swap.id);
2001
+ if (existing) clearTimeout(existing);
2002
+ this.refundRetryTimers.set(
2003
+ swap.id,
2004
+ setTimeout(async () => {
2005
+ this.refundRetryTimers.delete(swap.id);
2006
+ if (!this.isRunning) return;
2007
+ if (!this.monitoredSwaps.has(swap.id)) return;
2008
+ try {
2009
+ await this.executeAutonomousAction(swap);
2010
+ } finally {
2011
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
2012
+ this.finalizeMonitoredSwap(swap);
2013
+ }
2014
+ }
2015
+ }, delayMs)
2016
+ );
1936
2017
  }
1937
2018
  /**
1938
2019
  * Execute autonomous action based on swap status
@@ -1987,10 +2068,27 @@ var SwapManager = class _SwapManager {
1987
2068
  } else if (isChainRefundableStatus(swap.status)) {
1988
2069
  if (swap.request.from === "ARK") {
1989
2070
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
1990
- await this.executeRefundArkAction(swap);
1991
- this.actionExecutedListeners.forEach(
1992
- (listener) => listener(swap, "refundArk")
1993
- );
2071
+ try {
2072
+ const outcome = await this.executeRefundArkAction(swap);
2073
+ if (outcome && outcome.skipped > 0) {
2074
+ logger.log(
2075
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2076
+ );
2077
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2078
+ }
2079
+ this.actionExecutedListeners.forEach(
2080
+ (listener) => listener(swap, "refundArk")
2081
+ );
2082
+ } catch (error) {
2083
+ logger.error(
2084
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2085
+ error
2086
+ );
2087
+ this.swapFailedListeners.forEach(
2088
+ (listener) => listener(swap, error)
2089
+ );
2090
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2091
+ }
1994
2092
  }
1995
2093
  if (swap.request.from === "BTC") {
1996
2094
  logger.warn(
@@ -2071,7 +2169,7 @@ var SwapManager = class _SwapManager {
2071
2169
  logger.error("refundArk callback not set");
2072
2170
  return;
2073
2171
  }
2074
- await this.refundArkCallback(swap);
2172
+ return this.refundArkCallback(swap);
2075
2173
  }
2076
2174
  /**
2077
2175
  * Execute sign server claim action for chain swap.
@@ -2171,9 +2269,7 @@ var SwapManager = class _SwapManager {
2171
2269
  */
2172
2270
  async pollAllSwaps() {
2173
2271
  if (this.monitoredSwaps.size === 0) return;
2174
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2175
- (swap) => this.pollSingleSwap(swap)
2176
- );
2272
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2177
2273
  await Promise.allSettled(pollPromises);
2178
2274
  }
2179
2275
  async pollSingleSwap(swap) {
@@ -2226,6 +2322,7 @@ var SwapManager = class _SwapManager {
2226
2322
  * Boltz endpoint).
2227
2323
  */
2228
2324
  async handleSwapNotFound(swap) {
2325
+ if (this.refundRetryTimers.has(swap.id)) return;
2229
2326
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2230
2327
  this.notFoundCounts.set(swap.id, count);
2231
2328
  logger.warn(
@@ -2246,6 +2343,7 @@ var SwapManager = class _SwapManager {
2246
2343
  * 404s without recovering anything.
2247
2344
  */
2248
2345
  async markSwapAsUnknownToProvider(swap) {
2346
+ if (this.refundRetryTimers.has(swap.id)) return;
2249
2347
  if (!this.monitoredSwaps.has(swap.id)) {
2250
2348
  this.notFoundCounts.delete(swap.id);
2251
2349
  return;
@@ -2258,6 +2356,11 @@ var SwapManager = class _SwapManager {
2258
2356
  clearTimeout(retryTimer);
2259
2357
  this.pollRetryTimers.delete(swap.id);
2260
2358
  }
2359
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2360
+ if (refundRetryTimer) {
2361
+ clearTimeout(refundRetryTimer);
2362
+ this.refundRetryTimers.delete(swap.id);
2363
+ }
2261
2364
  this.notFoundCounts.delete(swap.id);
2262
2365
  this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2263
2366
  const subscribers = this.swapSubscriptions.get(swap.id);
@@ -3008,7 +3111,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3008
3111
  await this.claimBtc(swap);
3009
3112
  },
3010
3113
  refundArk: async (swap) => {
3011
- await this.refundArk(swap);
3114
+ return this.refundArk(swap);
3012
3115
  },
3013
3116
  signServerClaim: async (swap) => {
3014
3117
  await this.signCooperativeClaimForServer(swap);
@@ -3221,51 +3324,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3221
3324
  throw new Error(
3222
3325
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3223
3326
  );
3224
- let vtxo;
3327
+ let unspentVtxos = [];
3328
+ let rawVtxos = [];
3225
3329
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3226
- const { vtxos } = await this.indexerProvider.getVtxos({
3330
+ const result = await this.indexerProvider.getVtxos({
3227
3331
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3228
3332
  });
3229
- if (vtxos.length > 0) {
3230
- vtxo = vtxos[0];
3333
+ rawVtxos = result.vtxos;
3334
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3335
+ if (unspentVtxos.length > 0) {
3231
3336
  break;
3232
3337
  }
3233
3338
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3234
3339
  await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3235
3340
  }
3236
3341
  }
3237
- if (!vtxo) {
3238
- throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3239
- }
3240
- if (vtxo.isSpent) {
3342
+ if (unspentVtxos.length === 0) {
3343
+ if (rawVtxos.length === 0) {
3344
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3345
+ }
3241
3346
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3242
3347
  }
3243
- const input = {
3244
- ...vtxo,
3245
- tapLeafScript: vhtlcScript.claim(),
3246
- tapTree: vhtlcScript.encode()
3247
- };
3248
- const output = {
3249
- amount: BigInt(vtxo.value),
3250
- script: import_sdk8.ArkAddress.decode(address).pkScript
3251
- };
3252
3348
  const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
3253
- let finalStatus;
3254
- if ((0, import_sdk8.isRecoverable)(vtxo)) {
3255
- await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3256
- finalStatus = "transaction.claimed";
3257
- } else {
3258
- await claimVHTLCwithOffchainTx(
3259
- vhtlcIdentity,
3260
- vhtlcScript,
3261
- serverXOnly,
3262
- input,
3263
- output,
3264
- arkInfo,
3265
- this.arkProvider
3349
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3350
+ const claimErrors = [];
3351
+ let usedOffchainClaim = false;
3352
+ for (const vtxo of unspentVtxos) {
3353
+ const input = {
3354
+ ...vtxo,
3355
+ tapLeafScript: vhtlcScript.claim(),
3356
+ tapTree: vhtlcScript.encode()
3357
+ };
3358
+ const output = {
3359
+ amount: BigInt(vtxo.value),
3360
+ script: outputScript
3361
+ };
3362
+ try {
3363
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3364
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3365
+ } else {
3366
+ await claimVHTLCwithOffchainTx(
3367
+ vhtlcIdentity,
3368
+ vhtlcScript,
3369
+ serverXOnly,
3370
+ input,
3371
+ output,
3372
+ arkInfo,
3373
+ this.arkProvider
3374
+ );
3375
+ usedOffchainClaim = true;
3376
+ }
3377
+ } catch (error) {
3378
+ claimErrors.push({ vtxo, error });
3379
+ }
3380
+ }
3381
+ if (claimErrors.length > 0) {
3382
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3383
+ throw new Error(
3384
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3266
3385
  );
3267
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3268
3386
  }
3387
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3269
3388
  await updateReverseSwapStatus(
3270
3389
  pendingSwap,
3271
3390
  finalStatus,
@@ -3640,6 +3759,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3640
3759
  if (boltzCallCount > 0) {
3641
3760
  await new Promise((r) => setTimeout(r, 2e3));
3642
3761
  }
3762
+ boltzCallCount++;
3643
3763
  await refundVHTLCwithOffchainTx(
3644
3764
  pendingSwap.id,
3645
3765
  this.wallet.identity,
@@ -3652,7 +3772,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3652
3772
  arkInfo,
3653
3773
  this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3654
3774
  );
3655
- boltzCallCount++;
3656
3775
  sweptCount++;
3657
3776
  } catch (error) {
3658
3777
  if (!(error instanceof BoltzRefundError)) {
@@ -4150,8 +4269,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4150
4269
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4151
4270
  }
4152
4271
  /**
4153
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4272
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4273
+ * swap's ARK lockup address.
4274
+ *
4275
+ * Path selection per VTXO:
4276
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4277
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4278
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4279
+ *
4154
4280
  * @param pendingSwap - The pending chain swap to refund.
4281
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4282
+ * the call was a no-op — callers should retry after CLTV.
4155
4283
  */
4156
4284
  async refundArk(pendingSwap) {
4157
4285
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
@@ -4175,21 +4303,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4175
4303
  "boltz",
4176
4304
  pendingSwap.id
4177
4305
  );
4178
- const vhtlcPkScript = import_sdk8.ArkAddress.decode(
4179
- pendingSwap.response.lockupDetails.lockupAddress
4180
- ).pkScript;
4181
- const { vtxos } = await this.indexerProvider.getVtxos({
4182
- scripts: [import_base9.hex.encode(vhtlcPkScript)]
4183
- });
4184
- if (vtxos.length === 0) {
4185
- throw new Error(
4186
- `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4187
- );
4188
- }
4189
- const vtxo = vtxos[0];
4190
- if (vtxo.isSpent) {
4191
- throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4192
- }
4193
4306
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4194
4307
  network: arkInfo.network,
4195
4308
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4205,37 +4318,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
4205
4318
  message: "Unable to claim: invalid VHTLC address"
4206
4319
  });
4207
4320
  }
4208
- const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4209
- const input = {
4210
- ...vtxo,
4211
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
4212
- tapTree: vhtlcScript.encode()
4213
- };
4214
- const output = {
4215
- amount: BigInt(vtxo.value),
4216
- script: import_sdk8.ArkAddress.decode(address).pkScript
4217
- };
4218
- if (isRecoverableVtxo) {
4219
- await this.joinBatch(this.wallet.identity, input, output, arkInfo);
4220
- } else {
4221
- await refundVHTLCwithOffchainTx(
4222
- pendingSwap.id,
4223
- this.wallet.identity,
4224
- this.arkProvider,
4225
- boltzXOnlyPublicKey,
4226
- ourXOnlyPublicKey,
4227
- serverXOnlyPublicKey,
4228
- input,
4229
- output,
4230
- arkInfo,
4231
- this.swapProvider.refundChainSwap.bind(this.swapProvider)
4321
+ const { vtxos } = await this.indexerProvider.getVtxos({
4322
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4323
+ });
4324
+ if (vtxos.length === 0) {
4325
+ throw new Error(
4326
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4232
4327
  );
4233
4328
  }
4329
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4330
+ if (unspentVtxos.length === 0) {
4331
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4332
+ }
4333
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4334
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4335
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4336
+ let boltzCallCount = 0;
4337
+ let sweptCount = 0;
4338
+ let skippedCount = 0;
4339
+ for (const vtxo of unspentVtxos) {
4340
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4341
+ const output = {
4342
+ amount: BigInt(vtxo.value),
4343
+ script: outputScript
4344
+ };
4345
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4346
+ const input2 = {
4347
+ ...vtxo,
4348
+ tapLeafScript: refundWithoutReceiverLeaf,
4349
+ tapTree: vhtlcScript.encode()
4350
+ };
4351
+ await this.joinBatch(
4352
+ this.wallet.identity,
4353
+ input2,
4354
+ output,
4355
+ arkInfo,
4356
+ isRecoverableVtxo
4357
+ );
4358
+ sweptCount++;
4359
+ continue;
4360
+ }
4361
+ if (isRecoverableVtxo) {
4362
+ logger.error(
4363
+ `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.`
4364
+ );
4365
+ skippedCount++;
4366
+ continue;
4367
+ }
4368
+ const input = {
4369
+ ...vtxo,
4370
+ tapLeafScript: vhtlcScript.refund(),
4371
+ tapTree: vhtlcScript.encode()
4372
+ };
4373
+ try {
4374
+ if (boltzCallCount > 0) {
4375
+ await new Promise((r) => setTimeout(r, 2e3));
4376
+ }
4377
+ boltzCallCount++;
4378
+ await refundVHTLCwithOffchainTx(
4379
+ pendingSwap.id,
4380
+ this.wallet.identity,
4381
+ this.arkProvider,
4382
+ boltzXOnlyPublicKey,
4383
+ ourXOnlyPublicKey,
4384
+ serverXOnlyPublicKey,
4385
+ input,
4386
+ output,
4387
+ arkInfo,
4388
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4389
+ );
4390
+ sweptCount++;
4391
+ } catch (error) {
4392
+ if (!(error instanceof BoltzRefundError)) {
4393
+ throw error;
4394
+ }
4395
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4396
+ logger.error(
4397
+ `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.`
4398
+ );
4399
+ skippedCount++;
4400
+ continue;
4401
+ }
4402
+ logger.warn(
4403
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4404
+ );
4405
+ const fallbackInput = {
4406
+ ...vtxo,
4407
+ tapLeafScript: refundWithoutReceiverLeaf,
4408
+ tapTree: vhtlcScript.encode()
4409
+ };
4410
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4411
+ sweptCount++;
4412
+ }
4413
+ }
4234
4414
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4235
4415
  await this.savePendingChainSwap({
4236
4416
  ...pendingSwap,
4237
4417
  status: finalStatus.status
4238
4418
  });
4419
+ return { swept: sweptCount, skipped: skippedCount };
4239
4420
  }
4240
4421
  // =========================================================================
4241
4422
  // Chain swaps: BTC -> ARK
@@ -4656,7 +4837,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4656
4837
  this.validateQuoteOptions(options);
4657
4838
  const floor = await this.resolveQuoteFloor(swapId, options);
4658
4839
  const slippageBps = options?.maxSlippageBps ?? 0;
4659
- return Math.floor(floor - floor * slippageBps / 1e4);
4840
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4841
+ if (effectiveFloor < 1) {
4842
+ throw new TypeError(
4843
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4844
+ );
4845
+ }
4846
+ return effectiveFloor;
4660
4847
  }
4661
4848
  async resolveQuoteFloor(swapId, options) {
4662
4849
  if (options?.minAcceptableAmount !== void 0) {
@@ -4692,7 +4879,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4692
4879
  }
4693
4880
  }
4694
4881
  validateQuote(amount, effectiveFloor) {
4695
- if (!(amount > 0)) {
4882
+ if (!Number.isSafeInteger(amount)) {
4883
+ throw new QuoteRejectedError({
4884
+ reason: "non_safe_integer",
4885
+ quotedAmount: amount
4886
+ });
4887
+ }
4888
+ if (amount <= 0) {
4696
4889
  throw new QuoteRejectedError({
4697
4890
  reason: "non_positive",
4698
4891
  quotedAmount: amount
@@ -4875,20 +5068,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
4875
5068
  onchainAmount: amount,
4876
5069
  lockupAddress,
4877
5070
  refundPublicKey: serverPublicKey,
4878
- timeoutBlockHeights: timeoutBlockHeights ?? {
4879
- refund: extractTimeLockFromLeafOutput(
4880
- tree.refundWithoutBoltzLeaf?.output ?? ""
4881
- ),
4882
- unilateralClaim: extractTimeLockFromLeafOutput(
4883
- tree.unilateralClaimLeaf?.output ?? ""
4884
- ),
4885
- unilateralRefund: extractTimeLockFromLeafOutput(
4886
- tree.unilateralRefundLeaf?.output ?? ""
4887
- ),
4888
- unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4889
- tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4890
- )
4891
- }
5071
+ timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
4892
5072
  },
4893
5073
  status,
4894
5074
  type: "reverse",
@@ -4921,26 +5101,20 @@ var ArkadeSwaps = class _ArkadeSwaps {
4921
5101
  address: lockupAddress,
4922
5102
  expectedAmount: amount,
4923
5103
  claimPublicKey: serverPublicKey,
4924
- timeoutBlockHeights: timeoutBlockHeights ?? {
4925
- refund: extractTimeLockFromLeafOutput(
4926
- tree.refundWithoutBoltzLeaf?.output ?? ""
4927
- ),
4928
- unilateralClaim: extractTimeLockFromLeafOutput(
4929
- tree.unilateralClaimLeaf?.output ?? ""
4930
- ),
4931
- unilateralRefund: extractTimeLockFromLeafOutput(
4932
- tree.unilateralRefundLeaf?.output ?? ""
4933
- ),
4934
- unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
4935
- tree.unilateralRefundWithoutBoltzLeaf?.output ?? ""
4936
- )
4937
- }
5104
+ timeoutBlockHeights: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
4938
5105
  }
4939
5106
  });
4940
5107
  } else if (isRestoredChainSwap(swap)) {
4941
5108
  const refundDetails = swap.refundDetails;
4942
5109
  if (!refundDetails) continue;
4943
- const { amount, lockupAddress, serverPublicKey, timeoutBlockHeight } = refundDetails;
5110
+ const {
5111
+ amount,
5112
+ lockupAddress,
5113
+ serverPublicKey,
5114
+ timeoutBlockHeight,
5115
+ tree,
5116
+ timeoutBlockHeights
5117
+ } = refundDetails;
4944
5118
  chainSwaps.push({
4945
5119
  id,
4946
5120
  type: "chain",
@@ -4966,7 +5140,8 @@ var ArkadeSwaps = class _ArkadeSwaps {
4966
5140
  amount,
4967
5141
  lockupAddress,
4968
5142
  serverPublicKey,
4969
- timeoutBlockHeight
5143
+ timeoutBlockHeight,
5144
+ timeouts: resolveVhtlcTimeouts(tree, timeoutBlockHeights)
4970
5145
  }
4971
5146
  }
4972
5147
  });
@@ -5087,6 +5262,7 @@ function createBackgroundWalletShim(args) {
5087
5262
  getBoardingUtxos: async () => notImplemented("getBoardingUtxos"),
5088
5263
  getTransactionHistory: async () => notImplemented("getTransactionHistory"),
5089
5264
  getContractManager: async () => notImplemented("getContractManager"),
5265
+ getDelegateManager: async () => notImplemented("getDelegateManager"),
5090
5266
  getDelegatorManager: async () => notImplemented("getDelegatorManager"),
5091
5267
  sendBitcoin: async () => notImplemented("sendBitcoin"),
5092
5268
  send: async () => notImplemented("send"),
@@ -1,8 +1,8 @@
1
- import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-CuITxZie.cjs';
2
- export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-CuITxZie.cjs';
1
+ import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-BpAqG0V6.cjs';
2
+ export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-BpAqG0V6.cjs';
3
3
  import '@arkade-os/sdk/worker/expo';
4
4
  import '@arkade-os/sdk';
5
- import '../types-CrKkVzBB.cjs';
5
+ import '../types--axEWA8c.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-CEgeGlbP.js';
2
- export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-CEgeGlbP.js';
1
+ import { D as DefineSwapBackgroundTaskOptions } from '../swapsPollProcessor-DFVOAy_-.js';
2
+ export { P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies, s as swapsPollProcessor } from '../swapsPollProcessor-DFVOAy_-.js';
3
3
  import '@arkade-os/sdk/worker/expo';
4
4
  import '@arkade-os/sdk';
5
- import '../types-CrKkVzBB.js';
5
+ import '../types--axEWA8c.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-HNQDJOLM.js";
4
+ } from "../chunk-H6F67K2A.js";
5
5
  import {
6
6
  BoltzSwapProvider
7
- } from "../chunk-SJ5SYSMK.js";
7
+ } from "../chunk-B4CYBKFJ.js";
8
8
  import "../chunk-SJQJQO7P.js";
9
9
 
10
10
  // src/expo/background.ts
@@ -28,6 +28,7 @@ function createBackgroundWalletShim(args) {
28
28
  getBoardingUtxos: async () => notImplemented("getBoardingUtxos"),
29
29
  getTransactionHistory: async () => notImplemented("getTransactionHistory"),
30
30
  getContractManager: async () => notImplemented("getContractManager"),
31
+ getDelegateManager: async () => notImplemented("getDelegateManager"),
31
32
  getDelegatorManager: async () => notImplemented("getDelegatorManager"),
32
33
  sendBitcoin: async () => notImplemented("sendBitcoin"),
33
34
  send: async () => notImplemented("send"),