@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.
@@ -145,6 +145,8 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
145
145
  return `Boltz quote ${options.quotedAmount} is below acceptable floor ${options.floor}`;
146
146
  case "non_positive":
147
147
  return `Boltz quote ${options.quotedAmount} is not positive`;
148
+ case "non_safe_integer":
149
+ return `Boltz quote ${options.quotedAmount} is not a safe positive satoshi integer`;
148
150
  case "no_baseline":
149
151
  return "Cannot accept quote: no minAcceptableAmount and no stored pending swap";
150
152
  }
@@ -198,6 +200,7 @@ var QuoteRejectedError = class _QuoteRejectedError extends SwapError {
198
200
  message
199
201
  });
200
202
  case "non_positive":
203
+ case "non_safe_integer":
201
204
  if (quotedAmount === null) return null;
202
205
  return new _QuoteRejectedError({
203
206
  reason,
@@ -213,6 +216,7 @@ var QUOTE_REJECTION_TRANSPORT_PREFIX = "QUOTE_REJECTED::";
213
216
  var QUOTE_REJECTION_REASONS = /* @__PURE__ */ new Set([
214
217
  "below_floor",
215
218
  "non_positive",
219
+ "non_safe_integer",
216
220
  "no_baseline"
217
221
  ]);
218
222
  var BoltzRefundError = class extends Error {
@@ -1373,6 +1377,13 @@ var SwapManager = class _SwapManager {
1373
1377
  * enough that a real "swap unknown to this provider" surfaces quickly.
1374
1378
  */
1375
1379
  static NOT_FOUND_THRESHOLD = 10;
1380
+ /**
1381
+ * Delay between re-attempts of a chain refund that left VTXOs deferred
1382
+ * (e.g. pre-CLTV recoverable VTXO, or Boltz 3-of-3 rejected before CLTV
1383
+ * has elapsed). Boltz won't send another status update once the swap
1384
+ * is `swap.expired`, so the manager owns the local retry cadence.
1385
+ */
1386
+ static REFUND_RETRY_DELAY_MS = 6e4;
1376
1387
  swapProvider;
1377
1388
  config;
1378
1389
  // Event listeners storage (supports multiple listeners per event)
@@ -1391,6 +1402,11 @@ var SwapManager = class _SwapManager {
1391
1402
  reconnectTimer = null;
1392
1403
  initialPollTimer = null;
1393
1404
  pollRetryTimers = /* @__PURE__ */ new Map();
1405
+ // Per-swap retry timers for chain refunds that left work undone
1406
+ // (refundArk returned `skipped > 0`). The swap is held in
1407
+ // `monitoredSwaps` past its terminal Boltz status until the local
1408
+ // refund completes or the manager stops.
1409
+ refundRetryTimers = /* @__PURE__ */ new Map();
1394
1410
  // Per-swap counter of consecutive `SwapNotFoundError` responses from
1395
1411
  // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1396
1412
  // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
@@ -1587,6 +1603,10 @@ var SwapManager = class _SwapManager {
1587
1603
  clearTimeout(timer);
1588
1604
  }
1589
1605
  this.pollRetryTimers.clear();
1606
+ for (const timer of this.refundRetryTimers.values()) {
1607
+ clearTimeout(timer);
1608
+ }
1609
+ this.refundRetryTimers.clear();
1590
1610
  this.notFoundCounts.clear();
1591
1611
  }
1592
1612
  /**
@@ -1643,6 +1663,11 @@ var SwapManager = class _SwapManager {
1643
1663
  clearTimeout(retryTimer);
1644
1664
  this.pollRetryTimers.delete(swapId);
1645
1665
  }
1666
+ const refundRetryTimer = this.refundRetryTimers.get(swapId);
1667
+ if (refundRetryTimer) {
1668
+ clearTimeout(refundRetryTimer);
1669
+ this.refundRetryTimers.delete(swapId);
1670
+ }
1646
1671
  this.notFoundCounts.delete(swapId);
1647
1672
  logger.log(`Removed swap ${swapId} from monitoring`);
1648
1673
  }
@@ -1914,15 +1939,57 @@ var SwapManager = class _SwapManager {
1914
1939
  await this.executeAutonomousAction(swap);
1915
1940
  }
1916
1941
  if (this.isFinalStatus(swap)) {
1917
- this.monitoredSwaps.delete(swap.id);
1918
- this.swapSubscriptions.delete(swap.id);
1919
- const retryTimer = this.pollRetryTimers.get(swap.id);
1920
- if (retryTimer) {
1921
- clearTimeout(retryTimer);
1922
- this.pollRetryTimers.delete(swap.id);
1942
+ if (this.refundRetryTimers.has(swap.id)) {
1943
+ return;
1923
1944
  }
1924
- this.swapCompletedListeners.forEach((listener) => listener(swap));
1945
+ this.finalizeMonitoredSwap(swap);
1946
+ }
1947
+ }
1948
+ /**
1949
+ * Drop a swap from monitoring and emit the terminal completion event.
1950
+ * Shared between the on-status-update finalization path and the
1951
+ * refund-retry finalization path (used when a previously-deferred
1952
+ * chain refund has finished its remaining work).
1953
+ */
1954
+ finalizeMonitoredSwap(swap) {
1955
+ if (!this.monitoredSwaps.has(swap.id)) return;
1956
+ this.monitoredSwaps.delete(swap.id);
1957
+ this.swapSubscriptions.delete(swap.id);
1958
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1959
+ if (retryTimer) {
1960
+ clearTimeout(retryTimer);
1961
+ this.pollRetryTimers.delete(swap.id);
1962
+ }
1963
+ const refundRetry = this.refundRetryTimers.get(swap.id);
1964
+ if (refundRetry) {
1965
+ clearTimeout(refundRetry);
1966
+ this.refundRetryTimers.delete(swap.id);
1925
1967
  }
1968
+ this.swapCompletedListeners.forEach((listener) => listener(swap));
1969
+ }
1970
+ /**
1971
+ * Schedule another `executeAutonomousAction` run for a chain swap whose
1972
+ * refund left VTXOs deferred. After the retry completes, if no further
1973
+ * deferral was reported, finalize monitoring cleanup.
1974
+ */
1975
+ scheduleRefundRetry(swap, delayMs) {
1976
+ const existing = this.refundRetryTimers.get(swap.id);
1977
+ if (existing) clearTimeout(existing);
1978
+ this.refundRetryTimers.set(
1979
+ swap.id,
1980
+ setTimeout(async () => {
1981
+ this.refundRetryTimers.delete(swap.id);
1982
+ if (!this.isRunning) return;
1983
+ if (!this.monitoredSwaps.has(swap.id)) return;
1984
+ try {
1985
+ await this.executeAutonomousAction(swap);
1986
+ } finally {
1987
+ if (!this.refundRetryTimers.has(swap.id) && this.isFinalStatus(swap)) {
1988
+ this.finalizeMonitoredSwap(swap);
1989
+ }
1990
+ }
1991
+ }, delayMs)
1992
+ );
1926
1993
  }
1927
1994
  /**
1928
1995
  * Execute autonomous action based on swap status
@@ -1977,10 +2044,27 @@ var SwapManager = class _SwapManager {
1977
2044
  } else if (isChainRefundableStatus(swap.status)) {
1978
2045
  if (swap.request.from === "ARK") {
1979
2046
  logger.log(`Auto-refunding ARK chain swap ${swap.id}`);
1980
- await this.executeRefundArkAction(swap);
1981
- this.actionExecutedListeners.forEach(
1982
- (listener) => listener(swap, "refundArk")
1983
- );
2047
+ try {
2048
+ const outcome = await this.executeRefundArkAction(swap);
2049
+ if (outcome && outcome.skipped > 0) {
2050
+ logger.log(
2051
+ `Chain swap ${swap.id}: ${outcome.skipped} VTXO(s) deferred \u2014 scheduling refund retry`
2052
+ );
2053
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2054
+ }
2055
+ this.actionExecutedListeners.forEach(
2056
+ (listener) => listener(swap, "refundArk")
2057
+ );
2058
+ } catch (error) {
2059
+ logger.error(
2060
+ `Auto-refunding ARK chain swap ${swap.id} failed; scheduling retry`,
2061
+ error
2062
+ );
2063
+ this.swapFailedListeners.forEach(
2064
+ (listener) => listener(swap, error)
2065
+ );
2066
+ this.scheduleRefundRetry(swap, _SwapManager.REFUND_RETRY_DELAY_MS);
2067
+ }
1984
2068
  }
1985
2069
  if (swap.request.from === "BTC") {
1986
2070
  logger.warn(
@@ -2061,7 +2145,7 @@ var SwapManager = class _SwapManager {
2061
2145
  logger.error("refundArk callback not set");
2062
2146
  return;
2063
2147
  }
2064
- await this.refundArkCallback(swap);
2148
+ return this.refundArkCallback(swap);
2065
2149
  }
2066
2150
  /**
2067
2151
  * Execute sign server claim action for chain swap.
@@ -2161,9 +2245,7 @@ var SwapManager = class _SwapManager {
2161
2245
  */
2162
2246
  async pollAllSwaps() {
2163
2247
  if (this.monitoredSwaps.size === 0) return;
2164
- const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2165
- (swap) => this.pollSingleSwap(swap)
2166
- );
2248
+ const pollPromises = Array.from(this.monitoredSwaps.values()).filter((swap) => !this.refundRetryTimers.has(swap.id)).map((swap) => this.pollSingleSwap(swap));
2167
2249
  await Promise.allSettled(pollPromises);
2168
2250
  }
2169
2251
  async pollSingleSwap(swap) {
@@ -2216,6 +2298,7 @@ var SwapManager = class _SwapManager {
2216
2298
  * Boltz endpoint).
2217
2299
  */
2218
2300
  async handleSwapNotFound(swap) {
2301
+ if (this.refundRetryTimers.has(swap.id)) return;
2219
2302
  const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2220
2303
  this.notFoundCounts.set(swap.id, count);
2221
2304
  logger.warn(
@@ -2236,6 +2319,7 @@ var SwapManager = class _SwapManager {
2236
2319
  * 404s without recovering anything.
2237
2320
  */
2238
2321
  async markSwapAsUnknownToProvider(swap) {
2322
+ if (this.refundRetryTimers.has(swap.id)) return;
2239
2323
  if (!this.monitoredSwaps.has(swap.id)) {
2240
2324
  this.notFoundCounts.delete(swap.id);
2241
2325
  return;
@@ -2248,6 +2332,11 @@ var SwapManager = class _SwapManager {
2248
2332
  clearTimeout(retryTimer);
2249
2333
  this.pollRetryTimers.delete(swap.id);
2250
2334
  }
2335
+ const refundRetryTimer = this.refundRetryTimers.get(swap.id);
2336
+ if (refundRetryTimer) {
2337
+ clearTimeout(refundRetryTimer);
2338
+ this.refundRetryTimers.delete(swap.id);
2339
+ }
2251
2340
  this.notFoundCounts.delete(swap.id);
2252
2341
  this.swapUpdateListeners.forEach((listener) => listener(swap, oldStatus));
2253
2342
  const subscribers = this.swapSubscriptions.get(swap.id);
@@ -2998,7 +3087,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
2998
3087
  await this.claimBtc(swap);
2999
3088
  },
3000
3089
  refundArk: async (swap) => {
3001
- await this.refundArk(swap);
3090
+ return this.refundArk(swap);
3002
3091
  },
3003
3092
  signServerClaim: async (swap) => {
3004
3093
  await this.signCooperativeClaimForServer(swap);
@@ -3211,51 +3300,67 @@ var ArkadeSwaps = class _ArkadeSwaps {
3211
3300
  throw new Error(
3212
3301
  `Swap ${pendingSwap.id}: VHTLC address mismatch. Expected ${lockupAddress}, got ${vhtlcAddress}`
3213
3302
  );
3214
- let vtxo;
3303
+ let unspentVtxos = [];
3304
+ let rawVtxos = [];
3215
3305
  for (let attempt = 1; attempt <= CLAIM_VTXO_RETRY_ATTEMPTS; attempt++) {
3216
- const { vtxos } = await this.indexerProvider.getVtxos({
3306
+ const result = await this.indexerProvider.getVtxos({
3217
3307
  scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
3218
3308
  });
3219
- if (vtxos.length > 0) {
3220
- vtxo = vtxos[0];
3309
+ rawVtxos = result.vtxos;
3310
+ unspentVtxos = result.vtxos.filter((vtxo) => !vtxo.isSpent);
3311
+ if (unspentVtxos.length > 0) {
3221
3312
  break;
3222
3313
  }
3223
3314
  if (attempt < CLAIM_VTXO_RETRY_ATTEMPTS) {
3224
3315
  await new Promise((resolve) => setTimeout(resolve, CLAIM_VTXO_RETRY_DELAY_MS));
3225
3316
  }
3226
3317
  }
3227
- if (!vtxo) {
3228
- throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3229
- }
3230
- if (vtxo.isSpent) {
3318
+ if (unspentVtxos.length === 0) {
3319
+ if (rawVtxos.length === 0) {
3320
+ throw new Error(`Swap ${pendingSwap.id}: no spendable virtual coins found`);
3321
+ }
3231
3322
  throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
3232
3323
  }
3233
- const input = {
3234
- ...vtxo,
3235
- tapLeafScript: vhtlcScript.claim(),
3236
- tapTree: vhtlcScript.encode()
3237
- };
3238
- const output = {
3239
- amount: BigInt(vtxo.value),
3240
- script: import_sdk8.ArkAddress.decode(address).pkScript
3241
- };
3242
3324
  const vhtlcIdentity = claimVHTLCIdentity(this.wallet.identity, preimage);
3243
- let finalStatus;
3244
- if ((0, import_sdk8.isRecoverable)(vtxo)) {
3245
- await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3246
- finalStatus = "transaction.claimed";
3247
- } else {
3248
- await claimVHTLCwithOffchainTx(
3249
- vhtlcIdentity,
3250
- vhtlcScript,
3251
- serverXOnly,
3252
- input,
3253
- output,
3254
- arkInfo,
3255
- this.arkProvider
3325
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
3326
+ const claimErrors = [];
3327
+ let usedOffchainClaim = false;
3328
+ for (const vtxo of unspentVtxos) {
3329
+ const input = {
3330
+ ...vtxo,
3331
+ tapLeafScript: vhtlcScript.claim(),
3332
+ tapTree: vhtlcScript.encode()
3333
+ };
3334
+ const output = {
3335
+ amount: BigInt(vtxo.value),
3336
+ script: outputScript
3337
+ };
3338
+ try {
3339
+ if ((0, import_sdk8.isRecoverable)(vtxo)) {
3340
+ await this.joinBatch(vhtlcIdentity, input, output, arkInfo);
3341
+ } else {
3342
+ await claimVHTLCwithOffchainTx(
3343
+ vhtlcIdentity,
3344
+ vhtlcScript,
3345
+ serverXOnly,
3346
+ input,
3347
+ output,
3348
+ arkInfo,
3349
+ this.arkProvider
3350
+ );
3351
+ usedOffchainClaim = true;
3352
+ }
3353
+ } catch (error) {
3354
+ claimErrors.push({ vtxo, error });
3355
+ }
3356
+ }
3357
+ if (claimErrors.length > 0) {
3358
+ const details = claimErrors.map(({ vtxo, error }) => `${vtxo.txid}:${vtxo.vout} (${error.message})`).join("; ");
3359
+ throw new Error(
3360
+ `Swap ${pendingSwap.id}: failed to claim ${claimErrors.length}/${unspentVtxos.length} VTXOs: ${details}`
3256
3361
  );
3257
- finalStatus = (await this.getSwapStatus(pendingSwap.id)).status;
3258
3362
  }
3363
+ const finalStatus = usedOffchainClaim ? (await this.getSwapStatus(pendingSwap.id)).status : "transaction.claimed";
3259
3364
  await updateReverseSwapStatus(
3260
3365
  pendingSwap,
3261
3366
  finalStatus,
@@ -3630,6 +3735,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
3630
3735
  if (boltzCallCount > 0) {
3631
3736
  await new Promise((r) => setTimeout(r, 2e3));
3632
3737
  }
3738
+ boltzCallCount++;
3633
3739
  await refundVHTLCwithOffchainTx(
3634
3740
  pendingSwap.id,
3635
3741
  this.wallet.identity,
@@ -3642,7 +3748,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
3642
3748
  arkInfo,
3643
3749
  this.swapProvider.refundSubmarineSwap.bind(this.swapProvider)
3644
3750
  );
3645
- boltzCallCount++;
3646
3751
  sweptCount++;
3647
3752
  } catch (error) {
3648
3753
  if (!(error instanceof BoltzRefundError)) {
@@ -4140,8 +4245,17 @@ var ArkadeSwaps = class _ArkadeSwaps {
4140
4245
  await this.swapProvider.postBtcTransaction(claimTx.hex);
4141
4246
  }
4142
4247
  /**
4143
- * When an ARK to BTC swap fails, refund sats on ARK chain by claiming the VHTLC.
4248
+ * When an ARK to BTC swap fails, refund every unspent VTXO at the chain
4249
+ * swap's ARK lockup address.
4250
+ *
4251
+ * Path selection per VTXO:
4252
+ * - CLTV has elapsed → `refundWithoutReceiver` via `joinBatch` (no Boltz).
4253
+ * - Pre-CLTV recoverable → skipped (Boltz can't co-sign swept-batch refund).
4254
+ * - Pre-CLTV non-recoverable → cooperative 3-of-3 refund via Boltz.
4255
+ *
4144
4256
  * @param pendingSwap - The pending chain swap to refund.
4257
+ * @returns Counts of VTXOs swept vs. deferred. A `swept: 0` outcome means
4258
+ * the call was a no-op — callers should retry after CLTV.
4145
4259
  */
4146
4260
  async refundArk(pendingSwap) {
4147
4261
  if (!pendingSwap.response.lockupDetails.serverPublicKey)
@@ -4165,21 +4279,6 @@ var ArkadeSwaps = class _ArkadeSwaps {
4165
4279
  "boltz",
4166
4280
  pendingSwap.id
4167
4281
  );
4168
- const vhtlcPkScript = import_sdk8.ArkAddress.decode(
4169
- pendingSwap.response.lockupDetails.lockupAddress
4170
- ).pkScript;
4171
- const { vtxos } = await this.indexerProvider.getVtxos({
4172
- scripts: [import_base9.hex.encode(vhtlcPkScript)]
4173
- });
4174
- if (vtxos.length === 0) {
4175
- throw new Error(
4176
- `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4177
- );
4178
- }
4179
- const vtxo = vtxos[0];
4180
- if (vtxo.isSpent) {
4181
- throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4182
- }
4183
4282
  const { vhtlcAddress, vhtlcScript } = this.createVHTLCScript({
4184
4283
  network: arkInfo.network,
4185
4284
  preimageHash: import_base9.hex.decode(pendingSwap.request.preimageHash),
@@ -4195,37 +4294,105 @@ var ArkadeSwaps = class _ArkadeSwaps {
4195
4294
  message: "Unable to claim: invalid VHTLC address"
4196
4295
  });
4197
4296
  }
4198
- const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4199
- const input = {
4200
- ...vtxo,
4201
- tapLeafScript: isRecoverableVtxo ? vhtlcScript.refundWithoutReceiver() : vhtlcScript.refund(),
4202
- tapTree: vhtlcScript.encode()
4203
- };
4204
- const output = {
4205
- amount: BigInt(vtxo.value),
4206
- script: import_sdk8.ArkAddress.decode(address).pkScript
4207
- };
4208
- if (isRecoverableVtxo) {
4209
- await this.joinBatch(this.wallet.identity, input, output, arkInfo);
4210
- } else {
4211
- await refundVHTLCwithOffchainTx(
4212
- pendingSwap.id,
4213
- this.wallet.identity,
4214
- this.arkProvider,
4215
- boltzXOnlyPublicKey,
4216
- ourXOnlyPublicKey,
4217
- serverXOnlyPublicKey,
4218
- input,
4219
- output,
4220
- arkInfo,
4221
- this.swapProvider.refundChainSwap.bind(this.swapProvider)
4297
+ const { vtxos } = await this.indexerProvider.getVtxos({
4298
+ scripts: [import_base9.hex.encode(vhtlcScript.pkScript)]
4299
+ });
4300
+ if (vtxos.length === 0) {
4301
+ throw new Error(
4302
+ `Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.lockupDetails.lockupAddress}`
4222
4303
  );
4223
4304
  }
4305
+ const unspentVtxos = vtxos.filter((vtxo) => !vtxo.isSpent);
4306
+ if (unspentVtxos.length === 0) {
4307
+ throw new Error(`Swap ${pendingSwap.id}: VHTLC is already spent`);
4308
+ }
4309
+ const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
4310
+ const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
4311
+ const refundLocktime = pendingSwap.response.lockupDetails.timeouts.refund;
4312
+ let boltzCallCount = 0;
4313
+ let sweptCount = 0;
4314
+ let skippedCount = 0;
4315
+ for (const vtxo of unspentVtxos) {
4316
+ const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
4317
+ const output = {
4318
+ amount: BigInt(vtxo.value),
4319
+ script: outputScript
4320
+ };
4321
+ if (isSubmarineRefundLocktimeReached(refundLocktime)) {
4322
+ const input2 = {
4323
+ ...vtxo,
4324
+ tapLeafScript: refundWithoutReceiverLeaf,
4325
+ tapTree: vhtlcScript.encode()
4326
+ };
4327
+ await this.joinBatch(
4328
+ this.wallet.identity,
4329
+ input2,
4330
+ output,
4331
+ arkInfo,
4332
+ isRecoverableVtxo
4333
+ );
4334
+ sweptCount++;
4335
+ continue;
4336
+ }
4337
+ if (isRecoverableVtxo) {
4338
+ logger.error(
4339
+ `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.`
4340
+ );
4341
+ skippedCount++;
4342
+ continue;
4343
+ }
4344
+ const input = {
4345
+ ...vtxo,
4346
+ tapLeafScript: vhtlcScript.refund(),
4347
+ tapTree: vhtlcScript.encode()
4348
+ };
4349
+ try {
4350
+ if (boltzCallCount > 0) {
4351
+ await new Promise((r) => setTimeout(r, 2e3));
4352
+ }
4353
+ boltzCallCount++;
4354
+ await refundVHTLCwithOffchainTx(
4355
+ pendingSwap.id,
4356
+ this.wallet.identity,
4357
+ this.arkProvider,
4358
+ boltzXOnlyPublicKey,
4359
+ ourXOnlyPublicKey,
4360
+ serverXOnlyPublicKey,
4361
+ input,
4362
+ output,
4363
+ arkInfo,
4364
+ this.swapProvider.refundChainSwap.bind(this.swapProvider)
4365
+ );
4366
+ sweptCount++;
4367
+ } catch (error) {
4368
+ if (!(error instanceof BoltzRefundError)) {
4369
+ throw error;
4370
+ }
4371
+ if (!isSubmarineRefundLocktimeReached(refundLocktime)) {
4372
+ logger.error(
4373
+ `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.`
4374
+ );
4375
+ skippedCount++;
4376
+ continue;
4377
+ }
4378
+ logger.warn(
4379
+ `Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint, falling back to refundWithoutReceiver via joinBatch`
4380
+ );
4381
+ const fallbackInput = {
4382
+ ...vtxo,
4383
+ tapLeafScript: refundWithoutReceiverLeaf,
4384
+ tapTree: vhtlcScript.encode()
4385
+ };
4386
+ await this.joinBatch(this.wallet.identity, fallbackInput, output, arkInfo, false);
4387
+ sweptCount++;
4388
+ }
4389
+ }
4224
4390
  const finalStatus = await this.getSwapStatus(pendingSwap.id);
4225
4391
  await this.savePendingChainSwap({
4226
4392
  ...pendingSwap,
4227
4393
  status: finalStatus.status
4228
4394
  });
4395
+ return { swept: sweptCount, skipped: skippedCount };
4229
4396
  }
4230
4397
  // =========================================================================
4231
4398
  // Chain swaps: BTC -> ARK
@@ -4646,7 +4813,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4646
4813
  this.validateQuoteOptions(options);
4647
4814
  const floor = await this.resolveQuoteFloor(swapId, options);
4648
4815
  const slippageBps = options?.maxSlippageBps ?? 0;
4649
- return Math.floor(floor - floor * slippageBps / 1e4);
4816
+ const effectiveFloor = Math.floor(floor - floor * slippageBps / 1e4);
4817
+ if (effectiveFloor < 1) {
4818
+ throw new TypeError(
4819
+ `Invalid quote configuration: maxSlippageBps=${slippageBps} reduces floor ${floor} below 1 sat`
4820
+ );
4821
+ }
4822
+ return effectiveFloor;
4650
4823
  }
4651
4824
  async resolveQuoteFloor(swapId, options) {
4652
4825
  if (options?.minAcceptableAmount !== void 0) {
@@ -4682,7 +4855,13 @@ var ArkadeSwaps = class _ArkadeSwaps {
4682
4855
  }
4683
4856
  }
4684
4857
  validateQuote(amount, effectiveFloor) {
4685
- if (!(amount > 0)) {
4858
+ if (!Number.isSafeInteger(amount)) {
4859
+ throw new QuoteRejectedError({
4860
+ reason: "non_safe_integer",
4861
+ quotedAmount: amount
4862
+ });
4863
+ }
4864
+ if (amount <= 0) {
4686
4865
  throw new QuoteRejectedError({
4687
4866
  reason: "non_positive",
4688
4867
  quotedAmount: amount
@@ -1,7 +1,7 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-9M7FRuq1.cjs';
2
- import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, k as ArkToBtcResponse, l as BtcToArkResponse, d as Chain, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-CrKkVzBB.cjs';
3
- import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-CuITxZie.cjs';
4
- export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-CuITxZie.cjs';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-CfMets16.cjs';
2
+ import { o as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, k as ArkToBtcResponse, m as ChainArkRefundOutcome, l as BtcToArkResponse, d as Chain, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types--axEWA8c.cjs';
3
+ import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-BpAqG0V6.cjs';
4
+ export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-BpAqG0V6.cjs';
5
5
  import { ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
6
6
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
7
7
  import '@arkade-os/sdk/worker/expo';
@@ -116,7 +116,7 @@ declare class ExpoArkadeSwaps implements IArkadeSwaps {
116
116
  txid: string;
117
117
  }>;
118
118
  claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
119
- refundArk(pendingSwap: BoltzChainSwap): Promise<void>;
119
+ refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
120
120
  btcToArk(args: {
121
121
  feeSatsPerByte?: number;
122
122
  senderLockAmount?: number;
@@ -1,7 +1,7 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-DNsyWeFr.js';
2
- import { n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, k as ArkToBtcResponse, l as BtcToArkResponse, d as Chain, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types-CrKkVzBB.js';
3
- import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-CEgeGlbP.js';
4
- export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-CEgeGlbP.js';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps, Q as QuoteSwapOptions, V as VhtlcTimeouts } from '../arkade-swaps-BXAD1s8j.js';
2
+ import { o as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, b as BoltzReverseSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, F as FeesResponse, a as BoltzChainSwap, k as ArkToBtcResponse, m as ChainArkRefundOutcome, l as BtcToArkResponse, d as Chain, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from '../types--axEWA8c.js';
3
+ import { E as ExpoArkadeSwapsConfig } from '../swapsPollProcessor-DFVOAy_-.js';
4
+ export { D as DefineSwapBackgroundTaskOptions, b as ExpoArkadeLightningConfig, c as ExpoSwapBackgroundConfig, P as PersistedSwapBackgroundConfig, S as SWAP_POLL_TASK_TYPE, a as SwapTaskDependencies } from '../swapsPollProcessor-DFVOAy_-.js';
5
5
  import { ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
6
6
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
7
7
  import '@arkade-os/sdk/worker/expo';
@@ -116,7 +116,7 @@ declare class ExpoArkadeSwaps implements IArkadeSwaps {
116
116
  txid: string;
117
117
  }>;
118
118
  claimBtc(pendingSwap: BoltzChainSwap): Promise<void>;
119
- refundArk(pendingSwap: BoltzChainSwap): Promise<void>;
119
+ refundArk(pendingSwap: BoltzChainSwap): Promise<ChainArkRefundOutcome>;
120
120
  btcToArk(args: {
121
121
  feeSatsPerByte?: number;
122
122
  senderLockAmount?: number;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SWAP_POLL_TASK_TYPE
3
- } from "../chunk-HNQDJOLM.js";
3
+ } from "../chunk-5K2FS2FE.js";
4
4
  import {
5
5
  ArkadeSwaps
6
- } from "../chunk-SJ5SYSMK.js";
6
+ } from "../chunk-TDBUZE4N.js";
7
7
  import "../chunk-SJQJQO7P.js";
8
8
 
9
9
  // src/expo/arkade-lightning.ts