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