@routstr/sdk 0.2.6 → 0.2.8

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,6 @@
1
1
  'use strict';
2
2
 
3
+ var cashuTs = require('@cashu/cashu-ts');
3
4
  var vanilla = require('zustand/vanilla');
4
5
  var stream = require('stream');
5
6
 
@@ -101,8 +102,6 @@ function selectMintWithBalance(balances, units, amount, excludeMints = []) {
101
102
  }
102
103
  return { selectedMintUrl: null, selectedMintBalance: 0 };
103
104
  }
104
-
105
- // wallet/CashuSpender.ts
106
105
  var CashuSpender = class {
107
106
  constructor(walletAdapter, storageAdapter, _providerRegistry, balanceManager) {
108
107
  this.walletAdapter = walletAdapter;
@@ -113,23 +112,43 @@ var CashuSpender = class {
113
112
  _isBusy = false;
114
113
  debugLevel = "WARN";
115
114
  async receiveToken(token) {
116
- const result = await this.walletAdapter.receiveToken(token);
117
- if (!result.success && result.message?.includes("Failed to fetch mint")) {
118
- const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
119
- const existingIndex = cachedTokens.findIndex((t) => t.token === token);
120
- if (existingIndex === -1) {
121
- this.storageAdapter.setCachedReceiveTokens([
122
- ...cachedTokens,
123
- {
124
- token,
125
- amount: result.amount,
126
- unit: result.unit,
127
- createdAt: Date.now()
128
- }
129
- ]);
115
+ try {
116
+ const result = await this.walletAdapter.receiveToken(token);
117
+ return result;
118
+ } catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : String(error);
120
+ if (errorMessage.includes("Failed to fetch mint")) {
121
+ const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
122
+ const existingIndex = cachedTokens.findIndex((t) => t.token === token);
123
+ if (existingIndex === -1) {
124
+ const { amount: amount2, unit: unit2 } = this._decodeTokenAmount(token);
125
+ this.storageAdapter.setCachedReceiveTokens([
126
+ ...cachedTokens,
127
+ {
128
+ token,
129
+ amount: amount2,
130
+ unit: unit2,
131
+ createdAt: Date.now()
132
+ }
133
+ ]);
134
+ }
130
135
  }
136
+ const { amount, unit } = this._decodeTokenAmount(token);
137
+ return { success: false, amount, unit, message: errorMessage };
138
+ }
139
+ }
140
+ _decodeTokenAmount(token) {
141
+ try {
142
+ const decoded = cashuTs.getDecodedToken(token);
143
+ const amount = decoded.proofs.reduce(
144
+ (acc, proof) => acc + proof.amount,
145
+ 0
146
+ );
147
+ const unit = decoded.unit || "sat";
148
+ return { amount, unit };
149
+ } catch {
150
+ return { amount: 0, unit: "sat" };
131
151
  }
132
- return result;
133
152
  }
134
153
  async _getBalanceState() {
135
154
  if (this.balanceManager) {
@@ -449,8 +468,9 @@ var CashuSpender = class {
449
468
  return null;
450
469
  }
451
470
  /**
452
- * Refund all xcashu tokens from storage and increment tryCounts on failure.
453
- * Reuses receiveToken from BalanceManager/CashuSpender for receiving refunds.
471
+ * Refund all xcashu tokens from storage by calling the provider's refund endpoint.
472
+ * The xcashu token acts as an API key to claim the refund, and the response contains
473
+ * the actual refunded Cashu token which is then received into the wallet.
454
474
  * @param mintUrl - The mint URL for receiving tokens
455
475
  * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
456
476
  * @returns Results for each xcashu token refund attempt
@@ -463,7 +483,20 @@ var CashuSpender = class {
463
483
  if (excludedUrls.has(baseUrl)) continue;
464
484
  for (const xcashuToken of tokens) {
465
485
  try {
466
- const receiveResult = await this.receiveToken(xcashuToken.token);
486
+ if (!this.balanceManager) {
487
+ throw new Error("BalanceManager not available for xcashu refund");
488
+ }
489
+ const fetchResult = await this.balanceManager.fetchRefundToken(
490
+ baseUrl,
491
+ xcashuToken.token,
492
+ true
493
+ );
494
+ if (!fetchResult.success || !fetchResult.token) {
495
+ throw new Error(
496
+ fetchResult.error || "Failed to fetch refund token from provider"
497
+ );
498
+ }
499
+ const receiveResult = await this.receiveToken(fetchResult.token);
467
500
  if (receiveResult.success) {
468
501
  this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
469
502
  results.push({
@@ -478,7 +511,10 @@ var CashuSpender = class {
478
511
  } else {
479
512
  const currentTryCount = xcashuToken.tryCount ?? 0;
480
513
  const newTryCount = currentTryCount + 1;
481
- this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
514
+ this.storageAdapter.updateXcashuTokenTryCount(
515
+ xcashuToken.token,
516
+ newTryCount
517
+ );
482
518
  results.push({
483
519
  baseUrl,
484
520
  token: xcashuToken.token,
@@ -487,13 +523,16 @@ var CashuSpender = class {
487
523
  });
488
524
  this._log(
489
525
  "DEBUG",
490
- `[CashuSpender] refundXcashuTokens: Failed to refund xcashu token for ${baseUrl}, incremented tryCount to ${newTryCount}`
526
+ `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}: ${receiveResult.message}`
491
527
  );
492
528
  }
493
529
  } catch (error) {
494
530
  const currentTryCount = xcashuToken.tryCount ?? 0;
495
531
  const newTryCount = currentTryCount + 1;
496
- this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
532
+ this.storageAdapter.updateXcashuTokenTryCount(
533
+ xcashuToken.token,
534
+ newTryCount
535
+ );
497
536
  const errorMessage = error instanceof Error ? error.message : String(error);
498
537
  results.push({
499
538
  baseUrl,
@@ -530,7 +569,10 @@ var CashuSpender = class {
530
569
  if (refundResult.success) {
531
570
  this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
532
571
  } else {
533
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, apiKeyEntry.amount);
572
+ this.storageAdapter.updateApiKeyBalance(
573
+ apiKeyEntry.baseUrl,
574
+ apiKeyEntry.amount
575
+ );
534
576
  }
535
577
  results.push({
536
578
  baseUrl: apiKeyEntry.baseUrl,
@@ -668,7 +710,7 @@ var BalanceManager = class {
668
710
  }
669
711
  let fetchResult;
670
712
  try {
671
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
713
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
672
714
  if (!fetchResult.success) {
673
715
  return {
674
716
  success: false,
@@ -696,6 +738,7 @@ var BalanceManager = class {
696
738
  return {
697
739
  success: receiveResult.success,
698
740
  refundedAmount: totalAmountMsat,
741
+ message: receiveResult.message,
699
742
  requestId: fetchResult.requestId
700
743
  };
701
744
  } catch (error) {
@@ -704,9 +747,9 @@ var BalanceManager = class {
704
747
  }
705
748
  }
706
749
  /**
707
- * Fetch refund token from provider API using API key authentication
750
+ * Fetch refund token from provider API using API key (or xcashu token) authentication
708
751
  */
709
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
752
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
710
753
  if (!baseUrl) {
711
754
  return {
712
755
  success: false,
@@ -720,12 +763,17 @@ var BalanceManager = class {
720
763
  controller.abort();
721
764
  }, 6e4);
722
765
  try {
766
+ const headers = {
767
+ "Content-Type": "application/json"
768
+ };
769
+ if (xCashu) {
770
+ headers["X-Cashu"] = apiKeyOrToken;
771
+ } else {
772
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
773
+ }
723
774
  const response = await fetch(url, {
724
775
  method: "POST",
725
- headers: {
726
- Authorization: `Bearer ${apiKey}`,
727
- "Content-Type": "application/json"
728
- },
776
+ headers,
729
777
  signal: controller.signal
730
778
  });
731
779
  clearTimeout(timeoutId);
@@ -746,10 +794,7 @@ var BalanceManager = class {
746
794
  };
747
795
  } catch (error) {
748
796
  clearTimeout(timeoutId);
749
- console.error(
750
- "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
751
- error
752
- );
797
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
753
798
  if (error instanceof Error) {
754
799
  if (error.name === "AbortError") {
755
800
  return {
@@ -796,11 +841,7 @@ var BalanceManager = class {
796
841
  };
797
842
  }
798
843
  cashuToken = tokenResult.token;
799
- const topUpResult = await this._postTopUp(
800
- baseUrl,
801
- apiKey,
802
- cashuToken
803
- );
844
+ const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
804
845
  requestId = topUpResult.requestId;
805
846
  console.log(topUpResult);
806
847
  if (!topUpResult.success) {
@@ -1166,7 +1207,7 @@ var BalanceManager = class {
1166
1207
  console.log(response.status);
1167
1208
  const data = await response.json();
1168
1209
  console.log("FAILED ", data);
1169
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1210
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1170
1211
  return {
1171
1212
  amount: -1,
1172
1213
  reserved: data.reserved ?? 0,
@@ -1608,8 +1649,13 @@ function isInsecureHttpUrl(url) {
1608
1649
  return url.startsWith("http://");
1609
1650
  }
1610
1651
  var ProviderManager = class _ProviderManager {
1611
- constructor(providerRegistry) {
1652
+ constructor(providerRegistry, store) {
1612
1653
  this.providerRegistry = providerRegistry;
1654
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1655
+ if (store) {
1656
+ this.store = store;
1657
+ this.hydrateFromStore();
1658
+ }
1613
1659
  }
1614
1660
  failedProviders = /* @__PURE__ */ new Set();
1615
1661
  /** Track when each provider last failed (provider URL -> timestamp) */
@@ -1618,14 +1664,57 @@ var ProviderManager = class _ProviderManager {
1618
1664
  providersOnCoolDown = [];
1619
1665
  /** Cooldown duration in milliseconds (5 minutes) */
1620
1666
  static COOLDOWN_DURATION_MS = 5 * 60 * 1e3;
1667
+ /** Optional persistent store for failure tracking */
1668
+ store = null;
1669
+ /** Instance ID for debugging */
1670
+ instanceId;
1671
+ /**
1672
+ * Hydrate in-memory state from persistent store
1673
+ */
1674
+ hydrateFromStore() {
1675
+ if (!this.store) return;
1676
+ const state = this.store.getState();
1677
+ this.failedProviders = new Set(state.failedProviders);
1678
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
1679
+ const now = Date.now();
1680
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
1681
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1682
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
1683
+ console.log(`[ProviderManager:${this.instanceId}] Hydrated from store:`);
1684
+ console.log(` failedProviders: ${this.failedProviders.size}`);
1685
+ console.log(` lastFailed: ${this.lastFailed.size}`);
1686
+ console.log(` providersOnCooldown: ${this.providersOnCoolDown.length}`);
1687
+ }
1688
+ /**
1689
+ * Get instance ID for debugging
1690
+ */
1691
+ getInstanceId() {
1692
+ return this.instanceId;
1693
+ }
1621
1694
  /**
1622
1695
  * Clean up expired cooldown entries
1623
1696
  */
1624
1697
  cleanupExpiredCooldowns() {
1625
1698
  const now = Date.now();
1699
+ const before = this.providersOnCoolDown.length;
1626
1700
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
1627
- ([, timestamp]) => now - timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1701
+ ([url, timestamp]) => {
1702
+ const age = now - timestamp;
1703
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1704
+ if (isExpired) {
1705
+ console.log(
1706
+ `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
1707
+ );
1708
+ }
1709
+ return !isExpired;
1710
+ }
1628
1711
  );
1712
+ const after = this.providersOnCoolDown.length;
1713
+ if (before !== after) {
1714
+ console.log(
1715
+ `[cleanupExpiredCooldowns:${this.instanceId}] Cleaned up ${before - after} expired cooldown(s), ${after} remaining`
1716
+ );
1717
+ }
1629
1718
  }
1630
1719
  /**
1631
1720
  * Get the cooldown duration in milliseconds
@@ -1638,7 +1727,8 @@ var ProviderManager = class _ProviderManager {
1638
1727
  */
1639
1728
  isOnCooldown(baseUrl) {
1640
1729
  this.cleanupExpiredCooldowns();
1641
- return this.providersOnCoolDown.some(([url]) => url === baseUrl);
1730
+ const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1731
+ return result;
1642
1732
  }
1643
1733
  /**
1644
1734
  * Get all providers currently on cooldown
@@ -1652,6 +1742,9 @@ var ProviderManager = class _ProviderManager {
1652
1742
  */
1653
1743
  resetFailedProviders() {
1654
1744
  this.failedProviders.clear();
1745
+ if (this.store) {
1746
+ this.store.getState().setFailedProviders([]);
1747
+ }
1655
1748
  }
1656
1749
  /**
1657
1750
  * Get the last failed timestamp for a provider
@@ -1672,13 +1765,62 @@ var ProviderManager = class _ProviderManager {
1672
1765
  markFailed(baseUrl) {
1673
1766
  const now = Date.now();
1674
1767
  const lastFailure = this.lastFailed.get(baseUrl);
1768
+ console.log(`[markFailed:${this.instanceId}] baseUrl: ${baseUrl}`);
1769
+ console.log(
1770
+ `[markFailed:${this.instanceId}] lastFailure from map: ${lastFailure}`
1771
+ );
1772
+ console.log(
1773
+ `[markFailed:${this.instanceId}] current timestamp (now): ${now}`
1774
+ );
1775
+ console.log(
1776
+ `[markFailed:${this.instanceId}] COOLDOWN_DURATION_MS: ${_ProviderManager.COOLDOWN_DURATION_MS}`
1777
+ );
1778
+ if (lastFailure !== void 0) {
1779
+ const timeSinceLastFailure = now - lastFailure;
1780
+ console.log(
1781
+ `[markFailed:${this.instanceId}] timeSinceLastFailure: ${timeSinceLastFailure}ms`
1782
+ );
1783
+ console.log(
1784
+ `[markFailed:${this.instanceId}] isWithinCooldownWindow: ${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`
1785
+ );
1786
+ }
1675
1787
  this.lastFailed.set(baseUrl, now);
1676
1788
  this.failedProviders.add(baseUrl);
1789
+ if (this.store) {
1790
+ this.store.getState().setLastFailedTimestamp(baseUrl, now);
1791
+ this.store.getState().addFailedProvider(baseUrl);
1792
+ }
1793
+ console.log(
1794
+ `[markFailed:${this.instanceId}] Updated lastFailed map for ${baseUrl} to ${now}`
1795
+ );
1796
+ console.log(
1797
+ `[markFailed:${this.instanceId}] failedProviders set size: ${this.failedProviders.size}`
1798
+ );
1677
1799
  if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1800
+ console.log(
1801
+ `[markFailed:${this.instanceId}] Second failure detected within cooldown window for ${baseUrl}`
1802
+ );
1678
1803
  if (!this.isOnCooldown(baseUrl)) {
1679
1804
  this.providersOnCoolDown.push([baseUrl, now]);
1805
+ if (this.store) {
1806
+ this.store.getState().addProviderOnCooldown(baseUrl, now);
1807
+ }
1808
+ console.log(
1809
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
1810
+ );
1811
+ } else {
1680
1812
  console.log(
1681
- `Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
1813
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} is already on cooldown`
1814
+ );
1815
+ }
1816
+ } else {
1817
+ if (lastFailure === void 0) {
1818
+ console.log(
1819
+ `[markFailed:${this.instanceId}] First failure for ${baseUrl} - not adding to cooldown yet`
1820
+ );
1821
+ } else {
1822
+ console.log(
1823
+ `[markFailed:${this.instanceId}] Failure outside cooldown window for ${baseUrl} (timeSinceLastFailure: ${now - lastFailure}ms)`
1682
1824
  );
1683
1825
  }
1684
1826
  }
@@ -1690,18 +1832,27 @@ var ProviderManager = class _ProviderManager {
1690
1832
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
1691
1833
  ([url]) => url !== baseUrl
1692
1834
  );
1835
+ if (this.store) {
1836
+ this.store.getState().removeProviderFromCooldown(baseUrl);
1837
+ }
1693
1838
  }
1694
1839
  /**
1695
1840
  * Clear all cooldown tracking
1696
1841
  */
1697
1842
  clearCooldowns() {
1698
1843
  this.providersOnCoolDown = [];
1844
+ if (this.store) {
1845
+ this.store.getState().clearProvidersOnCooldown();
1846
+ }
1699
1847
  }
1700
1848
  /**
1701
1849
  * Clear all failure tracking (lastFailed timestamps)
1702
1850
  */
1703
1851
  clearFailureHistory() {
1704
1852
  this.lastFailed.clear();
1853
+ if (this.store) {
1854
+ this.store.getState().setLastFailed({});
1855
+ }
1705
1856
  }
1706
1857
  /**
1707
1858
  * Check if a provider has failed
@@ -2123,7 +2274,10 @@ var SDK_STORAGE_KEYS = {
2123
2274
  LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
2124
2275
  CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
2125
2276
  USAGE_TRACKING: "usage_tracking",
2126
- CLIENT_IDS: "client_ids"
2277
+ CLIENT_IDS: "client_ids",
2278
+ FAILED_PROVIDERS: "failed_providers",
2279
+ LAST_FAILED: "last_failed",
2280
+ PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
2127
2281
  };
2128
2282
 
2129
2283
  // storage/usageTracking/indexedDB.ts
@@ -2770,6 +2924,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2770
2924
  lastRoutstr21ModelsUpdate: null,
2771
2925
  cachedReceiveTokens: [],
2772
2926
  clientIds: [],
2927
+ failedProviders: [],
2928
+ lastFailed: {},
2929
+ providersOnCooldown: [],
2773
2930
  setModelsFromAllProviders: (value) => {
2774
2931
  const normalized = {};
2775
2932
  for (const [baseUrl, models] of Object.entries(value)) {
@@ -2909,6 +3066,71 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2909
3066
  void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
2910
3067
  return { clientIds: normalized };
2911
3068
  });
3069
+ },
3070
+ // ========== Failure Tracking ==========
3071
+ setFailedProviders: (value) => {
3072
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
3073
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3074
+ set({ failedProviders: normalized });
3075
+ },
3076
+ addFailedProvider: (baseUrl) => {
3077
+ const normalized = normalizeBaseUrl5(baseUrl);
3078
+ const current = get().failedProviders;
3079
+ if (!current.includes(normalized)) {
3080
+ const updated = [...current, normalized];
3081
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3082
+ set({ failedProviders: updated });
3083
+ }
3084
+ },
3085
+ removeFailedProvider: (baseUrl) => {
3086
+ const normalized = normalizeBaseUrl5(baseUrl);
3087
+ const current = get().failedProviders;
3088
+ const updated = current.filter((url) => url !== normalized);
3089
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3090
+ set({ failedProviders: updated });
3091
+ },
3092
+ setLastFailed: (value) => {
3093
+ const normalized = {};
3094
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
3095
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3096
+ }
3097
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3098
+ set({ lastFailed: normalized });
3099
+ },
3100
+ setLastFailedTimestamp: (baseUrl, timestamp) => {
3101
+ const normalized = normalizeBaseUrl5(baseUrl);
3102
+ const current = get().lastFailed;
3103
+ const updated = { ...current, [normalized]: timestamp };
3104
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
3105
+ set({ lastFailed: updated });
3106
+ },
3107
+ setProvidersOnCooldown: (value) => {
3108
+ const normalized = value.map((entry) => ({
3109
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3110
+ timestamp: entry.timestamp
3111
+ }));
3112
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3113
+ set({ providersOnCooldown: normalized });
3114
+ },
3115
+ addProviderOnCooldown: (baseUrl, timestamp) => {
3116
+ const normalized = normalizeBaseUrl5(baseUrl);
3117
+ const current = get().providersOnCooldown;
3118
+ if (!current.some((entry) => entry.baseUrl === normalized)) {
3119
+ const updated = [...current, { baseUrl: normalized, timestamp }];
3120
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3121
+ set({ providersOnCooldown: updated });
3122
+ }
3123
+ },
3124
+ removeProviderFromCooldown: (baseUrl) => {
3125
+ const normalized = normalizeBaseUrl5(baseUrl);
3126
+ const current = get().providersOnCooldown;
3127
+ const updated = current.filter((entry) => entry.baseUrl !== normalized);
3128
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3129
+ set({ providersOnCooldown: updated });
3130
+ },
3131
+ clearProvidersOnCooldown: () => {
3132
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, []);
3133
+ set({ providersOnCooldown: [] });
2912
3134
  }
2913
3135
  }));
2914
3136
  var hydrateStoreFromDriver = async (store, driver) => {
@@ -2927,7 +3149,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
2927
3149
  rawRoutstr21Models,
2928
3150
  rawLastRoutstr21ModelsUpdate,
2929
3151
  rawCachedReceiveTokens,
2930
- rawClientIds
3152
+ rawClientIds,
3153
+ rawFailedProviders,
3154
+ rawLastFailed,
3155
+ rawProvidersOnCooldown
2931
3156
  ] = await Promise.all([
2932
3157
  driver.getItem(
2933
3158
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -2958,7 +3183,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
2958
3183
  null
2959
3184
  ),
2960
3185
  driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
2961
- driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
3186
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
3187
+ driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
3188
+ driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
3189
+ driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
2962
3190
  ]);
2963
3191
  const modelsFromAllProviders = Object.fromEntries(
2964
3192
  Object.entries(rawModels).map(([baseUrl, models]) => [
@@ -3026,6 +3254,17 @@ var hydrateStoreFromDriver = async (store, driver) => {
3026
3254
  createdAt: entry.createdAt ?? Date.now(),
3027
3255
  lastUsed: entry.lastUsed ?? null
3028
3256
  }));
3257
+ const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
3258
+ const lastFailed = Object.fromEntries(
3259
+ Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
3260
+ normalizeBaseUrl5(baseUrl),
3261
+ timestamp
3262
+ ])
3263
+ );
3264
+ const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
3265
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3266
+ timestamp: entry.timestamp
3267
+ }));
3029
3268
  store.setState({
3030
3269
  modelsFromAllProviders,
3031
3270
  lastUsedModel,
@@ -3041,7 +3280,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
3041
3280
  routstr21Models,
3042
3281
  lastRoutstr21ModelsUpdate,
3043
3282
  cachedReceiveTokens,
3044
- clientIds
3283
+ clientIds,
3284
+ failedProviders,
3285
+ lastFailed,
3286
+ providersOnCooldown
3045
3287
  });
3046
3288
  };
3047
3289
  var createSdkStore = ({
@@ -3207,11 +3449,11 @@ var RoutstrClient = class {
3207
3449
  this.balanceManager
3208
3450
  );
3209
3451
  this.streamProcessor = new StreamProcessor();
3210
- this.providerManager = new ProviderManager(providerRegistry);
3211
3452
  this.alertLevel = alertLevel;
3212
3453
  this.mode = mode;
3213
3454
  this.usageTrackingDriver = options.usageTrackingDriver;
3214
3455
  this.sdkStore = options.sdkStore;
3456
+ this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore);
3215
3457
  }
3216
3458
  cashuSpender;
3217
3459
  balanceManager;
@@ -3674,19 +3916,19 @@ var RoutstrClient = class {
3674
3916
  `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
3675
3917
  );
3676
3918
  if (params.token.startsWith("cashu")) {
3677
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
3919
+ const receiveResult = await this.cashuSpender.receiveToken(
3678
3920
  params.token
3679
3921
  );
3680
- if (tryReceiveTokenResult.success) {
3922
+ if (receiveResult.success) {
3681
3923
  this._log(
3682
3924
  "DEBUG",
3683
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
3925
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
3684
3926
  );
3685
3927
  tryNextProvider = true;
3686
3928
  } else {
3687
3929
  this._log(
3688
3930
  "DEBUG",
3689
- `[RoutstrClient] _handleErrorResponse: Failed to receive token. `
3931
+ `[RoutstrClient] _handleErrorResponse: Failed to receive token: ${receiveResult.message}`
3690
3932
  );
3691
3933
  }
3692
3934
  }
@@ -3696,23 +3938,18 @@ var RoutstrClient = class {
3696
3938
  "DEBUG",
3697
3939
  `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
3698
3940
  );
3699
- try {
3700
- const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
3701
- if (receiveResult.success) {
3702
- this._log(
3703
- "DEBUG",
3704
- `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
3705
- );
3706
- tryNextProvider = true;
3707
- } else
3708
- throw new ProviderError(
3709
- baseUrl,
3710
- status,
3711
- "xcashu refund failed",
3712
- requestId
3713
- );
3714
- } catch (error) {
3715
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
3941
+ const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
3942
+ if (receiveResult.success) {
3943
+ this._log(
3944
+ "DEBUG",
3945
+ `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
3946
+ );
3947
+ tryNextProvider = true;
3948
+ } else {
3949
+ this._log(
3950
+ "ERROR",
3951
+ `[xcashu] Failed to receive refund token: ${receiveResult.message}`
3952
+ );
3716
3953
  throw new ProviderError(
3717
3954
  baseUrl,
3718
3955
  status,
@@ -3741,6 +3978,10 @@ var RoutstrClient = class {
3741
3978
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
3742
3979
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
3743
3980
  topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
3981
+ this._log(
3982
+ "DEBUG",
3983
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
3984
+ );
3744
3985
  } catch (e) {
3745
3986
  this._log(
3746
3987
  "WARN",
@@ -3870,6 +4111,20 @@ var RoutstrClient = class {
3870
4111
  tryNextProvider = true;
3871
4112
  }
3872
4113
  }
4114
+ if (status === 401 && this.mode === "apikeys") {
4115
+ this._log(
4116
+ "DEBUG",
4117
+ `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
4118
+ );
4119
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
4120
+ token,
4121
+ baseUrl
4122
+ );
4123
+ if (latestBalanceInfo.isInvalidApiKey) {
4124
+ this.storageAdapter.removeApiKey(baseUrl);
4125
+ tryNextProvider = true;
4126
+ }
4127
+ }
3873
4128
  if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
3874
4129
  this._log(
3875
4130
  "DEBUG",
@@ -3880,13 +4135,13 @@ var RoutstrClient = class {
3880
4135
  "DEBUG",
3881
4136
  `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
3882
4137
  );
3883
- const initialBalance = await this.balanceManager.getTokenBalance(
4138
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3884
4139
  token,
3885
4140
  baseUrl
3886
4141
  );
3887
4142
  this._log(
3888
4143
  "DEBUG",
3889
- `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
4144
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
3890
4145
  );
3891
4146
  const refundResult = await this.balanceManager.refundApiKey({
3892
4147
  mintUrl,
@@ -3898,7 +4153,7 @@ var RoutstrClient = class {
3898
4153
  "DEBUG",
3899
4154
  `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
3900
4155
  );
3901
- if (!refundResult.success && initialBalance.amount > 0) {
4156
+ if (!refundResult.success && latestBalanceInfo.amount > 0) {
3902
4157
  throw new ProviderError(
3903
4158
  baseUrl,
3904
4159
  status,
@@ -3987,14 +4242,15 @@ var RoutstrClient = class {
3987
4242
  if (this.mode === "xcashu" && response) {
3988
4243
  const refundToken = response.headers.get("x-cashu") ?? void 0;
3989
4244
  if (refundToken) {
3990
- try {
3991
- const receiveResult = await this.cashuSpender.receiveToken(refundToken);
3992
- if (receiveResult.success) {
3993
- this.storageAdapter.removeXcashuToken(baseUrl, token);
3994
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
3995
- }
3996
- } catch (error) {
3997
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
4245
+ const receiveResult = await this.cashuSpender.receiveToken(refundToken);
4246
+ if (receiveResult.success) {
4247
+ this.storageAdapter.removeXcashuToken(baseUrl, token);
4248
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
4249
+ } else {
4250
+ this._log(
4251
+ "ERROR",
4252
+ `[xcashu] Failed to receive refund token: ${receiveResult.message}`
4253
+ );
3998
4254
  }
3999
4255
  }
4000
4256
  } else if (this.mode === "apikeys") {
@@ -4038,7 +4294,6 @@ var RoutstrClient = class {
4038
4294
  try {
4039
4295
  const xcashuResults = await this.cashuSpender.refundXcashuTokens(mintUrl);
4040
4296
  this._log("DEBUG", "Refund xcashu tokens results:", xcashuResults);
4041
- const results = await this.cashuSpender.refundProviders(mintUrl);
4042
4297
  } catch (error) {
4043
4298
  this._log("ERROR", "Failed to refund providers:", error);
4044
4299
  }
@@ -4247,18 +4502,18 @@ var RoutstrClient = class {
4247
4502
  this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
4248
4503
  } catch (error) {
4249
4504
  if (error instanceof Error && error.message.includes("ApiKey already exists")) {
4250
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
4505
+ const receiveResult = await this.cashuSpender.receiveToken(
4251
4506
  spendResult2.token
4252
4507
  );
4253
- if (tryReceiveTokenResult.success) {
4508
+ if (receiveResult.success) {
4254
4509
  this._log(
4255
4510
  "DEBUG",
4256
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
4511
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4257
4512
  );
4258
4513
  } else {
4259
4514
  this._log(
4260
4515
  "DEBUG",
4261
- `[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
4516
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
4262
4517
  );
4263
4518
  }
4264
4519
  this._log(