@routstr/sdk 0.2.1 → 0.2.3

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.
@@ -24,19 +24,66 @@ interface ModelProviderPrice {
24
24
  declare class ProviderManager {
25
25
  private providerRegistry;
26
26
  private failedProviders;
27
+ /** Track when each provider last failed (provider URL -> timestamp) */
28
+ private lastFailed;
29
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
30
+ private providersOnCoolDown;
31
+ /** Cooldown duration in milliseconds (5 minutes) */
32
+ private static readonly COOLDOWN_DURATION_MS;
27
33
  constructor(providerRegistry: ProviderRegistry);
34
+ /**
35
+ * Clean up expired cooldown entries
36
+ */
37
+ private cleanupExpiredCooldowns;
38
+ /**
39
+ * Get the cooldown duration in milliseconds
40
+ */
41
+ getCooldownDurationMs(): number;
42
+ /**
43
+ * Check if a provider is currently on cooldown
44
+ */
45
+ isOnCooldown(baseUrl: string): boolean;
46
+ /**
47
+ * Get all providers currently on cooldown
48
+ */
49
+ getProvidersOnCooldown(): [string, number][];
28
50
  /**
29
51
  * Reset the failed providers list
30
52
  */
31
53
  resetFailedProviders(): void;
54
+ /**
55
+ * Get the last failed timestamp for a provider
56
+ */
57
+ getLastFailed(baseUrl: string): number | undefined;
58
+ /**
59
+ * Get all providers with their last failed timestamps
60
+ */
61
+ getAllLastFailed(): Map<string, number>;
32
62
  /**
33
63
  * Mark a provider as failed
64
+ * If a provider fails twice within 5 minutes, it's added to cooldown
34
65
  */
35
66
  markFailed(baseUrl: string): void;
67
+ /**
68
+ * Remove a provider from cooldown (e.g., after successful request)
69
+ */
70
+ removeFromCooldown(baseUrl: string): void;
71
+ /**
72
+ * Clear all cooldown tracking
73
+ */
74
+ clearCooldowns(): void;
75
+ /**
76
+ * Clear all failure tracking (lastFailed timestamps)
77
+ */
78
+ clearFailureHistory(): void;
36
79
  /**
37
80
  * Check if a provider has failed
38
81
  */
39
82
  hasFailed(baseUrl: string): boolean;
83
+ /**
84
+ * Get a copy of the failed providers set
85
+ */
86
+ getFailedProviders(): Set<string>;
40
87
  /**
41
88
  * Find the next best provider for a model
42
89
  * @param modelId The model ID to find a provider for
@@ -206,10 +253,6 @@ declare class RoutstrClient {
206
253
  * Get pending cashu token amount
207
254
  */
208
255
  private _getPendingCashuTokenAmount;
209
- /**
210
- * Check if error message indicates a network error
211
- */
212
- private _isNetworkError;
213
256
  /**
214
257
  * Handle errors and notify callbacks
215
258
  */
@@ -24,19 +24,66 @@ interface ModelProviderPrice {
24
24
  declare class ProviderManager {
25
25
  private providerRegistry;
26
26
  private failedProviders;
27
+ /** Track when each provider last failed (provider URL -> timestamp) */
28
+ private lastFailed;
29
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
30
+ private providersOnCoolDown;
31
+ /** Cooldown duration in milliseconds (5 minutes) */
32
+ private static readonly COOLDOWN_DURATION_MS;
27
33
  constructor(providerRegistry: ProviderRegistry);
34
+ /**
35
+ * Clean up expired cooldown entries
36
+ */
37
+ private cleanupExpiredCooldowns;
38
+ /**
39
+ * Get the cooldown duration in milliseconds
40
+ */
41
+ getCooldownDurationMs(): number;
42
+ /**
43
+ * Check if a provider is currently on cooldown
44
+ */
45
+ isOnCooldown(baseUrl: string): boolean;
46
+ /**
47
+ * Get all providers currently on cooldown
48
+ */
49
+ getProvidersOnCooldown(): [string, number][];
28
50
  /**
29
51
  * Reset the failed providers list
30
52
  */
31
53
  resetFailedProviders(): void;
54
+ /**
55
+ * Get the last failed timestamp for a provider
56
+ */
57
+ getLastFailed(baseUrl: string): number | undefined;
58
+ /**
59
+ * Get all providers with their last failed timestamps
60
+ */
61
+ getAllLastFailed(): Map<string, number>;
32
62
  /**
33
63
  * Mark a provider as failed
64
+ * If a provider fails twice within 5 minutes, it's added to cooldown
34
65
  */
35
66
  markFailed(baseUrl: string): void;
67
+ /**
68
+ * Remove a provider from cooldown (e.g., after successful request)
69
+ */
70
+ removeFromCooldown(baseUrl: string): void;
71
+ /**
72
+ * Clear all cooldown tracking
73
+ */
74
+ clearCooldowns(): void;
75
+ /**
76
+ * Clear all failure tracking (lastFailed timestamps)
77
+ */
78
+ clearFailureHistory(): void;
36
79
  /**
37
80
  * Check if a provider has failed
38
81
  */
39
82
  hasFailed(baseUrl: string): boolean;
83
+ /**
84
+ * Get a copy of the failed providers set
85
+ */
86
+ getFailedProviders(): Set<string>;
40
87
  /**
41
88
  * Find the next best provider for a model
42
89
  * @param modelId The model ID to find a provider for
@@ -206,10 +253,6 @@ declare class RoutstrClient {
206
253
  * Get pending cashu token amount
207
254
  */
208
255
  private _getPendingCashuTokenAmount;
209
- /**
210
- * Check if error message indicates a network error
211
- */
212
- private _isNetworkError;
213
256
  /**
214
257
  * Handle errors and notify callbacks
215
258
  */
@@ -81,7 +81,7 @@ var auditLogger = AuditLogger.getInstance();
81
81
 
82
82
  // wallet/tokenUtils.ts
83
83
  function isNetworkErrorMessage(message) {
84
- return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
84
+ return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed") || message.includes("ERR_TLS_CERT_ALTNAME_INVALID") || message.includes("ERR_TLS_CERT_NOT_YET_VALID") || message.includes("ERR_TLS_CERT_EXPIRED") || message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE") || message.includes("SELF_SIGNED_CERT_IN_CHAIN");
85
85
  }
86
86
  function getBalanceInSats(balance, unit) {
87
87
  return unit === "msat" ? balance / 1e3 : balance;
@@ -371,7 +371,26 @@ var CashuSpender = class {
371
371
  }
372
372
  }
373
373
  if (token && baseUrl) {
374
- this.storageAdapter.setToken(baseUrl, token);
374
+ try {
375
+ this.storageAdapter.setToken(baseUrl, token);
376
+ } catch (error) {
377
+ if (error instanceof Error && error.message.includes("Token already exists")) {
378
+ this._log(
379
+ "DEBUG",
380
+ `[CashuSpender] _spendInternal: Token already exists for ${baseUrl}, receiving newly created token and using existing`
381
+ );
382
+ const receiveResult = await this.receiveToken(token);
383
+ if (receiveResult.success) {
384
+ this._log(
385
+ "DEBUG",
386
+ `[CashuSpender] _spendInternal: Token restored successfully, amount=${receiveResult.amount}`
387
+ );
388
+ }
389
+ token = this.storageAdapter.getToken(baseUrl);
390
+ } else {
391
+ throw error;
392
+ }
393
+ }
375
394
  }
376
395
  this._logTransaction("spend", {
377
396
  amount: spentAmount,
@@ -849,7 +868,9 @@ var BalanceManager = class {
849
868
  p2pkPubkey
850
869
  } = options;
851
870
  const adjustedAmount = Math.ceil(amount);
871
+ console.log(`[BalanceManager.createProviderToken] Starting: baseUrl=${baseUrl}, mintUrl=${mintUrl}, amount=${amount}, adjustedAmount=${adjustedAmount}, retryCount=${retryCount}`);
852
872
  if (!adjustedAmount || isNaN(adjustedAmount)) {
873
+ console.error(`[BalanceManager.createProviderToken] FAILURE: Invalid amount - amount=${amount}, adjustedAmount=${adjustedAmount}`);
853
874
  return { success: false, error: "Invalid top up amount" };
854
875
  }
855
876
  const balanceState = await this.getBalanceState();
@@ -880,6 +901,7 @@ var BalanceManager = class {
880
901
  { url: "", balance: 0 }
881
902
  ).url
882
903
  );
904
+ console.error(`[BalanceManager.createProviderToken] FAILURE: Insufficient balance - required=${adjustedAmount}, available=${totalMintBalance + targetProviderBalance}, totalMintBalance=${totalMintBalance}, targetProviderBalance=${targetProviderBalance}, refundableProviderBalance=${refundableProviderBalance}`);
883
905
  return { success: false, error: error.message };
884
906
  }
885
907
  if (targetProviderBalance >= adjustedAmount) {
@@ -921,6 +943,7 @@ var BalanceManager = class {
921
943
  maxMintUrl = mintUrl2;
922
944
  }
923
945
  }
946
+ console.error(`[BalanceManager.createProviderToken] FAILURE: No candidate mints found - requiredAmount=${requiredAmount}, totalMintBalance=${totalMintBalance}, maxBalance=${maxBalance}, maxMintUrl=${maxMintUrl}, providerMints=${JSON.stringify(providerMints)}`);
924
947
  const error = new InsufficientBalanceError(
925
948
  adjustedAmount,
926
949
  totalMintBalance,
@@ -932,11 +955,13 @@ var BalanceManager = class {
932
955
  let lastError;
933
956
  for (const candidateMint of candidates) {
934
957
  try {
958
+ console.log(`[BalanceManager.createProviderToken] Attempting mint: ${candidateMint}, amount: ${requiredAmount}`);
935
959
  const token = await this.walletAdapter.sendToken(
936
960
  candidateMint,
937
961
  requiredAmount,
938
962
  p2pkPubkey
939
963
  );
964
+ console.log(`[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`);
940
965
  return {
941
966
  success: true,
942
967
  token,
@@ -944,9 +969,12 @@ var BalanceManager = class {
944
969
  amountSpent: requiredAmount
945
970
  };
946
971
  } catch (error) {
972
+ const errorMsg = error instanceof Error ? error.message : String(error);
973
+ console.error(`[BalanceManager.createProviderToken] FAILURE: Mint ${candidateMint} failed with error: ${errorMsg}`);
947
974
  if (error instanceof Error) {
948
- lastError = error.message;
975
+ lastError = errorMsg;
949
976
  if (isNetworkErrorMessage(error.message)) {
977
+ console.warn(`[BalanceManager.createProviderToken] Network error from ${candidateMint}, trying next mint...`);
950
978
  continue;
951
979
  }
952
980
  }
@@ -956,6 +984,7 @@ var BalanceManager = class {
956
984
  };
957
985
  }
958
986
  }
987
+ console.error(`[BalanceManager.createProviderToken] FAILURE: All candidate mints exhausted - lastError=${lastError}, candidates=${JSON.stringify(candidates)}`);
959
988
  return {
960
989
  success: false,
961
990
  error: lastError || "All candidate mints failed while creating top up token"
@@ -1241,7 +1270,6 @@ var BalanceManager = class {
1241
1270
  });
1242
1271
  if (response.ok) {
1243
1272
  const data = await response.json();
1244
- console.log("TOKENA FASJDFAS", data);
1245
1273
  return {
1246
1274
  amount: data.balance,
1247
1275
  reserved: data.reserved ?? 0,
@@ -1614,22 +1642,101 @@ function calculateImageTokens(width, height, detail = "auto") {
1614
1642
  function isInsecureHttpUrl(url) {
1615
1643
  return url.startsWith("http://");
1616
1644
  }
1617
- var ProviderManager = class {
1645
+ var ProviderManager = class _ProviderManager {
1618
1646
  constructor(providerRegistry) {
1619
1647
  this.providerRegistry = providerRegistry;
1620
1648
  }
1621
1649
  failedProviders = /* @__PURE__ */ new Set();
1650
+ /** Track when each provider last failed (provider URL -> timestamp) */
1651
+ lastFailed = /* @__PURE__ */ new Map();
1652
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
1653
+ providersOnCoolDown = [];
1654
+ /** Cooldown duration in milliseconds (5 minutes) */
1655
+ static COOLDOWN_DURATION_MS = 5 * 60 * 1e3;
1656
+ /**
1657
+ * Clean up expired cooldown entries
1658
+ */
1659
+ cleanupExpiredCooldowns() {
1660
+ const now = Date.now();
1661
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1662
+ ([, timestamp]) => now - timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1663
+ );
1664
+ }
1665
+ /**
1666
+ * Get the cooldown duration in milliseconds
1667
+ */
1668
+ getCooldownDurationMs() {
1669
+ return _ProviderManager.COOLDOWN_DURATION_MS;
1670
+ }
1671
+ /**
1672
+ * Check if a provider is currently on cooldown
1673
+ */
1674
+ isOnCooldown(baseUrl) {
1675
+ this.cleanupExpiredCooldowns();
1676
+ return this.providersOnCoolDown.some(([url]) => url === baseUrl);
1677
+ }
1678
+ /**
1679
+ * Get all providers currently on cooldown
1680
+ */
1681
+ getProvidersOnCooldown() {
1682
+ this.cleanupExpiredCooldowns();
1683
+ return [...this.providersOnCoolDown];
1684
+ }
1622
1685
  /**
1623
1686
  * Reset the failed providers list
1624
1687
  */
1625
1688
  resetFailedProviders() {
1626
1689
  this.failedProviders.clear();
1627
1690
  }
1691
+ /**
1692
+ * Get the last failed timestamp for a provider
1693
+ */
1694
+ getLastFailed(baseUrl) {
1695
+ return this.lastFailed.get(baseUrl);
1696
+ }
1697
+ /**
1698
+ * Get all providers with their last failed timestamps
1699
+ */
1700
+ getAllLastFailed() {
1701
+ return new Map(this.lastFailed);
1702
+ }
1628
1703
  /**
1629
1704
  * Mark a provider as failed
1705
+ * If a provider fails twice within 5 minutes, it's added to cooldown
1630
1706
  */
1631
1707
  markFailed(baseUrl) {
1708
+ const now = Date.now();
1709
+ const lastFailure = this.lastFailed.get(baseUrl);
1710
+ this.lastFailed.set(baseUrl, now);
1632
1711
  this.failedProviders.add(baseUrl);
1712
+ if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1713
+ if (!this.isOnCooldown(baseUrl)) {
1714
+ this.providersOnCoolDown.push([baseUrl, now]);
1715
+ console.log(
1716
+ `Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
1717
+ );
1718
+ }
1719
+ }
1720
+ }
1721
+ /**
1722
+ * Remove a provider from cooldown (e.g., after successful request)
1723
+ */
1724
+ removeFromCooldown(baseUrl) {
1725
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1726
+ ([url]) => url !== baseUrl
1727
+ );
1728
+ }
1729
+ /**
1730
+ * Clear all cooldown tracking
1731
+ */
1732
+ clearCooldowns() {
1733
+ this.providersOnCoolDown = [];
1734
+ }
1735
+ /**
1736
+ * Clear all failure tracking (lastFailed timestamps)
1737
+ */
1738
+ clearFailureHistory() {
1739
+ this.lastFailed.clear();
1633
1740
  }
1634
1741
  /**
1635
1742
  * Check if a provider has failed
@@ -1637,6 +1744,12 @@ var ProviderManager = class {
1637
1744
  hasFailed(baseUrl) {
1638
1745
  return this.failedProviders.has(baseUrl);
1639
1746
  }
1747
+ /**
1748
+ * Get a copy of the failed providers set
1749
+ */
1750
+ getFailedProviders() {
1751
+ return new Set(this.failedProviders);
1752
+ }
1640
1753
  /**
1641
1754
  * Find the next best provider for a model
1642
1755
  * @param modelId The model ID to find a provider for
@@ -1652,7 +1765,7 @@ var ProviderManager = class {
1652
1765
  const allProviders = this.providerRegistry.getAllProvidersModels();
1653
1766
  const candidates = [];
1654
1767
  for (const [baseUrl, models] of Object.entries(allProviders)) {
1655
- if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl)) {
1768
+ if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl) || this.isOnCooldown(baseUrl)) {
1656
1769
  continue;
1657
1770
  }
1658
1771
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
@@ -1699,6 +1812,7 @@ var ProviderManager = class {
1699
1812
  const torMode = isTorContext();
1700
1813
  for (const [baseUrl, models] of Object.entries(allProviders)) {
1701
1814
  if (disabledProviders.has(baseUrl)) continue;
1815
+ if (this.isOnCooldown(baseUrl)) continue;
1702
1816
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
1703
1817
  continue;
1704
1818
  const model = models.find((m) => m.id === modelId);
@@ -1722,6 +1836,7 @@ var ProviderManager = class {
1722
1836
  const results = [];
1723
1837
  for (const [baseUrl, models] of Object.entries(allModels)) {
1724
1838
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
1839
+ if (this.isOnCooldown(baseUrl)) continue;
1725
1840
  if (torMode && !baseUrl.includes(".onion")) continue;
1726
1841
  if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1727
1842
  continue;
@@ -2135,7 +2250,6 @@ var RoutstrClient = class {
2135
2250
  body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
2136
2251
  });
2137
2252
  if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
2138
- this._log("DEBUG", "response,", response);
2139
2253
  response.baseUrl = baseUrl;
2140
2254
  response.token = token;
2141
2255
  if (!response.ok) {
@@ -2158,7 +2272,7 @@ var RoutstrClient = class {
2158
2272
  }
2159
2273
  return response;
2160
2274
  } catch (error) {
2161
- if (this._isNetworkError(error?.message || "")) {
2275
+ if (isNetworkErrorMessage(error?.message || "")) {
2162
2276
  return await this._handleErrorResponse(
2163
2277
  params,
2164
2278
  token,
@@ -2293,7 +2407,13 @@ var RoutstrClient = class {
2293
2407
  "DEBUG",
2294
2408
  `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
2295
2409
  );
2296
- throw new InsufficientBalanceError(required, available, 0, "", message);
2410
+ throw new InsufficientBalanceError(
2411
+ required,
2412
+ available,
2413
+ 0,
2414
+ "",
2415
+ message
2416
+ );
2297
2417
  } else {
2298
2418
  this._log(
2299
2419
  "DEBUG",
@@ -2506,7 +2626,10 @@ var RoutstrClient = class {
2506
2626
  retryCount: 0
2507
2627
  });
2508
2628
  }
2509
- throw new FailoverError(baseUrl, Array.from(this.providerManager));
2629
+ throw new FailoverError(
2630
+ baseUrl,
2631
+ Array.from(this.providerManager.getFailedProviders())
2632
+ );
2510
2633
  }
2511
2634
  /**
2512
2635
  * Handle post-response balance update for all modes
@@ -2656,12 +2779,6 @@ var RoutstrClient = class {
2656
2779
  const distribution = this.storageAdapter.getCachedTokenDistribution();
2657
2780
  return distribution.reduce((total, item) => total + item.amount, 0);
2658
2781
  }
2659
- /**
2660
- * Check if error message indicates a network error
2661
- */
2662
- _isNetworkError(message) {
2663
- return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
2664
- }
2665
2782
  /**
2666
2783
  * Handle errors and notify callbacks
2667
2784
  */