@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.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  var applesauceRelay = require('applesauce-relay');
4
4
  var applesauceCore = require('applesauce-core');
5
5
  var rxjs = require('rxjs');
6
+ var cashuTs = require('@cashu/cashu-ts');
6
7
  var vanilla = require('zustand/vanilla');
7
8
  var stream = require('stream');
8
9
 
@@ -468,7 +469,7 @@ var ModelManager = class _ModelManager {
468
469
  }
469
470
  }
470
471
  const DEFAULT_RELAYS = [
471
- "wss://relay.primal.net",
472
+ "wss://relay.damus.io",
472
473
  "wss://nos.lol",
473
474
  "wss://relay.routstr.com"
474
475
  ];
@@ -735,8 +736,6 @@ function selectMintWithBalance(balances, units, amount, excludeMints = []) {
735
736
  }
736
737
  return { selectedMintUrl: null, selectedMintBalance: 0 };
737
738
  }
738
-
739
- // wallet/CashuSpender.ts
740
739
  var CashuSpender = class {
741
740
  constructor(walletAdapter, storageAdapter, _providerRegistry, balanceManager) {
742
741
  this.walletAdapter = walletAdapter;
@@ -747,23 +746,43 @@ var CashuSpender = class {
747
746
  _isBusy = false;
748
747
  debugLevel = "WARN";
749
748
  async receiveToken(token) {
750
- const result = await this.walletAdapter.receiveToken(token);
751
- if (!result.success && result.message?.includes("Failed to fetch mint")) {
752
- const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
753
- const existingIndex = cachedTokens.findIndex((t) => t.token === token);
754
- if (existingIndex === -1) {
755
- this.storageAdapter.setCachedReceiveTokens([
756
- ...cachedTokens,
757
- {
758
- token,
759
- amount: result.amount,
760
- unit: result.unit,
761
- createdAt: Date.now()
762
- }
763
- ]);
749
+ try {
750
+ const result = await this.walletAdapter.receiveToken(token);
751
+ return result;
752
+ } catch (error) {
753
+ const errorMessage = error instanceof Error ? error.message : String(error);
754
+ if (errorMessage.includes("Failed to fetch mint")) {
755
+ const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
756
+ const existingIndex = cachedTokens.findIndex((t) => t.token === token);
757
+ if (existingIndex === -1) {
758
+ const { amount: amount2, unit: unit2 } = this._decodeTokenAmount(token);
759
+ this.storageAdapter.setCachedReceiveTokens([
760
+ ...cachedTokens,
761
+ {
762
+ token,
763
+ amount: amount2,
764
+ unit: unit2,
765
+ createdAt: Date.now()
766
+ }
767
+ ]);
768
+ }
764
769
  }
770
+ const { amount, unit } = this._decodeTokenAmount(token);
771
+ return { success: false, amount, unit, message: errorMessage };
772
+ }
773
+ }
774
+ _decodeTokenAmount(token) {
775
+ try {
776
+ const decoded = cashuTs.getDecodedToken(token);
777
+ const amount = decoded.proofs.reduce(
778
+ (acc, proof) => acc + proof.amount,
779
+ 0
780
+ );
781
+ const unit = decoded.unit || "sat";
782
+ return { amount, unit };
783
+ } catch {
784
+ return { amount: 0, unit: "sat" };
765
785
  }
766
- return result;
767
786
  }
768
787
  async _getBalanceState() {
769
788
  if (this.balanceManager) {
@@ -1083,8 +1102,9 @@ var CashuSpender = class {
1083
1102
  return null;
1084
1103
  }
1085
1104
  /**
1086
- * Refund all xcashu tokens from storage and increment tryCounts on failure.
1087
- * Reuses receiveToken from BalanceManager/CashuSpender for receiving refunds.
1105
+ * Refund all xcashu tokens from storage by calling the provider's refund endpoint.
1106
+ * The xcashu token acts as an API key to claim the refund, and the response contains
1107
+ * the actual refunded Cashu token which is then received into the wallet.
1088
1108
  * @param mintUrl - The mint URL for receiving tokens
1089
1109
  * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
1090
1110
  * @returns Results for each xcashu token refund attempt
@@ -1097,7 +1117,20 @@ var CashuSpender = class {
1097
1117
  if (excludedUrls.has(baseUrl)) continue;
1098
1118
  for (const xcashuToken of tokens) {
1099
1119
  try {
1100
- const receiveResult = await this.receiveToken(xcashuToken.token);
1120
+ if (!this.balanceManager) {
1121
+ throw new Error("BalanceManager not available for xcashu refund");
1122
+ }
1123
+ const fetchResult = await this.balanceManager.fetchRefundToken(
1124
+ baseUrl,
1125
+ xcashuToken.token,
1126
+ true
1127
+ );
1128
+ if (!fetchResult.success || !fetchResult.token) {
1129
+ throw new Error(
1130
+ fetchResult.error || "Failed to fetch refund token from provider"
1131
+ );
1132
+ }
1133
+ const receiveResult = await this.receiveToken(fetchResult.token);
1101
1134
  if (receiveResult.success) {
1102
1135
  this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
1103
1136
  results.push({
@@ -1112,7 +1145,10 @@ var CashuSpender = class {
1112
1145
  } else {
1113
1146
  const currentTryCount = xcashuToken.tryCount ?? 0;
1114
1147
  const newTryCount = currentTryCount + 1;
1115
- this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
1148
+ this.storageAdapter.updateXcashuTokenTryCount(
1149
+ xcashuToken.token,
1150
+ newTryCount
1151
+ );
1116
1152
  results.push({
1117
1153
  baseUrl,
1118
1154
  token: xcashuToken.token,
@@ -1121,13 +1157,16 @@ var CashuSpender = class {
1121
1157
  });
1122
1158
  this._log(
1123
1159
  "DEBUG",
1124
- `[CashuSpender] refundXcashuTokens: Failed to refund xcashu token for ${baseUrl}, incremented tryCount to ${newTryCount}`
1160
+ `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}: ${receiveResult.message}`
1125
1161
  );
1126
1162
  }
1127
1163
  } catch (error) {
1128
1164
  const currentTryCount = xcashuToken.tryCount ?? 0;
1129
1165
  const newTryCount = currentTryCount + 1;
1130
- this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
1166
+ this.storageAdapter.updateXcashuTokenTryCount(
1167
+ xcashuToken.token,
1168
+ newTryCount
1169
+ );
1131
1170
  const errorMessage = error instanceof Error ? error.message : String(error);
1132
1171
  results.push({
1133
1172
  baseUrl,
@@ -1164,7 +1203,10 @@ var CashuSpender = class {
1164
1203
  if (refundResult.success) {
1165
1204
  this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
1166
1205
  } else {
1167
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, apiKeyEntry.amount);
1206
+ this.storageAdapter.updateApiKeyBalance(
1207
+ apiKeyEntry.baseUrl,
1208
+ apiKeyEntry.amount
1209
+ );
1168
1210
  }
1169
1211
  results.push({
1170
1212
  baseUrl: apiKeyEntry.baseUrl,
@@ -1302,7 +1344,7 @@ var BalanceManager = class {
1302
1344
  }
1303
1345
  let fetchResult;
1304
1346
  try {
1305
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
1347
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
1306
1348
  if (!fetchResult.success) {
1307
1349
  return {
1308
1350
  success: false,
@@ -1330,6 +1372,7 @@ var BalanceManager = class {
1330
1372
  return {
1331
1373
  success: receiveResult.success,
1332
1374
  refundedAmount: totalAmountMsat,
1375
+ message: receiveResult.message,
1333
1376
  requestId: fetchResult.requestId
1334
1377
  };
1335
1378
  } catch (error) {
@@ -1338,9 +1381,9 @@ var BalanceManager = class {
1338
1381
  }
1339
1382
  }
1340
1383
  /**
1341
- * Fetch refund token from provider API using API key authentication
1384
+ * Fetch refund token from provider API using API key (or xcashu token) authentication
1342
1385
  */
1343
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
1386
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
1344
1387
  if (!baseUrl) {
1345
1388
  return {
1346
1389
  success: false,
@@ -1354,12 +1397,17 @@ var BalanceManager = class {
1354
1397
  controller.abort();
1355
1398
  }, 6e4);
1356
1399
  try {
1400
+ const headers = {
1401
+ "Content-Type": "application/json"
1402
+ };
1403
+ if (xCashu) {
1404
+ headers["X-Cashu"] = apiKeyOrToken;
1405
+ } else {
1406
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
1407
+ }
1357
1408
  const response = await fetch(url, {
1358
1409
  method: "POST",
1359
- headers: {
1360
- Authorization: `Bearer ${apiKey}`,
1361
- "Content-Type": "application/json"
1362
- },
1410
+ headers,
1363
1411
  signal: controller.signal
1364
1412
  });
1365
1413
  clearTimeout(timeoutId);
@@ -1380,10 +1428,7 @@ var BalanceManager = class {
1380
1428
  };
1381
1429
  } catch (error) {
1382
1430
  clearTimeout(timeoutId);
1383
- console.error(
1384
- "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
1385
- error
1386
- );
1431
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
1387
1432
  if (error instanceof Error) {
1388
1433
  if (error.name === "AbortError") {
1389
1434
  return {
@@ -1430,11 +1475,7 @@ var BalanceManager = class {
1430
1475
  };
1431
1476
  }
1432
1477
  cashuToken = tokenResult.token;
1433
- const topUpResult = await this._postTopUp(
1434
- baseUrl,
1435
- apiKey,
1436
- cashuToken
1437
- );
1478
+ const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
1438
1479
  requestId = topUpResult.requestId;
1439
1480
  console.log(topUpResult);
1440
1481
  if (!topUpResult.success) {
@@ -1800,7 +1841,7 @@ var BalanceManager = class {
1800
1841
  console.log(response.status);
1801
1842
  const data = await response.json();
1802
1843
  console.log("FAILED ", data);
1803
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1844
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1804
1845
  return {
1805
1846
  amount: -1,
1806
1847
  reserved: data.reserved ?? 0,
@@ -2298,8 +2339,13 @@ function isInsecureHttpUrl(url) {
2298
2339
  return url.startsWith("http://");
2299
2340
  }
2300
2341
  var ProviderManager = class _ProviderManager {
2301
- constructor(providerRegistry) {
2342
+ constructor(providerRegistry, store) {
2302
2343
  this.providerRegistry = providerRegistry;
2344
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2345
+ if (store) {
2346
+ this.store = store;
2347
+ this.hydrateFromStore();
2348
+ }
2303
2349
  }
2304
2350
  failedProviders = /* @__PURE__ */ new Set();
2305
2351
  /** Track when each provider last failed (provider URL -> timestamp) */
@@ -2308,14 +2354,57 @@ var ProviderManager = class _ProviderManager {
2308
2354
  providersOnCoolDown = [];
2309
2355
  /** Cooldown duration in milliseconds (5 minutes) */
2310
2356
  static COOLDOWN_DURATION_MS = 5 * 60 * 1e3;
2357
+ /** Optional persistent store for failure tracking */
2358
+ store = null;
2359
+ /** Instance ID for debugging */
2360
+ instanceId;
2361
+ /**
2362
+ * Hydrate in-memory state from persistent store
2363
+ */
2364
+ hydrateFromStore() {
2365
+ if (!this.store) return;
2366
+ const state = this.store.getState();
2367
+ this.failedProviders = new Set(state.failedProviders);
2368
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
2369
+ const now = Date.now();
2370
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
2371
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2372
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
2373
+ console.log(`[ProviderManager:${this.instanceId}] Hydrated from store:`);
2374
+ console.log(` failedProviders: ${this.failedProviders.size}`);
2375
+ console.log(` lastFailed: ${this.lastFailed.size}`);
2376
+ console.log(` providersOnCooldown: ${this.providersOnCoolDown.length}`);
2377
+ }
2378
+ /**
2379
+ * Get instance ID for debugging
2380
+ */
2381
+ getInstanceId() {
2382
+ return this.instanceId;
2383
+ }
2311
2384
  /**
2312
2385
  * Clean up expired cooldown entries
2313
2386
  */
2314
2387
  cleanupExpiredCooldowns() {
2315
2388
  const now = Date.now();
2389
+ const before = this.providersOnCoolDown.length;
2316
2390
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
2317
- ([, timestamp]) => now - timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2391
+ ([url, timestamp]) => {
2392
+ const age = now - timestamp;
2393
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
2394
+ if (isExpired) {
2395
+ console.log(
2396
+ `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
2397
+ );
2398
+ }
2399
+ return !isExpired;
2400
+ }
2318
2401
  );
2402
+ const after = this.providersOnCoolDown.length;
2403
+ if (before !== after) {
2404
+ console.log(
2405
+ `[cleanupExpiredCooldowns:${this.instanceId}] Cleaned up ${before - after} expired cooldown(s), ${after} remaining`
2406
+ );
2407
+ }
2319
2408
  }
2320
2409
  /**
2321
2410
  * Get the cooldown duration in milliseconds
@@ -2328,7 +2417,8 @@ var ProviderManager = class _ProviderManager {
2328
2417
  */
2329
2418
  isOnCooldown(baseUrl) {
2330
2419
  this.cleanupExpiredCooldowns();
2331
- return this.providersOnCoolDown.some(([url]) => url === baseUrl);
2420
+ const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
2421
+ return result;
2332
2422
  }
2333
2423
  /**
2334
2424
  * Get all providers currently on cooldown
@@ -2342,6 +2432,9 @@ var ProviderManager = class _ProviderManager {
2342
2432
  */
2343
2433
  resetFailedProviders() {
2344
2434
  this.failedProviders.clear();
2435
+ if (this.store) {
2436
+ this.store.getState().setFailedProviders([]);
2437
+ }
2345
2438
  }
2346
2439
  /**
2347
2440
  * Get the last failed timestamp for a provider
@@ -2362,13 +2455,62 @@ var ProviderManager = class _ProviderManager {
2362
2455
  markFailed(baseUrl) {
2363
2456
  const now = Date.now();
2364
2457
  const lastFailure = this.lastFailed.get(baseUrl);
2458
+ console.log(`[markFailed:${this.instanceId}] baseUrl: ${baseUrl}`);
2459
+ console.log(
2460
+ `[markFailed:${this.instanceId}] lastFailure from map: ${lastFailure}`
2461
+ );
2462
+ console.log(
2463
+ `[markFailed:${this.instanceId}] current timestamp (now): ${now}`
2464
+ );
2465
+ console.log(
2466
+ `[markFailed:${this.instanceId}] COOLDOWN_DURATION_MS: ${_ProviderManager.COOLDOWN_DURATION_MS}`
2467
+ );
2468
+ if (lastFailure !== void 0) {
2469
+ const timeSinceLastFailure = now - lastFailure;
2470
+ console.log(
2471
+ `[markFailed:${this.instanceId}] timeSinceLastFailure: ${timeSinceLastFailure}ms`
2472
+ );
2473
+ console.log(
2474
+ `[markFailed:${this.instanceId}] isWithinCooldownWindow: ${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`
2475
+ );
2476
+ }
2365
2477
  this.lastFailed.set(baseUrl, now);
2366
2478
  this.failedProviders.add(baseUrl);
2479
+ if (this.store) {
2480
+ this.store.getState().setLastFailedTimestamp(baseUrl, now);
2481
+ this.store.getState().addFailedProvider(baseUrl);
2482
+ }
2483
+ console.log(
2484
+ `[markFailed:${this.instanceId}] Updated lastFailed map for ${baseUrl} to ${now}`
2485
+ );
2486
+ console.log(
2487
+ `[markFailed:${this.instanceId}] failedProviders set size: ${this.failedProviders.size}`
2488
+ );
2367
2489
  if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
2490
+ console.log(
2491
+ `[markFailed:${this.instanceId}] Second failure detected within cooldown window for ${baseUrl}`
2492
+ );
2368
2493
  if (!this.isOnCooldown(baseUrl)) {
2369
2494
  this.providersOnCoolDown.push([baseUrl, now]);
2495
+ if (this.store) {
2496
+ this.store.getState().addProviderOnCooldown(baseUrl, now);
2497
+ }
2498
+ console.log(
2499
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
2500
+ );
2501
+ } else {
2502
+ console.log(
2503
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} is already on cooldown`
2504
+ );
2505
+ }
2506
+ } else {
2507
+ if (lastFailure === void 0) {
2508
+ console.log(
2509
+ `[markFailed:${this.instanceId}] First failure for ${baseUrl} - not adding to cooldown yet`
2510
+ );
2511
+ } else {
2370
2512
  console.log(
2371
- `Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
2513
+ `[markFailed:${this.instanceId}] Failure outside cooldown window for ${baseUrl} (timeSinceLastFailure: ${now - lastFailure}ms)`
2372
2514
  );
2373
2515
  }
2374
2516
  }
@@ -2380,18 +2522,27 @@ var ProviderManager = class _ProviderManager {
2380
2522
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
2381
2523
  ([url]) => url !== baseUrl
2382
2524
  );
2525
+ if (this.store) {
2526
+ this.store.getState().removeProviderFromCooldown(baseUrl);
2527
+ }
2383
2528
  }
2384
2529
  /**
2385
2530
  * Clear all cooldown tracking
2386
2531
  */
2387
2532
  clearCooldowns() {
2388
2533
  this.providersOnCoolDown = [];
2534
+ if (this.store) {
2535
+ this.store.getState().clearProvidersOnCooldown();
2536
+ }
2389
2537
  }
2390
2538
  /**
2391
2539
  * Clear all failure tracking (lastFailed timestamps)
2392
2540
  */
2393
2541
  clearFailureHistory() {
2394
2542
  this.lastFailed.clear();
2543
+ if (this.store) {
2544
+ this.store.getState().setLastFailed({});
2545
+ }
2395
2546
  }
2396
2547
  /**
2397
2548
  * Check if a provider has failed
@@ -2960,7 +3111,10 @@ var SDK_STORAGE_KEYS = {
2960
3111
  LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
2961
3112
  CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
2962
3113
  USAGE_TRACKING: "usage_tracking",
2963
- CLIENT_IDS: "client_ids"
3114
+ CLIENT_IDS: "client_ids",
3115
+ FAILED_PROVIDERS: "failed_providers",
3116
+ LAST_FAILED: "last_failed",
3117
+ PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
2964
3118
  };
2965
3119
 
2966
3120
  // storage/usageTracking/indexedDB.ts
@@ -3607,6 +3761,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3607
3761
  lastRoutstr21ModelsUpdate: null,
3608
3762
  cachedReceiveTokens: [],
3609
3763
  clientIds: [],
3764
+ failedProviders: [],
3765
+ lastFailed: {},
3766
+ providersOnCooldown: [],
3610
3767
  setModelsFromAllProviders: (value) => {
3611
3768
  const normalized = {};
3612
3769
  for (const [baseUrl, models] of Object.entries(value)) {
@@ -3746,6 +3903,71 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3746
3903
  void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
3747
3904
  return { clientIds: normalized };
3748
3905
  });
3906
+ },
3907
+ // ========== Failure Tracking ==========
3908
+ setFailedProviders: (value) => {
3909
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
3910
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3911
+ set({ failedProviders: normalized });
3912
+ },
3913
+ addFailedProvider: (baseUrl) => {
3914
+ const normalized = normalizeBaseUrl5(baseUrl);
3915
+ const current = get().failedProviders;
3916
+ if (!current.includes(normalized)) {
3917
+ const updated = [...current, normalized];
3918
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3919
+ set({ failedProviders: updated });
3920
+ }
3921
+ },
3922
+ removeFailedProvider: (baseUrl) => {
3923
+ const normalized = normalizeBaseUrl5(baseUrl);
3924
+ const current = get().failedProviders;
3925
+ const updated = current.filter((url) => url !== normalized);
3926
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3927
+ set({ failedProviders: updated });
3928
+ },
3929
+ setLastFailed: (value) => {
3930
+ const normalized = {};
3931
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
3932
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3933
+ }
3934
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3935
+ set({ lastFailed: normalized });
3936
+ },
3937
+ setLastFailedTimestamp: (baseUrl, timestamp) => {
3938
+ const normalized = normalizeBaseUrl5(baseUrl);
3939
+ const current = get().lastFailed;
3940
+ const updated = { ...current, [normalized]: timestamp };
3941
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
3942
+ set({ lastFailed: updated });
3943
+ },
3944
+ setProvidersOnCooldown: (value) => {
3945
+ const normalized = value.map((entry) => ({
3946
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3947
+ timestamp: entry.timestamp
3948
+ }));
3949
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3950
+ set({ providersOnCooldown: normalized });
3951
+ },
3952
+ addProviderOnCooldown: (baseUrl, timestamp) => {
3953
+ const normalized = normalizeBaseUrl5(baseUrl);
3954
+ const current = get().providersOnCooldown;
3955
+ if (!current.some((entry) => entry.baseUrl === normalized)) {
3956
+ const updated = [...current, { baseUrl: normalized, timestamp }];
3957
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3958
+ set({ providersOnCooldown: updated });
3959
+ }
3960
+ },
3961
+ removeProviderFromCooldown: (baseUrl) => {
3962
+ const normalized = normalizeBaseUrl5(baseUrl);
3963
+ const current = get().providersOnCooldown;
3964
+ const updated = current.filter((entry) => entry.baseUrl !== normalized);
3965
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3966
+ set({ providersOnCooldown: updated });
3967
+ },
3968
+ clearProvidersOnCooldown: () => {
3969
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, []);
3970
+ set({ providersOnCooldown: [] });
3749
3971
  }
3750
3972
  }));
3751
3973
  var hydrateStoreFromDriver = async (store, driver) => {
@@ -3764,7 +3986,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
3764
3986
  rawRoutstr21Models,
3765
3987
  rawLastRoutstr21ModelsUpdate,
3766
3988
  rawCachedReceiveTokens,
3767
- rawClientIds
3989
+ rawClientIds,
3990
+ rawFailedProviders,
3991
+ rawLastFailed,
3992
+ rawProvidersOnCooldown
3768
3993
  ] = await Promise.all([
3769
3994
  driver.getItem(
3770
3995
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -3795,7 +4020,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
3795
4020
  null
3796
4021
  ),
3797
4022
  driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
3798
- driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
4023
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
4024
+ driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
4025
+ driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
4026
+ driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
3799
4027
  ]);
3800
4028
  const modelsFromAllProviders = Object.fromEntries(
3801
4029
  Object.entries(rawModels).map(([baseUrl, models]) => [
@@ -3863,6 +4091,17 @@ var hydrateStoreFromDriver = async (store, driver) => {
3863
4091
  createdAt: entry.createdAt ?? Date.now(),
3864
4092
  lastUsed: entry.lastUsed ?? null
3865
4093
  }));
4094
+ const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
4095
+ const lastFailed = Object.fromEntries(
4096
+ Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4097
+ normalizeBaseUrl5(baseUrl),
4098
+ timestamp
4099
+ ])
4100
+ );
4101
+ const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
4102
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
4103
+ timestamp: entry.timestamp
4104
+ }));
3866
4105
  store.setState({
3867
4106
  modelsFromAllProviders,
3868
4107
  lastUsedModel,
@@ -3878,7 +4117,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
3878
4117
  routstr21Models,
3879
4118
  lastRoutstr21ModelsUpdate,
3880
4119
  cachedReceiveTokens,
3881
- clientIds
4120
+ clientIds,
4121
+ failedProviders,
4122
+ lastFailed,
4123
+ providersOnCooldown
3882
4124
  });
3883
4125
  };
3884
4126
  var createSdkStore = ({
@@ -4294,11 +4536,11 @@ var RoutstrClient = class {
4294
4536
  this.balanceManager
4295
4537
  );
4296
4538
  this.streamProcessor = new StreamProcessor();
4297
- this.providerManager = new ProviderManager(providerRegistry);
4298
4539
  this.alertLevel = alertLevel;
4299
4540
  this.mode = mode;
4300
4541
  this.usageTrackingDriver = options.usageTrackingDriver;
4301
4542
  this.sdkStore = options.sdkStore;
4543
+ this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore);
4302
4544
  }
4303
4545
  cashuSpender;
4304
4546
  balanceManager;
@@ -4761,19 +5003,19 @@ var RoutstrClient = class {
4761
5003
  `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
4762
5004
  );
4763
5005
  if (params.token.startsWith("cashu")) {
4764
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
5006
+ const receiveResult = await this.cashuSpender.receiveToken(
4765
5007
  params.token
4766
5008
  );
4767
- if (tryReceiveTokenResult.success) {
5009
+ if (receiveResult.success) {
4768
5010
  this._log(
4769
5011
  "DEBUG",
4770
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
5012
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4771
5013
  );
4772
5014
  tryNextProvider = true;
4773
5015
  } else {
4774
5016
  this._log(
4775
5017
  "DEBUG",
4776
- `[RoutstrClient] _handleErrorResponse: Failed to receive token. `
5018
+ `[RoutstrClient] _handleErrorResponse: Failed to receive token: ${receiveResult.message}`
4777
5019
  );
4778
5020
  }
4779
5021
  }
@@ -4783,23 +5025,18 @@ var RoutstrClient = class {
4783
5025
  "DEBUG",
4784
5026
  `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
4785
5027
  );
4786
- try {
4787
- const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
4788
- if (receiveResult.success) {
4789
- this._log(
4790
- "DEBUG",
4791
- `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
4792
- );
4793
- tryNextProvider = true;
4794
- } else
4795
- throw new ProviderError(
4796
- baseUrl,
4797
- status,
4798
- "xcashu refund failed",
4799
- requestId
4800
- );
4801
- } catch (error) {
4802
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
5028
+ const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
5029
+ if (receiveResult.success) {
5030
+ this._log(
5031
+ "DEBUG",
5032
+ `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
5033
+ );
5034
+ tryNextProvider = true;
5035
+ } else {
5036
+ this._log(
5037
+ "ERROR",
5038
+ `[xcashu] Failed to receive refund token: ${receiveResult.message}`
5039
+ );
4803
5040
  throw new ProviderError(
4804
5041
  baseUrl,
4805
5042
  status,
@@ -4828,6 +5065,10 @@ var RoutstrClient = class {
4828
5065
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
4829
5066
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
4830
5067
  topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
5068
+ this._log(
5069
+ "DEBUG",
5070
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
5071
+ );
4831
5072
  } catch (e) {
4832
5073
  this._log(
4833
5074
  "WARN",
@@ -4957,6 +5198,20 @@ var RoutstrClient = class {
4957
5198
  tryNextProvider = true;
4958
5199
  }
4959
5200
  }
5201
+ if (status === 401 && this.mode === "apikeys") {
5202
+ this._log(
5203
+ "DEBUG",
5204
+ `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
5205
+ );
5206
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5207
+ token,
5208
+ baseUrl
5209
+ );
5210
+ if (latestBalanceInfo.isInvalidApiKey) {
5211
+ this.storageAdapter.removeApiKey(baseUrl);
5212
+ tryNextProvider = true;
5213
+ }
5214
+ }
4960
5215
  if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
4961
5216
  this._log(
4962
5217
  "DEBUG",
@@ -4967,13 +5222,13 @@ var RoutstrClient = class {
4967
5222
  "DEBUG",
4968
5223
  `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
4969
5224
  );
4970
- const initialBalance = await this.balanceManager.getTokenBalance(
5225
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
4971
5226
  token,
4972
5227
  baseUrl
4973
5228
  );
4974
5229
  this._log(
4975
5230
  "DEBUG",
4976
- `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
5231
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
4977
5232
  );
4978
5233
  const refundResult = await this.balanceManager.refundApiKey({
4979
5234
  mintUrl,
@@ -4985,7 +5240,7 @@ var RoutstrClient = class {
4985
5240
  "DEBUG",
4986
5241
  `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
4987
5242
  );
4988
- if (!refundResult.success && initialBalance.amount > 0) {
5243
+ if (!refundResult.success && latestBalanceInfo.amount > 0) {
4989
5244
  throw new ProviderError(
4990
5245
  baseUrl,
4991
5246
  status,
@@ -5074,14 +5329,15 @@ var RoutstrClient = class {
5074
5329
  if (this.mode === "xcashu" && response) {
5075
5330
  const refundToken = response.headers.get("x-cashu") ?? void 0;
5076
5331
  if (refundToken) {
5077
- try {
5078
- const receiveResult = await this.cashuSpender.receiveToken(refundToken);
5079
- if (receiveResult.success) {
5080
- this.storageAdapter.removeXcashuToken(baseUrl, token);
5081
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
5082
- }
5083
- } catch (error) {
5084
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
5332
+ const receiveResult = await this.cashuSpender.receiveToken(refundToken);
5333
+ if (receiveResult.success) {
5334
+ this.storageAdapter.removeXcashuToken(baseUrl, token);
5335
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
5336
+ } else {
5337
+ this._log(
5338
+ "ERROR",
5339
+ `[xcashu] Failed to receive refund token: ${receiveResult.message}`
5340
+ );
5085
5341
  }
5086
5342
  }
5087
5343
  } else if (this.mode === "apikeys") {
@@ -5125,7 +5381,6 @@ var RoutstrClient = class {
5125
5381
  try {
5126
5382
  const xcashuResults = await this.cashuSpender.refundXcashuTokens(mintUrl);
5127
5383
  this._log("DEBUG", "Refund xcashu tokens results:", xcashuResults);
5128
- const results = await this.cashuSpender.refundProviders(mintUrl);
5129
5384
  } catch (error) {
5130
5385
  this._log("ERROR", "Failed to refund providers:", error);
5131
5386
  }
@@ -5334,18 +5589,18 @@ var RoutstrClient = class {
5334
5589
  this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5335
5590
  } catch (error) {
5336
5591
  if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5337
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
5592
+ const receiveResult = await this.cashuSpender.receiveToken(
5338
5593
  spendResult2.token
5339
5594
  );
5340
- if (tryReceiveTokenResult.success) {
5595
+ if (receiveResult.success) {
5341
5596
  this._log(
5342
5597
  "DEBUG",
5343
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
5598
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5344
5599
  );
5345
5600
  } else {
5346
5601
  this._log(
5347
5602
  "DEBUG",
5348
- `[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
5603
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
5349
5604
  );
5350
5605
  }
5351
5606
  this._log(
@@ -5466,7 +5721,8 @@ async function resolveRouteRequestContext(options) {
5466
5721
  debugLevel,
5467
5722
  mode = "apikeys",
5468
5723
  usageTrackingDriver,
5469
- sdkStore
5724
+ sdkStore,
5725
+ providerManager: providedProviderManager
5470
5726
  } = options;
5471
5727
  let modelManager;
5472
5728
  let providers;
@@ -5486,7 +5742,7 @@ async function resolveRouteRequestContext(options) {
5486
5742
  }
5487
5743
  await modelManager.fetchModels(providers, forceRefresh);
5488
5744
  }
5489
- const providerManager = new ProviderManager(providerRegistry);
5745
+ const providerManager = providedProviderManager ?? new ProviderManager(providerRegistry, sdkStore);
5490
5746
  let baseUrl;
5491
5747
  let selectedModel;
5492
5748
  if (forcedProvider) {
@@ -5531,7 +5787,7 @@ async function resolveRouteRequestContext(options) {
5531
5787
  providerRegistry,
5532
5788
  "min",
5533
5789
  mode,
5534
- { usageTrackingDriver, sdkStore }
5790
+ { usageTrackingDriver, sdkStore, providerManager }
5535
5791
  );
5536
5792
  if (debugLevel) {
5537
5793
  client.setDebugLevel(debugLevel);