@arkade-os/boltz-swap 0.3.25 → 0.3.26

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.
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { p as BoltzSwapProvider, Y as SwapManager, m as SwapRepository, _ as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-LCXS1AVA.cjs';
2
+ import { p as BoltzSwapProvider, Y as SwapManager, m as SwapRepository, _ as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-BBI7-KJ0.cjs';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { IWallet, ArkProvider, IndexerProvider, ArkInfo, Identity, ArkTxInput, VHTLC } from '@arkade-os/sdk';
2
- import { p as BoltzSwapProvider, Y as SwapManager, m as SwapRepository, _ as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-LCXS1AVA.js';
2
+ import { p as BoltzSwapProvider, Y as SwapManager, m as SwapRepository, _ as ArkadeSwapsCreateConfig, A as ArkadeSwapsConfig, n as SwapManagerClient, C as CreateLightningInvoiceRequest, e as CreateLightningInvoiceResponse, b as BoltzReverseSwap, S as SendLightningPaymentRequest, f as SendLightningPaymentResponse, c as BoltzSubmarineSwap, g as SubmarineRefundOutcome, h as SubmarineRecoveryInfo, i as SubmarineRecoveryResult, k as ArkToBtcResponse, a as BoltzChainSwap, l as BtcToArkResponse, d as Chain, F as FeesResponse, j as ChainFeesResponse, L as LimitsResponse, G as GetSwapStatusResponse, B as BoltzSwap } from './types-BBI7-KJ0.js';
3
3
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';
4
4
 
5
5
  /**
@@ -2,8 +2,8 @@ import {
2
2
  defineExpoSwapBackgroundTask,
3
3
  registerExpoSwapBackgroundTask,
4
4
  unregisterExpoSwapBackgroundTask
5
- } from "./chunk-FEXQELYZ.js";
6
- import "./chunk-XC2ARJZO.js";
5
+ } from "./chunk-X3JNWDAR.js";
6
+ import "./chunk-LWUXSE5N.js";
7
7
  import "./chunk-3RG5ZIWI.js";
8
8
  export {
9
9
  defineExpoSwapBackgroundTask,
@@ -47,6 +47,19 @@ var NetworkError = class extends Error {
47
47
  this.errorData = errorData;
48
48
  }
49
49
  };
50
+ var SwapNotFoundError = class extends NetworkError {
51
+ /** The swap ID Boltz did not recognise. */
52
+ swapId;
53
+ constructor(swapId, errorData) {
54
+ super(
55
+ `Boltz returned 404 for swap '${swapId}': swap unknown to this Boltz instance`,
56
+ 404,
57
+ errorData
58
+ );
59
+ this.name = "SwapNotFoundError";
60
+ this.swapId = swapId;
61
+ }
62
+ };
50
63
  var SchemaError = class extends SwapError {
51
64
  constructor(options = {}) {
52
65
  super({ message: "Invalid API response", ...options });
@@ -302,10 +315,17 @@ var isCreateSwapsRestoreResponse = (data) => {
302
315
  );
303
316
  };
304
317
  var BASE_URLS = {
305
- bitcoin: "https://api.ark.boltz.exchange",
318
+ bitcoin: "https://api.boltz.exchange",
306
319
  mutinynet: "https://api.boltz.mutinynet.arkade.sh",
307
320
  regtest: "http://localhost:9069"
308
321
  };
322
+ var isSwapNotFoundBody = (error) => {
323
+ const needle = "could not find swap";
324
+ const fromJson = error.errorData?.error;
325
+ if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle))
326
+ return true;
327
+ return error.message.toLowerCase().includes(needle);
328
+ };
309
329
  var BoltzSwapProvider = class {
310
330
  wsUrl;
311
331
  apiUrl;
@@ -397,12 +417,27 @@ var BoltzSwapProvider = class {
397
417
  });
398
418
  return res;
399
419
  }
400
- /** Queries the current status of a swap by ID. */
420
+ /**
421
+ * Queries the current status of a swap by ID.
422
+ *
423
+ * @throws {SwapNotFoundError} when Boltz responds with HTTP 404 and a body
424
+ * matching the "could not find swap" pattern. Distinct from a generic 404
425
+ * so callers (e.g. SwapManager polling) can drive a per-swap unknown-to-
426
+ * provider counter without tripping on transient route or proxy errors.
427
+ */
401
428
  async getSwapStatus(id) {
402
- const response = await this.request(
403
- `/v2/swap/${id}`,
404
- "GET"
405
- );
429
+ let response;
430
+ try {
431
+ response = await this.request(
432
+ `/v2/swap/${id}`,
433
+ "GET"
434
+ );
435
+ } catch (error) {
436
+ if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) {
437
+ throw new SwapNotFoundError(id, error.errorData);
438
+ }
439
+ throw error;
440
+ }
406
441
  if (!isGetSwapStatusResponse(response))
407
442
  throw new SchemaError({
408
443
  message: `error fetching status for swap: ${id}`
@@ -881,7 +916,15 @@ function setLogger(customLogger) {
881
916
  }
882
917
 
883
918
  // src/swap-manager.ts
884
- var SwapManager = class {
919
+ var SwapManager = class _SwapManager {
920
+ /**
921
+ * Number of consecutive Boltz 404s for a single swap ID before the
922
+ * polling loop gives up and transitions the swap to a terminal state.
923
+ * At the default 30s poll cadence this is roughly a 5-minute grace
924
+ * window — long enough to ride out a transient Boltz blip, short
925
+ * enough that a real "swap unknown to this provider" surfaces quickly.
926
+ */
927
+ static NOT_FOUND_THRESHOLD = 10;
885
928
  swapProvider;
886
929
  config;
887
930
  // Event listeners storage (supports multiple listeners per event)
@@ -900,6 +943,13 @@ var SwapManager = class {
900
943
  reconnectTimer = null;
901
944
  initialPollTimer = null;
902
945
  pollRetryTimers = /* @__PURE__ */ new Map();
946
+ // Per-swap counter of consecutive `SwapNotFoundError` responses from
947
+ // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
948
+ // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
949
+ // swap is transitioned to `swap.expired` (terminal) and dropped from
950
+ // monitoring — typically the canonical failure mode after a Boltz
951
+ // endpoint switch, where old swap IDs are unknown to the new instance.
952
+ notFoundCounts = /* @__PURE__ */ new Map();
903
953
  isRunning = false;
904
954
  currentReconnectDelay;
905
955
  currentPollRetryDelay;
@@ -1091,6 +1141,7 @@ var SwapManager = class {
1091
1141
  clearTimeout(timer);
1092
1142
  }
1093
1143
  this.pollRetryTimers.clear();
1144
+ this.notFoundCounts.clear();
1094
1145
  }
1095
1146
  /**
1096
1147
  * Set the polling interval (ms).
@@ -1151,6 +1202,7 @@ var SwapManager = class {
1151
1202
  clearTimeout(retryTimer);
1152
1203
  this.pollRetryTimers.delete(swapId);
1153
1204
  }
1205
+ this.notFoundCounts.delete(swapId);
1154
1206
  logger.log(`Removed swap ${swapId} from monitoring`);
1155
1207
  }
1156
1208
  /**
@@ -1427,6 +1479,7 @@ var SwapManager = class {
1427
1479
  * This is the core logic that determines what actions to take
1428
1480
  */
1429
1481
  async handleSwapStatusUpdate(swap, newStatus) {
1482
+ this.notFoundCounts.delete(swap.id);
1430
1483
  const oldStatus = swap.status;
1431
1484
  if (oldStatus === newStatus) return;
1432
1485
  swap.status = newStatus;
@@ -1712,51 +1765,135 @@ var SwapManager = class {
1712
1765
  async pollAllSwaps() {
1713
1766
  if (this.monitoredSwaps.size === 0) return;
1714
1767
  const pollPromises = Array.from(this.monitoredSwaps.values()).map(
1715
- async (swap) => {
1768
+ (swap) => this.pollSingleSwap(swap)
1769
+ );
1770
+ await Promise.allSettled(pollPromises);
1771
+ }
1772
+ async pollSingleSwap(swap) {
1773
+ try {
1774
+ const statusResponse = await this.swapProvider.getSwapStatus(
1775
+ swap.id
1776
+ );
1777
+ this.notFoundCounts.delete(swap.id);
1778
+ if (statusResponse.status !== swap.status) {
1779
+ await this.handleSwapStatusUpdate(swap, statusResponse.status);
1780
+ }
1781
+ } catch (error) {
1782
+ if (error instanceof SwapNotFoundError) {
1783
+ await this.handleSwapNotFound(swap);
1784
+ return;
1785
+ }
1786
+ if (error instanceof NetworkError && error.statusCode === 429) {
1787
+ logger.warn(
1788
+ `Rate-limited polling swap ${swap.id}, retrying in 2s`
1789
+ );
1790
+ const existing = this.pollRetryTimers.get(swap.id);
1791
+ if (existing) clearTimeout(existing);
1792
+ this.pollRetryTimers.set(
1793
+ swap.id,
1794
+ setTimeout(async () => {
1795
+ this.pollRetryTimers.delete(swap.id);
1796
+ try {
1797
+ const retry = await this.swapProvider.getSwapStatus(
1798
+ swap.id
1799
+ );
1800
+ this.notFoundCounts.delete(swap.id);
1801
+ if (retry.status !== swap.status) {
1802
+ await this.handleSwapStatusUpdate(
1803
+ swap,
1804
+ retry.status
1805
+ );
1806
+ }
1807
+ } catch (retryError) {
1808
+ if (retryError instanceof SwapNotFoundError) {
1809
+ await this.handleSwapNotFound(swap);
1810
+ return;
1811
+ }
1812
+ logger.error(
1813
+ `Retry poll for swap ${swap.id} also failed:`,
1814
+ retryError
1815
+ );
1816
+ }
1817
+ }, 2e3)
1818
+ );
1819
+ } else {
1820
+ logger.error(`Failed to poll swap ${swap.id}:`, error);
1821
+ }
1822
+ }
1823
+ }
1824
+ /**
1825
+ * Increment the consecutive-not-found counter and, once the threshold is
1826
+ * reached, transition the swap to a terminal state and stop polling it.
1827
+ * Driven from {@link pollSingleSwap} when `getSwapStatus` throws
1828
+ * {@link SwapNotFoundError}. The threshold rides out a transient blip
1829
+ * but ensures we stop hammering Boltz with requests for swap IDs the
1830
+ * server has no record of (e.g. after switching the configured
1831
+ * Boltz endpoint).
1832
+ */
1833
+ async handleSwapNotFound(swap) {
1834
+ const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
1835
+ this.notFoundCounts.set(swap.id, count);
1836
+ logger.warn(
1837
+ `Swap ${swap.id}: unknown to Boltz (${count}/${_SwapManager.NOT_FOUND_THRESHOLD} consecutive)`
1838
+ );
1839
+ if (count >= _SwapManager.NOT_FOUND_THRESHOLD) {
1840
+ await this.markSwapAsUnknownToProvider(swap);
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Transition a swap to {@code swap.expired} (terminal for all swap types)
1845
+ * after Boltz has consistently reported it unknown for
1846
+ * {@link SwapManager.NOT_FOUND_THRESHOLD} consecutive polls. The swap is
1847
+ * persisted, removed from monitoring, and reported via `onSwapFailed`.
1848
+ * Bypasses {@link handleSwapStatusUpdate} on purpose: we don't want to
1849
+ * trigger autonomous claim/refund actions against a Boltz instance that
1850
+ * has no record of this swap — the requests would just generate more
1851
+ * 404s without recovering anything.
1852
+ */
1853
+ async markSwapAsUnknownToProvider(swap) {
1854
+ if (!this.monitoredSwaps.has(swap.id)) {
1855
+ this.notFoundCounts.delete(swap.id);
1856
+ return;
1857
+ }
1858
+ const oldStatus = swap.status;
1859
+ swap.status = "swap.expired";
1860
+ this.monitoredSwaps.delete(swap.id);
1861
+ const retryTimer = this.pollRetryTimers.get(swap.id);
1862
+ if (retryTimer) {
1863
+ clearTimeout(retryTimer);
1864
+ this.pollRetryTimers.delete(swap.id);
1865
+ }
1866
+ this.notFoundCounts.delete(swap.id);
1867
+ this.swapUpdateListeners.forEach(
1868
+ (listener) => listener(swap, oldStatus)
1869
+ );
1870
+ const subscribers = this.swapSubscriptions.get(swap.id);
1871
+ if (subscribers) {
1872
+ subscribers.forEach((callback) => {
1716
1873
  try {
1717
- const statusResponse = await this.swapProvider.getSwapStatus(swap.id);
1718
- if (statusResponse.status !== swap.status) {
1719
- await this.handleSwapStatusUpdate(
1720
- swap,
1721
- statusResponse.status
1722
- );
1723
- }
1724
- } catch (error) {
1725
- if (error instanceof NetworkError && error.statusCode === 429) {
1726
- logger.warn(
1727
- `Rate-limited polling swap ${swap.id}, retrying in 2s`
1728
- );
1729
- const existing = this.pollRetryTimers.get(swap.id);
1730
- if (existing) clearTimeout(existing);
1731
- this.pollRetryTimers.set(
1732
- swap.id,
1733
- setTimeout(async () => {
1734
- this.pollRetryTimers.delete(swap.id);
1735
- try {
1736
- const retry = await this.swapProvider.getSwapStatus(
1737
- swap.id
1738
- );
1739
- if (retry.status !== swap.status) {
1740
- await this.handleSwapStatusUpdate(
1741
- swap,
1742
- retry.status
1743
- );
1744
- }
1745
- } catch (retryError) {
1746
- logger.error(
1747
- `Retry poll for swap ${swap.id} also failed:`,
1748
- retryError
1749
- );
1750
- }
1751
- }, 2e3)
1752
- );
1753
- } else {
1754
- logger.error(`Failed to poll swap ${swap.id}:`, error);
1755
- }
1874
+ callback(swap, oldStatus);
1875
+ } catch (subscriberError) {
1876
+ logger.error(
1877
+ `Error in swap subscription callback for ${swap.id}:`,
1878
+ subscriberError
1879
+ );
1756
1880
  }
1757
- }
1881
+ });
1882
+ }
1883
+ try {
1884
+ await this.saveSwap(swap);
1885
+ } catch (saveError) {
1886
+ logger.error(
1887
+ `Failed to persist unknown-to-provider state for swap ${swap.id}:`,
1888
+ saveError
1889
+ );
1890
+ }
1891
+ logger.warn(
1892
+ `Swap ${swap.id}: marked failed after ${_SwapManager.NOT_FOUND_THRESHOLD} consecutive Boltz 404s \u2014 swap is unknown to the configured Boltz instance`
1758
1893
  );
1759
- await Promise.allSettled(pollPromises);
1894
+ const error = new SwapNotFoundError(swap.id);
1895
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
1896
+ this.swapSubscriptions.delete(swap.id);
1760
1897
  }
1761
1898
  /**
1762
1899
  * Check if a status is final (no more updates expected)
@@ -5143,6 +5280,7 @@ export {
5143
5280
  InvoiceFailedToPayError,
5144
5281
  InsufficientFundsError,
5145
5282
  NetworkError,
5283
+ SwapNotFoundError,
5146
5284
  SchemaError,
5147
5285
  SwapExpiredError,
5148
5286
  TransactionFailedError,
@@ -8,7 +8,7 @@ import {
8
8
  isSubmarineFinalStatus,
9
9
  isSubmarineSwapRefundable,
10
10
  logger
11
- } from "./chunk-XC2ARJZO.js";
11
+ } from "./chunk-LWUXSE5N.js";
12
12
  import {
13
13
  __require
14
14
  } from "./chunk-3RG5ZIWI.js";
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/errors.ts
34
- var SwapError, InvoiceExpiredError, InvoiceFailedToPayError, NetworkError, SchemaError, SwapExpiredError, TransactionFailedError, TransactionLockupFailedError, TransactionRefundedError, BoltzRefundError;
34
+ var SwapError, InvoiceExpiredError, InvoiceFailedToPayError, NetworkError, SwapNotFoundError, SchemaError, SwapExpiredError, TransactionFailedError, TransactionLockupFailedError, TransactionRefundedError, BoltzRefundError;
35
35
  var init_errors = __esm({
36
36
  "src/errors.ts"() {
37
37
  "use strict";
@@ -77,6 +77,19 @@ var init_errors = __esm({
77
77
  this.errorData = errorData;
78
78
  }
79
79
  };
80
+ SwapNotFoundError = class extends NetworkError {
81
+ /** The swap ID Boltz did not recognise. */
82
+ swapId;
83
+ constructor(swapId, errorData) {
84
+ super(
85
+ `Boltz returned 404 for swap '${swapId}': swap unknown to this Boltz instance`,
86
+ 404,
87
+ errorData
88
+ );
89
+ this.name = "SwapNotFoundError";
90
+ this.swapId = swapId;
91
+ }
92
+ };
80
93
  SchemaError = class extends SwapError {
81
94
  constructor(options = {}) {
82
95
  super({ message: "Invalid API response", ...options });
@@ -118,7 +131,7 @@ var init_errors = __esm({
118
131
  });
119
132
 
120
133
  // src/boltz-swap-provider.ts
121
- var import_sdk, import_base, isSubmarineFinalStatus, isSubmarineRefundableStatus, isSubmarineSuccessStatus, isReverseFinalStatus, isReverseClaimableStatus, isChainClaimableStatus, isChainFinalStatus, isChainRefundableStatus, isChainSignableStatus, isPendingReverseSwap, isPendingSubmarineSwap, isPendingChainSwap, isSubmarineSwapRefundable, isTimeoutBlockHeights, isGetReverseSwapTxIdResponse, isGetSwapStatusResponse, isGetSubmarinePairsResponse, isGetReversePairsResponse, isCreateSubmarineSwapResponse, isGetSwapPreimageResponse, isCreateReverseSwapResponse, isRefundSubmarineSwapResponse, isRefundChainSwapResponse, isGetChainPairsResponse, isSwapTree, isChainSwapDetailsResponse, isCreateChainSwapResponse, isGetChainClaimDetailsResponse, isPostChainClaimDetailsResponse, isGetChainQuoteResponse, isPostChainQuoteResponse, isPostBtcTransactionResponse, isLeaf, isTree, isDetails, isRestoredChainSwap, isRestoredSubmarineSwap, isRestoredReverseSwap, isCreateSwapsRestoreResponse, BASE_URLS, BoltzSwapProvider;
134
+ var import_sdk, import_base, isSubmarineFinalStatus, isSubmarineRefundableStatus, isSubmarineSuccessStatus, isReverseFinalStatus, isReverseClaimableStatus, isChainClaimableStatus, isChainFinalStatus, isChainRefundableStatus, isChainSignableStatus, isPendingReverseSwap, isPendingSubmarineSwap, isPendingChainSwap, isSubmarineSwapRefundable, isTimeoutBlockHeights, isGetReverseSwapTxIdResponse, isGetSwapStatusResponse, isGetSubmarinePairsResponse, isGetReversePairsResponse, isCreateSubmarineSwapResponse, isGetSwapPreimageResponse, isCreateReverseSwapResponse, isRefundSubmarineSwapResponse, isRefundChainSwapResponse, isGetChainPairsResponse, isSwapTree, isChainSwapDetailsResponse, isCreateChainSwapResponse, isGetChainClaimDetailsResponse, isPostChainClaimDetailsResponse, isGetChainQuoteResponse, isPostChainQuoteResponse, isPostBtcTransactionResponse, isLeaf, isTree, isDetails, isRestoredChainSwap, isRestoredSubmarineSwap, isRestoredReverseSwap, isCreateSwapsRestoreResponse, BASE_URLS, isSwapNotFoundBody, BoltzSwapProvider;
122
135
  var init_boltz_swap_provider = __esm({
123
136
  "src/boltz-swap-provider.ts"() {
124
137
  "use strict";
@@ -269,10 +282,17 @@ var init_boltz_swap_provider = __esm({
269
282
  );
270
283
  };
271
284
  BASE_URLS = {
272
- bitcoin: "https://api.ark.boltz.exchange",
285
+ bitcoin: "https://api.boltz.exchange",
273
286
  mutinynet: "https://api.boltz.mutinynet.arkade.sh",
274
287
  regtest: "http://localhost:9069"
275
288
  };
289
+ isSwapNotFoundBody = (error) => {
290
+ const needle = "could not find swap";
291
+ const fromJson = error.errorData?.error;
292
+ if (typeof fromJson === "string" && fromJson.toLowerCase().includes(needle))
293
+ return true;
294
+ return error.message.toLowerCase().includes(needle);
295
+ };
276
296
  BoltzSwapProvider = class {
277
297
  wsUrl;
278
298
  apiUrl;
@@ -364,12 +384,27 @@ var init_boltz_swap_provider = __esm({
364
384
  });
365
385
  return res;
366
386
  }
367
- /** Queries the current status of a swap by ID. */
387
+ /**
388
+ * Queries the current status of a swap by ID.
389
+ *
390
+ * @throws {SwapNotFoundError} when Boltz responds with HTTP 404 and a body
391
+ * matching the "could not find swap" pattern. Distinct from a generic 404
392
+ * so callers (e.g. SwapManager polling) can drive a per-swap unknown-to-
393
+ * provider counter without tripping on transient route or proxy errors.
394
+ */
368
395
  async getSwapStatus(id) {
369
- const response = await this.request(
370
- `/v2/swap/${id}`,
371
- "GET"
372
- );
396
+ let response;
397
+ try {
398
+ response = await this.request(
399
+ `/v2/swap/${id}`,
400
+ "GET"
401
+ );
402
+ } catch (error) {
403
+ if (error instanceof NetworkError && error.statusCode === 404 && isSwapNotFoundBody(error)) {
404
+ throw new SwapNotFoundError(id, error.errorData);
405
+ }
406
+ throw error;
407
+ }
373
408
  if (!isGetSwapStatusResponse(response))
374
409
  throw new SchemaError({
375
410
  message: `error fetching status for swap: ${id}`
@@ -1296,7 +1331,15 @@ var init_swap_manager = __esm({
1296
1331
  init_boltz_swap_provider();
1297
1332
  init_errors();
1298
1333
  init_logger();
1299
- SwapManager = class {
1334
+ SwapManager = class _SwapManager {
1335
+ /**
1336
+ * Number of consecutive Boltz 404s for a single swap ID before the
1337
+ * polling loop gives up and transitions the swap to a terminal state.
1338
+ * At the default 30s poll cadence this is roughly a 5-minute grace
1339
+ * window — long enough to ride out a transient Boltz blip, short
1340
+ * enough that a real "swap unknown to this provider" surfaces quickly.
1341
+ */
1342
+ static NOT_FOUND_THRESHOLD = 10;
1300
1343
  swapProvider;
1301
1344
  config;
1302
1345
  // Event listeners storage (supports multiple listeners per event)
@@ -1315,6 +1358,13 @@ var init_swap_manager = __esm({
1315
1358
  reconnectTimer = null;
1316
1359
  initialPollTimer = null;
1317
1360
  pollRetryTimers = /* @__PURE__ */ new Map();
1361
+ // Per-swap counter of consecutive `SwapNotFoundError` responses from
1362
+ // `getSwapStatus`. Reset on any successful poll. Once a swap reaches
1363
+ // `NOT_FOUND_THRESHOLD` consecutive 404s the safety net trips and the
1364
+ // swap is transitioned to `swap.expired` (terminal) and dropped from
1365
+ // monitoring — typically the canonical failure mode after a Boltz
1366
+ // endpoint switch, where old swap IDs are unknown to the new instance.
1367
+ notFoundCounts = /* @__PURE__ */ new Map();
1318
1368
  isRunning = false;
1319
1369
  currentReconnectDelay;
1320
1370
  currentPollRetryDelay;
@@ -1506,6 +1556,7 @@ var init_swap_manager = __esm({
1506
1556
  clearTimeout(timer);
1507
1557
  }
1508
1558
  this.pollRetryTimers.clear();
1559
+ this.notFoundCounts.clear();
1509
1560
  }
1510
1561
  /**
1511
1562
  * Set the polling interval (ms).
@@ -1566,6 +1617,7 @@ var init_swap_manager = __esm({
1566
1617
  clearTimeout(retryTimer);
1567
1618
  this.pollRetryTimers.delete(swapId);
1568
1619
  }
1620
+ this.notFoundCounts.delete(swapId);
1569
1621
  logger.log(`Removed swap ${swapId} from monitoring`);
1570
1622
  }
1571
1623
  /**
@@ -1842,6 +1894,7 @@ var init_swap_manager = __esm({
1842
1894
  * This is the core logic that determines what actions to take
1843
1895
  */
1844
1896
  async handleSwapStatusUpdate(swap, newStatus) {
1897
+ this.notFoundCounts.delete(swap.id);
1845
1898
  const oldStatus = swap.status;
1846
1899
  if (oldStatus === newStatus) return;
1847
1900
  swap.status = newStatus;
@@ -2127,51 +2180,135 @@ var init_swap_manager = __esm({
2127
2180
  async pollAllSwaps() {
2128
2181
  if (this.monitoredSwaps.size === 0) return;
2129
2182
  const pollPromises = Array.from(this.monitoredSwaps.values()).map(
2130
- async (swap) => {
2183
+ (swap) => this.pollSingleSwap(swap)
2184
+ );
2185
+ await Promise.allSettled(pollPromises);
2186
+ }
2187
+ async pollSingleSwap(swap) {
2188
+ try {
2189
+ const statusResponse = await this.swapProvider.getSwapStatus(
2190
+ swap.id
2191
+ );
2192
+ this.notFoundCounts.delete(swap.id);
2193
+ if (statusResponse.status !== swap.status) {
2194
+ await this.handleSwapStatusUpdate(swap, statusResponse.status);
2195
+ }
2196
+ } catch (error) {
2197
+ if (error instanceof SwapNotFoundError) {
2198
+ await this.handleSwapNotFound(swap);
2199
+ return;
2200
+ }
2201
+ if (error instanceof NetworkError && error.statusCode === 429) {
2202
+ logger.warn(
2203
+ `Rate-limited polling swap ${swap.id}, retrying in 2s`
2204
+ );
2205
+ const existing = this.pollRetryTimers.get(swap.id);
2206
+ if (existing) clearTimeout(existing);
2207
+ this.pollRetryTimers.set(
2208
+ swap.id,
2209
+ setTimeout(async () => {
2210
+ this.pollRetryTimers.delete(swap.id);
2211
+ try {
2212
+ const retry = await this.swapProvider.getSwapStatus(
2213
+ swap.id
2214
+ );
2215
+ this.notFoundCounts.delete(swap.id);
2216
+ if (retry.status !== swap.status) {
2217
+ await this.handleSwapStatusUpdate(
2218
+ swap,
2219
+ retry.status
2220
+ );
2221
+ }
2222
+ } catch (retryError) {
2223
+ if (retryError instanceof SwapNotFoundError) {
2224
+ await this.handleSwapNotFound(swap);
2225
+ return;
2226
+ }
2227
+ logger.error(
2228
+ `Retry poll for swap ${swap.id} also failed:`,
2229
+ retryError
2230
+ );
2231
+ }
2232
+ }, 2e3)
2233
+ );
2234
+ } else {
2235
+ logger.error(`Failed to poll swap ${swap.id}:`, error);
2236
+ }
2237
+ }
2238
+ }
2239
+ /**
2240
+ * Increment the consecutive-not-found counter and, once the threshold is
2241
+ * reached, transition the swap to a terminal state and stop polling it.
2242
+ * Driven from {@link pollSingleSwap} when `getSwapStatus` throws
2243
+ * {@link SwapNotFoundError}. The threshold rides out a transient blip
2244
+ * but ensures we stop hammering Boltz with requests for swap IDs the
2245
+ * server has no record of (e.g. after switching the configured
2246
+ * Boltz endpoint).
2247
+ */
2248
+ async handleSwapNotFound(swap) {
2249
+ const count = (this.notFoundCounts.get(swap.id) ?? 0) + 1;
2250
+ this.notFoundCounts.set(swap.id, count);
2251
+ logger.warn(
2252
+ `Swap ${swap.id}: unknown to Boltz (${count}/${_SwapManager.NOT_FOUND_THRESHOLD} consecutive)`
2253
+ );
2254
+ if (count >= _SwapManager.NOT_FOUND_THRESHOLD) {
2255
+ await this.markSwapAsUnknownToProvider(swap);
2256
+ }
2257
+ }
2258
+ /**
2259
+ * Transition a swap to {@code swap.expired} (terminal for all swap types)
2260
+ * after Boltz has consistently reported it unknown for
2261
+ * {@link SwapManager.NOT_FOUND_THRESHOLD} consecutive polls. The swap is
2262
+ * persisted, removed from monitoring, and reported via `onSwapFailed`.
2263
+ * Bypasses {@link handleSwapStatusUpdate} on purpose: we don't want to
2264
+ * trigger autonomous claim/refund actions against a Boltz instance that
2265
+ * has no record of this swap — the requests would just generate more
2266
+ * 404s without recovering anything.
2267
+ */
2268
+ async markSwapAsUnknownToProvider(swap) {
2269
+ if (!this.monitoredSwaps.has(swap.id)) {
2270
+ this.notFoundCounts.delete(swap.id);
2271
+ return;
2272
+ }
2273
+ const oldStatus = swap.status;
2274
+ swap.status = "swap.expired";
2275
+ this.monitoredSwaps.delete(swap.id);
2276
+ const retryTimer = this.pollRetryTimers.get(swap.id);
2277
+ if (retryTimer) {
2278
+ clearTimeout(retryTimer);
2279
+ this.pollRetryTimers.delete(swap.id);
2280
+ }
2281
+ this.notFoundCounts.delete(swap.id);
2282
+ this.swapUpdateListeners.forEach(
2283
+ (listener) => listener(swap, oldStatus)
2284
+ );
2285
+ const subscribers = this.swapSubscriptions.get(swap.id);
2286
+ if (subscribers) {
2287
+ subscribers.forEach((callback) => {
2131
2288
  try {
2132
- const statusResponse = await this.swapProvider.getSwapStatus(swap.id);
2133
- if (statusResponse.status !== swap.status) {
2134
- await this.handleSwapStatusUpdate(
2135
- swap,
2136
- statusResponse.status
2137
- );
2138
- }
2139
- } catch (error) {
2140
- if (error instanceof NetworkError && error.statusCode === 429) {
2141
- logger.warn(
2142
- `Rate-limited polling swap ${swap.id}, retrying in 2s`
2143
- );
2144
- const existing = this.pollRetryTimers.get(swap.id);
2145
- if (existing) clearTimeout(existing);
2146
- this.pollRetryTimers.set(
2147
- swap.id,
2148
- setTimeout(async () => {
2149
- this.pollRetryTimers.delete(swap.id);
2150
- try {
2151
- const retry = await this.swapProvider.getSwapStatus(
2152
- swap.id
2153
- );
2154
- if (retry.status !== swap.status) {
2155
- await this.handleSwapStatusUpdate(
2156
- swap,
2157
- retry.status
2158
- );
2159
- }
2160
- } catch (retryError) {
2161
- logger.error(
2162
- `Retry poll for swap ${swap.id} also failed:`,
2163
- retryError
2164
- );
2165
- }
2166
- }, 2e3)
2167
- );
2168
- } else {
2169
- logger.error(`Failed to poll swap ${swap.id}:`, error);
2170
- }
2289
+ callback(swap, oldStatus);
2290
+ } catch (subscriberError) {
2291
+ logger.error(
2292
+ `Error in swap subscription callback for ${swap.id}:`,
2293
+ subscriberError
2294
+ );
2171
2295
  }
2172
- }
2296
+ });
2297
+ }
2298
+ try {
2299
+ await this.saveSwap(swap);
2300
+ } catch (saveError) {
2301
+ logger.error(
2302
+ `Failed to persist unknown-to-provider state for swap ${swap.id}:`,
2303
+ saveError
2304
+ );
2305
+ }
2306
+ logger.warn(
2307
+ `Swap ${swap.id}: marked failed after ${_SwapManager.NOT_FOUND_THRESHOLD} consecutive Boltz 404s \u2014 swap is unknown to the configured Boltz instance`
2173
2308
  );
2174
- await Promise.allSettled(pollPromises);
2309
+ const error = new SwapNotFoundError(swap.id);
2310
+ this.swapFailedListeners.forEach((listener) => listener(swap, error));
2311
+ this.swapSubscriptions.delete(swap.id);
2175
2312
  }
2176
2313
  /**
2177
2314
  * Check if a status is final (no more updates expected)
@@ -1,5 +1,5 @@
1
- import { I as IArkadeSwaps, A as ArkadeSwaps, V as VhtlcTimeouts } from '../arkade-swaps-CZF9XoFR.cjs';
2
- import { A as ArkadeSwapsConfig, m as SwapRepository, p as BoltzSwapProvider, N as Network, 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-LCXS1AVA.cjs';
1
+ import { I as IArkadeSwaps, A as ArkadeSwaps, V as VhtlcTimeouts } from '../arkade-swaps-CS8FZSVL.cjs';
2
+ import { A as ArkadeSwapsConfig, m as SwapRepository, p as BoltzSwapProvider, N as Network, 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-BBI7-KJ0.cjs';
3
3
  import { Identity, ArkProvider, IndexerProvider, IWallet, ArkInfo, ArkTxInput, VHTLC } from '@arkade-os/sdk';
4
4
  import { AsyncStorageTaskQueue, TaskProcessor } from '@arkade-os/sdk/worker/expo';
5
5
  import { TransactionOutput } from '@scure/btc-signer/psbt.js';