@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.
@@ -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 {
@@ -1383,6 +1387,13 @@ var SwapManager = class _SwapManager {
1383
1387
  * enough that a real "swap unknown to this provider" surfaces quickly.
1384
1388
  */
1385
1389
  static NOT_FOUND_THRESHOLD = 10;
1390
+ /**
1391
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1392
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1393
+ * has elapsed). Boltz won't send another status update once the swap
1394
+ * is `swap.expired`, so the manager owns the local retry cadence.
1395
+ */
1396
+ static REFUND_RETRY_DELAY_MS = 6e4;
1386
1397
  swapProvider;
1387
1398
  config;
1388
1399
  // Event listeners storage (supports multiple listeners per event)
@@ -1401,6 +1412,11 @@ var SwapManager = class _SwapManager {
1401
1412
  reconnectTimer = null;
1402
1413
  initialPollTimer = null;
1403
1414
  pollRetryTimers = /* @__PURE__ */ new Map();
1415
+ // Per-swap retry timers for chain refunds that left work undone
1416
+ // (refundArk returned `skipped > 0`). The swap is held in
1417
+ // `monitoredSwaps` past its terminal Boltz status until the local
1418
+ // refund completes or the manager stops.
1419
+ refundRetryTimers = /* @__PURE__ */ new Map();
1404
1420
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1405
1421
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1406
1422
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1597,6 +1613,10 @@ var SwapManager = class _SwapManager {
1597
1613
  clearTimeout(timer);
1598
1614
  }
1599
1615
  this.pollRetryTimers.clear();
1616
+ for (const timer of this.refundRetryTimers.values()) {
1617
+ clearTimeout(timer);
1618
+ }
1619
+ this.refundRetryTimers.clear();
1600
1620
  this.notFoundCounts.clear();
1601
1621
  }
1602
1622
  /**
@@ -1653,6 +1673,11 @@ var SwapManager = class _SwapManager {
1653
1673
  clearTimeout(retryTimer);
1654
1674
  this.pollRetryTimers.delete(swapId);
1655
1675
  }
1676
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1677
+ if (refundRetryTimer) {
1678
+ clearTimeout(refundRetryTimer);
1679
+ this.refundRetryTimers.delete(swapId);
1680
+ }
1656
1681
  this.notFoundCounts.delete(swapId);
1657
1682
  logger.log(`Removed swap ${swapId} from monitoring`);
1658
1683
  }
@@ -1924,15 +1949,57 @@ var SwapManager = class _SwapManager {
1924
1949
  await this.executeAutonomousAction(swap);
1925
1950
  }
1926
1951
  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);
1952
+ if (this.refundRetryTimers.has(swap.id)) {
1953
+ return;
1933
1954
  }
1934
- this.swapCompletedListeners.forEach((listener) => listener(swap));
1955
+ this.finalizeMonitoredSwap(swap);
1956
+ }
1957
+ }
1958
+ /**
1959
+ * Drop a swap from monitoring and emit the terminal completion event.
1960
+ * Shared between the on-status-update finalization path and the
1961
+ * refund-retry finalization path (used when a previously-deferred
1962
+ * chain refund has finished its remaining work).
1963
+ */
1964
+ finalizeMonitoredSwap(swap) {
1965
+ if (!this.monitoredSwaps.has(swap.id)) return;
1966
+ this.monitoredSwaps.delete(swap.id);
1967
+ this.swapSubscriptions.delete(swap.id);
1968
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1969
+ if (retryTimer) {
1970
+ clearTimeout(retryTimer);
1971
+ this.pollRetryTimers.delete(swap.id);
1972
+ }
1973
+ const refundRetry = this.refundRetryTimers.get(swap.id);
1974
+ if (refundRetry) {
1975
+ clearTimeout(refundRetry);
1976
+ this.refundRetryTimers.delete(swap.id);
1935
1977
  }
1978
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
1979
+ }
1980
+ /**
1981
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
1982
+ * refund left VTXOs deferred. After the retry completes, if no further
1983
+ * deferral was reported, finalize monitoring cleanup.
1984
+ */
1985
+ scheduleRefundRetry(swap, delayMs) {
1986
+ const existing = this.refundRetryTimers.get(swap.id);
1987
+ if (existing) clearTimeout(existing);
1988
+ this.refundRetryTimers.set(
1989
+ swap.id,
1990
+ setTimeout(async () => {
1991
+ this.refundRetryTimers.delete(swap.id);
1992
+ if (!this.isRunning) return;
1993
+ if (!this.monitoredSwaps.has(swap.id)) return;
1994
+ try {
1995
+ await this.executeAutonomousAction(swap);
1996
+ } finally {
1997
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
1998
+ this.finalizeMonitoredSwap(swap);
1999
+ }
2000
+ }
2001
+ }, delayMs)
2002
+ );
1936
2003
  }
1937
2004
  /**
1938
2005
  * Execute autonomous action based on swap status
@@ -1987,10 +2054,27 @@ var SwapManager = class _SwapManager {
1987
2054
  } else if (isChainRefundableStatus(swap.status)) {
1988
2055
  if (swap.request.from === "ARK") {
1989
2056
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
1990
- await this.executeRefundArkAction(swap);
1991
- this.actionExecutedListeners.forEach(
1992
- (listener) => listener(swap, "refundArk")
1993
- );
2057
+ try {
2058
+ const outcome = await this.executeRefundArkAction(swap);
2059
+ if (outcome && outcome.skipped > 0) {
2060
+ logger.log(
2061
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2062
+ );
2063
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2064
+ }
2065
+ this.actionExecutedListeners.forEach(
2066
+ (listener) => listener(swap, "refundArk")
2067
+ );
2068
+ } catch (error) {
2069
+ logger.error(
2070
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2071
+ error
2072
+ );
2073
+ this.swapFailedListeners.forEach(
2074
+ (listener) => listener(swap, error)
2075
+ );
2076
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2077
+ }
1994
2078
  }
1995
2079
  if (swap.request.from === "BTC") {
1996
2080
  logger.warn(
@@ -2071,7 +2155,7 @@ var SwapManager = class _SwapManager {
2071
2155
  logger.error("refundArk callback not set");
2072
2156
  return;
2073
2157
  }
2074
- await this.refundArkCallback(swap);
2158
+ return this.refundArkCallback(swap);
2075
2159
  }
2076
2160
  /**
2077
2161
  * Execute sign server claim action for chain swap.
@@ -2171,9 +2255,7 @@ var SwapManager = class _SwapManager {
2171
2255
  */
2172
2256
  async pollAllSwaps() {
2173
2257
  if (this.monitoredSwaps.size === 0) return;
2174
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2175
- (swap) => this.pollSingleSwap(swap)
2176
- );
2258
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2177
2259
  await Promise.allSettled(pollPromises);
2178
2260
  }
2179
2261
  async pollSingleSwap(swap) {
@@ -2226,6 +2308,7 @@ var SwapManager = class _SwapManager {
2226
2308
  * Boltz endpoint).
2227
2309
  */
2228
2310
  async handleSwapNotFound(swap) {
2311
+ if (this.refundRetryTimers.has(swap.id)) return;
2229
2312
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2230
2313
  this.notFoundCounts.set(swap.id, count);
2231
2314
  logger.warn(
@@ -2246,6 +2329,7 @@ var SwapManager = class _SwapManager {
2246
2329
  * 404s without recovering anything.
2247
2330
  */
2248
2331
  async markSwapAsUnknownToProvider(swap) {
2332
+ if (this.refundRetryTimers.has(swap.id)) return;
2249
2333
  if (!this.monitoredSwaps.has(swap.id)) {
2250
2334
  this.notFoundCounts.delete(swap.id);
2251
2335
  return;
@@ -2258,6 +2342,11 @@ var SwapManager = class _SwapManager {
2258
2342
  clearTimeout(retryTimer);
2259
2343
  this.pollRetryTimers.delete(swap.id);
2260
2344
  }
2345
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2346
+ if (refundRetryTimer) {
2347
+ clearTimeout(refundRetryTimer);
2348
+ this.refundRetryTimers.delete(swap.id);
2349
+ }
2261
2350
  this.notFoundCounts.delete(swap.id);
2262
2351
  this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2263
2352
  const subscribers = this.swapSubscriptions.get(swap.id);
@@ -3008,7 +3097,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3008
3097
  await this.claimBtc(swap);
3009
3098
  },
3010
3099
  refundArk: async (swap) => {
3011
- await this.refundArk(swap);
3100
+ return this.refundArk(swap);
3012
3101
  },
3013
3102
  signServerClaim: async (swap) => {
3014
3103
  await this.signCooperativeClaimForServer(swap);
@@ -3221,51 +3310,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3221
3310
  throw new Error(
3222
3311
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3223
3312
  );
3224
- let vtxo;
3313
+ let unspentVtxos = [];
3314
+ let rawVtxos = [];
3225
3315
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3226
- const { vtxos } = await this.indexerProvider.getVtxos({
3316
+ const result = await this.indexerProvider.getVtxos({
3227
3317
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3228
3318
  });
3229
- if (vtxos.length > 0) {
3230
- vtxo = vtxos[0];
3319
+ rawVtxos = result.vtxos;
3320
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3321
+ if (unspentVtxos.length > 0) {
3231
3322
  break;
3232
3323
  }
3233
3324
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3234
3325
  await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3235
3326
  }
3236
3327
  }
3237
- if (!vtxo) {
3238
- throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3239
- }
3240
- if (vtxo.isSpent) {
3328
+ if (unspentVtxos.length === 0) {
3329
+ if (rawVtxos.length === 0) {
3330
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3331
+ }
3241
3332
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3242
3333
  }
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
3334
  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
3335
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3336
+ const claimErrors = [];
3337
+ let usedOffchainClaim = false;
3338
+ for (const vtxo of unspentVtxos) {
3339
+ const input = {
3340
+ ...vtxo,
3341
+ tapLeafScript: vhtlcScript.claim(),
3342
+ tapTree: vhtlcScript.encode()
3343
+ };
3344
+ const output = {
3345
+ amount: BigInt(vtxo.value),
3346
+ script: outputScript
3347
+ };
3348
+ try {
3349
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3350
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3351
+ } else {
3352
+ await claimVHTLCwithOffchainTx(
3353
+ vhtlcIdentity,
3354
+ vhtlcScript,
3355
+ serverXOnly,
3356
+ input,
3357
+ output,
3358
+ arkInfo,
3359
+ this.arkProvider
3360
+ );
3361
+ usedOffchainClaim = true;
3362
+ }
3363
+ } catch (error) {
3364
+ claimErrors.push({ vtxo, error });
3365
+ }
3366
+ }
3367
+ if (claimErrors.length > 0) {
3368
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3369
+ throw new Error(
3370
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3266
3371
  );
3267
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3268
3372
  }
3373
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3269
3374
  await updateReverseSwapStatus(
3270
3375
  pendingSwap,
3271
3376
  finalStatus,
@@ -3640,6 +3745,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3640
3745
  if (boltzCallCount > 0) {
3641
3746
  await new Promise((r) => setTimeout(r, 2e3));
3642
3747
  }
3748
+ boltzCallCount++;
3643
3749
  await refundVHTLCwithOffchainTx(
3644
3750
  pendingSwap.id,
3645
3751
  this.wallet.identity,
@@ -3652,7 +3758,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3652
3758
  arkInfo,
3653
3759
  this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3654
3760
  );
3655
- boltzCallCount++;
3656
3761
  sweptCount++;
3657
3762
  } catch (error) {
3658
3763
  if (!(error instanceof BoltzRefundError)) {
@@ -4150,8 +4255,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4150
4255
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4151
4256
  }
4152
4257
  /**
4153
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4258
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4259
+ * swap's ARK lockup address.
4260
+ *
4261
+ * Path selection per VTXO:
4262
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4263
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4264
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4265
+ *
4154
4266
  * @param pendingSwap - The pending chain swap to refund.
4267
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4268
+ * the call was a no-op — callers should retry after CLTV.
4155
4269
  */
4156
4270
  async refundArk(pendingSwap) {
4157
4271
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
@@ -4175,21 +4289,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4175
4289
  "boltz",
4176
4290
  pendingSwap.id
4177
4291
  );
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
4292
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4194
4293
  network: arkInfo.network,
4195
4294
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4205,37 +4304,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
4205
4304
  message: "Unable to claim: invalid VHTLC address"
4206
4305
  });
4207
4306
  }
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)
4307
+ const { vtxos } = await this.indexerProvider.getVtxos({
4308
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4309
+ });
4310
+ if (vtxos.length === 0) {
4311
+ throw new Error(
4312
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4232
4313
  );
4233
4314
  }
4315
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4316
+ if (unspentVtxos.length === 0) {
4317
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4318
+ }
4319
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4320
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4321
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4322
+ let boltzCallCount = 0;
4323
+ let sweptCount = 0;
4324
+ let skippedCount = 0;
4325
+ for (const vtxo of unspentVtxos) {
4326
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4327
+ const output = {
4328
+ amount: BigInt(vtxo.value),
4329
+ script: outputScript
4330
+ };
4331
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4332
+ const input2 = {
4333
+ ...vtxo,
4334
+ tapLeafScript: refundWithoutReceiverLeaf,
4335
+ tapTree: vhtlcScript.encode()
4336
+ };
4337
+ await this.joinBatch(
4338
+ this.wallet.identity,
4339
+ input2,
4340
+ output,
4341
+ arkInfo,
4342
+ isRecoverableVtxo
4343
+ );
4344
+ sweptCount++;
4345
+ continue;
4346
+ }
4347
+ if (isRecoverableVtxo) {
4348
+ logger.error(
4349
+ `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.`
4350
+ );
4351
+ skippedCount++;
4352
+ continue;
4353
+ }
4354
+ const input = {
4355
+ ...vtxo,
4356
+ tapLeafScript: vhtlcScript.refund(),
4357
+ tapTree: vhtlcScript.encode()
4358
+ };
4359
+ try {
4360
+ if (boltzCallCount > 0) {
4361
+ await new Promise((r) => setTimeout(r, 2e3));
4362
+ }
4363
+ boltzCallCount++;
4364
+ await refundVHTLCwithOffchainTx(
4365
+ pendingSwap.id,
4366
+ this.wallet.identity,
4367
+ this.arkProvider,
4368
+ boltzXOnlyPublicKey,
4369
+ ourXOnlyPublicKey,
4370
+ serverXOnlyPublicKey,
4371
+ input,
4372
+ output,
4373
+ arkInfo,
4374
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4375
+ );
4376
+ sweptCount++;
4377
+ } catch (error) {
4378
+ if (!(error instanceof BoltzRefundError)) {
4379
+ throw error;
4380
+ }
4381
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4382
+ logger.error(
4383
+ `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.`
4384
+ );
4385
+ skippedCount++;
4386
+ continue;
4387
+ }
4388
+ logger.warn(
4389
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4390
+ );
4391
+ const fallbackInput = {
4392
+ ...vtxo,
4393
+ tapLeafScript: refundWithoutReceiverLeaf,
4394
+ tapTree: vhtlcScript.encode()
4395
+ };
4396
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4397
+ sweptCount++;
4398
+ }
4399
+ }
4234
4400
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4235
4401
  await this.savePendingChainSwap({
4236
4402
  ...pendingSwap,
4237
4403
  status: finalStatus.status
4238
4404
  });
4405
+ return { swept: sweptCount, skipped: skippedCount };
4239
4406
  }
4240
4407
  // =========================================================================
4241
4408
  // Chain swaps: BTC -> ARK
@@ -4656,7 +4823,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4656
4823
  this.validateQuoteOptions(options);
4657
4824
  const floor = await this.resolveQuoteFloor(swapId, options);
4658
4825
  const slippageBps = options?.maxSlippageBps ?? 0;
4659
- return Math.floor(floor - floor * slippageBps / 1e4);
4826
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4827
+ if (effectiveFloor < 1) {
4828
+ throw new TypeError(
4829
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4830
+ );
4831
+ }
4832
+ return effectiveFloor;
4660
4833
  }
4661
4834
  async resolveQuoteFloor(swapId, options) {
4662
4835
  if (options?.minAcceptableAmount !== void 0) {
@@ -4692,7 +4865,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4692
4865
  }
4693
4866
  }
4694
4867
  validateQuote(amount, effectiveFloor) {
4695
- if (!(amount > 0)) {
4868
+ if (!Number.isSafeInteger(amount)) {
4869
+ throw new QuoteRejectedError({
4870
+ reason: "non_safe_integer",
4871
+ quotedAmount: amount
4872
+ });
4873
+ }
4874
+ if (amount <= 0) {
4696
4875
  throw new QuoteRejectedError({
4697
4876
  reason: "non_positive",
4698
4877
  quotedAmount: amount
@@ -5087,6 +5266,7 @@ function createBackgroundWalletShim(args) {
5087
5266
  getBoardingUtxos: async () => notImplemented("getBoardingUtxos"),
5088
5267
  getTransactionHistory: async () => notImplemented("getTransactionHistory"),
5089
5268
  getContractManager: async () => notImplemented("getContractManager"),
5269
+ getDelegateManager: async () => notImplemented("getDelegateManager"),
5090
5270
  getDelegatorManager: async () => notImplemented("getDelegatorManager"),
5091
5271
  sendBitcoin: async () => notImplemented("sendBitcoin"),
5092
5272
  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-5K2FS2FE.js";
5
5
  import {
6
6
  BoltzSwapProvider
7
- } from "../chunk-SJ5SYSMK.js";
7
+ } from "../chunk-TDBUZE4N.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"),