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