@routstr/sdk 0.2.5 → 0.2.7

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,16 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var vanilla = require('zustand/vanilla');
4
- var cashuTs = require('@cashu/cashu-ts');
5
4
  var stream = require('stream');
6
5
 
7
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
- }) : x)(function(x) {
10
- if (typeof require !== "undefined") return require.apply(this, arguments);
11
- throw Error('Dynamic require of "' + x + '" is not supported');
12
- });
13
-
14
6
  // core/errors.ts
15
7
  var InsufficientBalanceError = class extends Error {
16
8
  constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
@@ -154,13 +146,8 @@ var CashuSpender = class {
154
146
  normalizedMintBalances[url] = balanceInSats;
155
147
  totalMintBalance += balanceInSats;
156
148
  }
157
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
158
149
  const providerBalances = {};
159
150
  let totalProviderBalance = 0;
160
- for (const pending of pendingDistribution) {
161
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
162
- totalProviderBalance += pending.amount;
163
- }
164
151
  const apiKeys = this.storageAdapter.getAllApiKeys();
165
152
  for (const apiKey of apiKeys) {
166
153
  if (!providerBalances[apiKey.baseUrl]) {
@@ -381,27 +368,11 @@ var CashuSpender = class {
381
368
  };
382
369
  }
383
370
  }
384
- if (token && baseUrl) {
385
- try {
386
- this.storageAdapter.setToken(baseUrl, token);
387
- } catch (error) {
388
- if (error instanceof Error && error.message.includes("Token already exists")) {
389
- this._log(
390
- "DEBUG",
391
- `[CashuSpender] _spendInternal: Token already exists for ${baseUrl}, receiving newly created token and using existing`
392
- );
393
- const receiveResult = await this.receiveToken(token);
394
- if (receiveResult.success) {
395
- this._log(
396
- "DEBUG",
397
- `[CashuSpender] _spendInternal: Token restored successfully, amount=${receiveResult.amount}`
398
- );
399
- }
400
- token = this.storageAdapter.getToken(baseUrl);
401
- } else {
402
- throw error;
403
- }
404
- }
371
+ if (token) {
372
+ this._log(
373
+ "DEBUG",
374
+ `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
375
+ );
405
376
  }
406
377
  this._logTransaction("spend", {
407
378
  amount: spentAmount,
@@ -422,19 +393,19 @@ var CashuSpender = class {
422
393
  };
423
394
  }
424
395
  /**
425
- * Try to reuse an existing token
396
+ * Try to reuse an existing API key
426
397
  */
427
398
  async _tryReuseToken(baseUrl, amount, mintUrl) {
428
- const storedToken = this.storageAdapter.getToken(baseUrl);
429
- if (!storedToken) return null;
430
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
431
- const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
432
- this._log("DEBUG", "RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
399
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
400
+ if (!apiKeyEntry) return null;
401
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
402
+ const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
403
+ this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
433
404
  if (balanceForBaseUrl > amount) {
434
405
  const units = this.walletAdapter.getMintUnits();
435
406
  const unit = units[mintUrl] || "sat";
436
407
  return {
437
- token: storedToken,
408
+ token: apiKeyEntry.key,
438
409
  status: "success",
439
410
  balance: balanceForBaseUrl,
440
411
  unit
@@ -445,7 +416,8 @@ var CashuSpender = class {
445
416
  const topUpResult = await this.balanceManager.topUp({
446
417
  mintUrl,
447
418
  baseUrl,
448
- amount: topUpAmount
419
+ amount: topUpAmount,
420
+ token: apiKeyEntry.key
449
421
  });
450
422
  this._log("DEBUG", "TOPUP ", topUpResult);
451
423
  if (topUpResult.success && topUpResult.toppedUpAmount) {
@@ -459,7 +431,7 @@ var CashuSpender = class {
459
431
  status: "success"
460
432
  });
461
433
  return {
462
- token: storedToken,
434
+ token: apiKeyEntry.key,
463
435
  status: "success",
464
436
  balance: newBalance,
465
437
  unit
@@ -467,84 +439,131 @@ var CashuSpender = class {
467
439
  }
468
440
  const providerBalance = await this._getProviderTokenBalance(
469
441
  baseUrl,
470
- storedToken
442
+ apiKeyEntry.key
471
443
  );
472
444
  this._log("DEBUG", providerBalance);
473
445
  if (providerBalance <= 0) {
474
- this.storageAdapter.removeToken(baseUrl);
446
+ this.storageAdapter.removeApiKey(baseUrl);
475
447
  }
476
448
  }
477
449
  return null;
478
450
  }
479
451
  /**
480
- * Refund specific providers without retrying spend
452
+ * Refund all xcashu tokens from storage by calling the provider's refund endpoint.
453
+ * The xcashu token acts as an API key to claim the refund, and the response contains
454
+ * the actual refunded Cashu token which is then received into the wallet.
455
+ * @param mintUrl - The mint URL for receiving tokens
456
+ * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
457
+ * @returns Results for each xcashu token refund attempt
481
458
  */
482
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
459
+ async refundXcashuTokens(mintUrl, excludeBaseUrls) {
483
460
  const results = [];
484
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
485
- const toRefund = pendingDistribution.filter(
486
- (p) => baseUrls.includes(p.baseUrl)
487
- );
488
- const refundResults = await Promise.allSettled(
489
- toRefund.map(async (pending) => {
490
- const token = this.storageAdapter.getToken(pending.baseUrl);
491
- this._log("DEBUG", token, this.balanceManager);
492
- if (!token || !this.balanceManager) {
493
- return { baseUrl: pending.baseUrl, success: false };
494
- }
495
- const tokenBalance = await this.balanceManager.getTokenBalance(
496
- token,
497
- pending.baseUrl
498
- );
499
- if (tokenBalance.reserved > 0) {
500
- return { baseUrl: pending.baseUrl, success: false };
501
- }
502
- const result = await this.balanceManager.refund({
503
- mintUrl,
504
- baseUrl: pending.baseUrl,
505
- token
506
- });
507
- this._log("DEBUG", result);
508
- if (result.success) {
509
- this.storageAdapter.removeToken(pending.baseUrl);
510
- }
511
- return { baseUrl: pending.baseUrl, success: result.success };
512
- })
513
- );
514
- results.push(
515
- ...refundResults.map(
516
- (r) => r.status === "fulfilled" ? r.value : { baseUrl: "", success: false }
517
- )
518
- );
519
- if (refundApiKeys) {
520
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
521
- const apiKeysToRefund = apiKeyDistribution.filter(
522
- (p) => baseUrls.includes(p.baseUrl)
523
- );
524
- for (const apiKeyEntry of apiKeysToRefund) {
525
- const apiKeyEntryFull = this.storageAdapter.getApiKey(
526
- apiKeyEntry.baseUrl
527
- );
528
- if (apiKeyEntryFull && this.balanceManager) {
529
- const refundResult = await this.balanceManager.refundApiKey({
530
- mintUrl,
531
- baseUrl: apiKeyEntry.baseUrl,
532
- apiKey: apiKeyEntryFull.key,
533
- forceRefund
534
- });
535
- if (refundResult.success) {
536
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
461
+ const xcashuTokens = this.storageAdapter.getXcashuTokens();
462
+ const excludedUrls = new Set(excludeBaseUrls || []);
463
+ for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
464
+ if (excludedUrls.has(baseUrl)) continue;
465
+ for (const xcashuToken of tokens) {
466
+ try {
467
+ if (!this.balanceManager) {
468
+ throw new Error("BalanceManager not available for xcashu refund");
469
+ }
470
+ const fetchResult = await this.balanceManager.fetchRefundToken(
471
+ baseUrl,
472
+ xcashuToken.token,
473
+ true
474
+ );
475
+ if (!fetchResult.success || !fetchResult.token) {
476
+ throw new Error(
477
+ fetchResult.error || "Failed to fetch refund token from provider"
478
+ );
479
+ }
480
+ const receiveResult = await this.receiveToken(fetchResult.token);
481
+ if (receiveResult.success) {
482
+ this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
483
+ results.push({
484
+ baseUrl,
485
+ token: xcashuToken.token,
486
+ success: true
487
+ });
488
+ this._log(
489
+ "DEBUG",
490
+ `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
491
+ );
492
+ } else {
493
+ const currentTryCount = xcashuToken.tryCount ?? 0;
494
+ const newTryCount = currentTryCount + 1;
495
+ this.storageAdapter.updateXcashuTokenTryCount(
496
+ xcashuToken.token,
497
+ newTryCount
498
+ );
499
+ results.push({
500
+ baseUrl,
501
+ token: xcashuToken.token,
502
+ success: false,
503
+ error: receiveResult.message ?? "Refund failed"
504
+ });
505
+ this._log(
506
+ "DEBUG",
507
+ `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}`
508
+ );
537
509
  }
510
+ } catch (error) {
511
+ const currentTryCount = xcashuToken.tryCount ?? 0;
512
+ const newTryCount = currentTryCount + 1;
513
+ this.storageAdapter.updateXcashuTokenTryCount(
514
+ xcashuToken.token,
515
+ newTryCount
516
+ );
517
+ const errorMessage = error instanceof Error ? error.message : String(error);
538
518
  results.push({
539
- baseUrl: apiKeyEntry.baseUrl,
540
- success: refundResult.success
519
+ baseUrl,
520
+ token: xcashuToken.token,
521
+ success: false,
522
+ error: errorMessage
541
523
  });
524
+ this._log(
525
+ "ERROR",
526
+ `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
527
+ );
528
+ }
529
+ }
530
+ }
531
+ return results;
532
+ }
533
+ /**
534
+ * Refund specific providers without retrying spend
535
+ */
536
+ async refundProviders(mintUrl, forceRefund) {
537
+ const results = [];
538
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
539
+ for (const apiKeyEntry of apiKeyDistribution) {
540
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
541
+ apiKeyEntry.baseUrl
542
+ );
543
+ if (apiKeyEntryFull && this.balanceManager) {
544
+ const refundResult = await this.balanceManager.refundApiKey({
545
+ mintUrl,
546
+ baseUrl: apiKeyEntry.baseUrl,
547
+ apiKey: apiKeyEntryFull.key,
548
+ forceRefund
549
+ });
550
+ if (refundResult.success) {
551
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
542
552
  } else {
543
- results.push({
544
- baseUrl: apiKeyEntry.baseUrl,
545
- success: false
546
- });
553
+ this.storageAdapter.updateApiKeyBalance(
554
+ apiKeyEntry.baseUrl,
555
+ apiKeyEntry.amount
556
+ );
547
557
  }
558
+ results.push({
559
+ baseUrl: apiKeyEntry.baseUrl,
560
+ success: refundResult.success
561
+ });
562
+ } else {
563
+ results.push({
564
+ baseUrl: apiKeyEntry.baseUrl,
565
+ success: false
566
+ });
548
567
  }
549
568
  }
550
569
  return results;
@@ -629,13 +648,8 @@ var BalanceManager = class {
629
648
  normalizedMintBalances[url] = balanceInSats;
630
649
  totalMintBalance += balanceInSats;
631
650
  }
632
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
633
651
  const providerBalances = {};
634
652
  let totalProviderBalance = 0;
635
- for (const pending of pendingDistribution) {
636
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
637
- totalProviderBalance += pending.amount;
638
- }
639
653
  const apiKeys = this.storageAdapter.getAllApiKeys();
640
654
  for (const apiKey of apiKeys) {
641
655
  if (!providerBalances[apiKey.baseUrl]) {
@@ -650,57 +664,6 @@ var BalanceManager = class {
650
664
  mintBalances: normalizedMintBalances
651
665
  };
652
666
  }
653
- /**
654
- * Unified refund - handles both NIP-60 and legacy wallet refunds
655
- */
656
- async refund(options) {
657
- const { mintUrl, baseUrl, token: providedToken } = options;
658
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
659
- if (!storedToken) {
660
- console.log("[BalanceManager] No token to refund, returning early");
661
- return { success: true, message: "No API key to refund" };
662
- }
663
- let fetchResult;
664
- try {
665
- fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
666
- if (!fetchResult.success) {
667
- return {
668
- success: false,
669
- message: fetchResult.error || "Refund failed",
670
- requestId: fetchResult.requestId
671
- };
672
- }
673
- if (!fetchResult.token) {
674
- return {
675
- success: false,
676
- message: "No token received from refund",
677
- requestId: fetchResult.requestId
678
- };
679
- }
680
- if (fetchResult.error === "No balance to refund") {
681
- console.log(
682
- "[BalanceManager] No balance to refund, removing stored token"
683
- );
684
- this.storageAdapter.removeToken(baseUrl);
685
- return { success: true, message: "No balance to refund" };
686
- }
687
- const receiveResult = await this.cashuSpender.receiveToken(
688
- fetchResult.token
689
- );
690
- const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
691
- if (!providedToken) {
692
- this.storageAdapter.removeToken(baseUrl);
693
- }
694
- return {
695
- success: receiveResult.success,
696
- refundedAmount: totalAmountMsat,
697
- requestId: fetchResult.requestId
698
- };
699
- } catch (error) {
700
- console.error("[BalanceManager] Refund error", error);
701
- return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
702
- }
703
- }
704
667
  /**
705
668
  * Refund API key balance - convert remaining API key balance to cashu token
706
669
  * @param options - Refund options including forceRefund flag
@@ -728,7 +691,7 @@ var BalanceManager = class {
728
691
  }
729
692
  let fetchResult;
730
693
  try {
731
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
694
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
732
695
  if (!fetchResult.success) {
733
696
  return {
734
697
  success: false,
@@ -764,9 +727,9 @@ var BalanceManager = class {
764
727
  }
765
728
  }
766
729
  /**
767
- * Fetch refund token from provider API using API key authentication
730
+ * Fetch refund token from provider API using API key (or xcashu token) authentication
768
731
  */
769
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
732
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
770
733
  if (!baseUrl) {
771
734
  return {
772
735
  success: false,
@@ -780,12 +743,17 @@ var BalanceManager = class {
780
743
  controller.abort();
781
744
  }, 6e4);
782
745
  try {
746
+ const headers = {
747
+ "Content-Type": "application/json"
748
+ };
749
+ if (xCashu) {
750
+ headers["X-Cashu"] = apiKeyOrToken;
751
+ } else {
752
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
753
+ }
783
754
  const response = await fetch(url, {
784
755
  method: "POST",
785
- headers: {
786
- Authorization: `Bearer ${apiKey}`,
787
- "Content-Type": "application/json"
788
- },
756
+ headers,
789
757
  signal: controller.signal
790
758
  });
791
759
  clearTimeout(timeoutId);
@@ -806,10 +774,7 @@ var BalanceManager = class {
806
774
  };
807
775
  } catch (error) {
808
776
  clearTimeout(timeoutId);
809
- console.error(
810
- "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
811
- error
812
- );
777
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
813
778
  if (error instanceof Error) {
814
779
  if (error.name === "AbortError") {
815
780
  return {
@@ -836,8 +801,9 @@ var BalanceManager = class {
836
801
  if (!amount || amount <= 0) {
837
802
  return { success: false, message: "Invalid top up amount" };
838
803
  }
839
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
840
- if (!storedToken) {
804
+ const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
805
+ const apiKey = providedToken || apiKeyEntry?.key;
806
+ if (!apiKey) {
841
807
  return { success: false, message: "No API key available for top up" };
842
808
  }
843
809
  let cashuToken = null;
@@ -855,11 +821,7 @@ var BalanceManager = class {
855
821
  };
856
822
  }
857
823
  cashuToken = tokenResult.token;
858
- const topUpResult = await this._postTopUp(
859
- baseUrl,
860
- storedToken,
861
- cashuToken
862
- );
824
+ const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
863
825
  requestId = topUpResult.requestId;
864
826
  console.log(topUpResult);
865
827
  if (!topUpResult.success) {
@@ -1072,38 +1034,11 @@ var BalanceManager = class {
1072
1034
  return candidates;
1073
1035
  }
1074
1036
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1075
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1076
1037
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1077
1038
  const forceRefund = retryCount >= 2;
1078
- const toRefund = pendingDistribution.filter(
1079
- (pending) => pending.baseUrl !== baseUrl
1080
- );
1081
1039
  const apiKeysToRefund = apiKeyDistribution.filter(
1082
1040
  (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1083
1041
  );
1084
- const tokenRefundResults = await Promise.allSettled(
1085
- toRefund.map(async (pending) => {
1086
- const token = this.storageAdapter.getToken(pending.baseUrl);
1087
- if (!token) {
1088
- return { baseUrl: pending.baseUrl, success: false };
1089
- }
1090
- const tokenBalance = await this.getTokenBalance(token, pending.baseUrl);
1091
- if (tokenBalance.reserved > 0) {
1092
- return { baseUrl: pending.baseUrl, success: false };
1093
- }
1094
- const result = await this.refund({
1095
- mintUrl,
1096
- baseUrl: pending.baseUrl,
1097
- token
1098
- });
1099
- return { baseUrl: pending.baseUrl, success: result.success };
1100
- })
1101
- );
1102
- for (const result of tokenRefundResults) {
1103
- if (result.status === "fulfilled" && result.value.success) {
1104
- this.storageAdapter.removeToken(result.value.baseUrl);
1105
- }
1106
- }
1107
1042
  const apiKeyRefundResults = await Promise.allSettled(
1108
1043
  apiKeysToRefund.map(async (apiKeyEntry) => {
1109
1044
  const fullApiKeyEntry = this.storageAdapter.getApiKey(
@@ -1127,77 +1062,6 @@ var BalanceManager = class {
1127
1062
  }
1128
1063
  }
1129
1064
  }
1130
- /**
1131
- * Fetch refund token from provider API
1132
- */
1133
- async _fetchRefundToken(baseUrl, storedToken) {
1134
- if (!baseUrl) {
1135
- return {
1136
- success: false,
1137
- error: "No base URL configured"
1138
- };
1139
- }
1140
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1141
- const url = `${normalizedBaseUrl}v1/wallet/refund`;
1142
- const controller = new AbortController();
1143
- const timeoutId = setTimeout(() => {
1144
- controller.abort();
1145
- }, 6e4);
1146
- try {
1147
- const response = await fetch(url, {
1148
- method: "POST",
1149
- headers: {
1150
- Authorization: `Bearer ${storedToken}`,
1151
- "Content-Type": "application/json"
1152
- },
1153
- signal: controller.signal
1154
- });
1155
- clearTimeout(timeoutId);
1156
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
1157
- if (!response.ok) {
1158
- const errorData = await response.json().catch(() => ({}));
1159
- if (response.status === 400 && errorData?.detail === "No balance to refund") {
1160
- this.storageAdapter.removeToken(baseUrl);
1161
- return {
1162
- success: false,
1163
- requestId,
1164
- error: "No balance to refund"
1165
- };
1166
- }
1167
- return {
1168
- success: false,
1169
- requestId,
1170
- error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1171
- };
1172
- }
1173
- const data = await response.json();
1174
- console.log("refund rsule", data);
1175
- return {
1176
- success: true,
1177
- token: data.token,
1178
- requestId
1179
- };
1180
- } catch (error) {
1181
- clearTimeout(timeoutId);
1182
- console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1183
- if (error instanceof Error) {
1184
- if (error.name === "AbortError") {
1185
- return {
1186
- success: false,
1187
- error: "Request timed out after 1 minute"
1188
- };
1189
- }
1190
- return {
1191
- success: false,
1192
- error: error.message
1193
- };
1194
- }
1195
- return {
1196
- success: false,
1197
- error: "Unknown error occurred during refund request"
1198
- };
1199
- }
1200
- }
1201
1065
  /**
1202
1066
  * Post topup request to provider API
1203
1067
  */
@@ -1323,7 +1187,7 @@ var BalanceManager = class {
1323
1187
  console.log(response.status);
1324
1188
  const data = await response.json();
1325
1189
  console.log("FAILED ", data);
1326
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1190
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1327
1191
  return {
1328
1192
  amount: -1,
1329
1193
  reserved: data.reserved ?? 0,
@@ -1765,8 +1629,13 @@ function isInsecureHttpUrl(url) {
1765
1629
  return url.startsWith("http://");
1766
1630
  }
1767
1631
  var ProviderManager = class _ProviderManager {
1768
- constructor(providerRegistry) {
1632
+ constructor(providerRegistry, store) {
1769
1633
  this.providerRegistry = providerRegistry;
1634
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1635
+ if (store) {
1636
+ this.store = store;
1637
+ this.hydrateFromStore();
1638
+ }
1770
1639
  }
1771
1640
  failedProviders = /* @__PURE__ */ new Set();
1772
1641
  /** Track when each provider last failed (provider URL -> timestamp) */
@@ -1775,14 +1644,57 @@ var ProviderManager = class _ProviderManager {
1775
1644
  providersOnCoolDown = [];
1776
1645
  /** Cooldown duration in milliseconds (5 minutes) */
1777
1646
  static COOLDOWN_DURATION_MS = 5 * 60 * 1e3;
1647
+ /** Optional persistent store for failure tracking */
1648
+ store = null;
1649
+ /** Instance ID for debugging */
1650
+ instanceId;
1651
+ /**
1652
+ * Hydrate in-memory state from persistent store
1653
+ */
1654
+ hydrateFromStore() {
1655
+ if (!this.store) return;
1656
+ const state = this.store.getState();
1657
+ this.failedProviders = new Set(state.failedProviders);
1658
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
1659
+ const now = Date.now();
1660
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
1661
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1662
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
1663
+ console.log(`[ProviderManager:${this.instanceId}] Hydrated from store:`);
1664
+ console.log(` failedProviders: ${this.failedProviders.size}`);
1665
+ console.log(` lastFailed: ${this.lastFailed.size}`);
1666
+ console.log(` providersOnCooldown: ${this.providersOnCoolDown.length}`);
1667
+ }
1668
+ /**
1669
+ * Get instance ID for debugging
1670
+ */
1671
+ getInstanceId() {
1672
+ return this.instanceId;
1673
+ }
1778
1674
  /**
1779
1675
  * Clean up expired cooldown entries
1780
1676
  */
1781
1677
  cleanupExpiredCooldowns() {
1782
1678
  const now = Date.now();
1679
+ const before = this.providersOnCoolDown.length;
1783
1680
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
1784
- ([, timestamp]) => now - timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1681
+ ([url, timestamp]) => {
1682
+ const age = now - timestamp;
1683
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1684
+ if (isExpired) {
1685
+ console.log(
1686
+ `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
1687
+ );
1688
+ }
1689
+ return !isExpired;
1690
+ }
1785
1691
  );
1692
+ const after = this.providersOnCoolDown.length;
1693
+ if (before !== after) {
1694
+ console.log(
1695
+ `[cleanupExpiredCooldowns:${this.instanceId}] Cleaned up ${before - after} expired cooldown(s), ${after} remaining`
1696
+ );
1697
+ }
1786
1698
  }
1787
1699
  /**
1788
1700
  * Get the cooldown duration in milliseconds
@@ -1795,7 +1707,8 @@ var ProviderManager = class _ProviderManager {
1795
1707
  */
1796
1708
  isOnCooldown(baseUrl) {
1797
1709
  this.cleanupExpiredCooldowns();
1798
- return this.providersOnCoolDown.some(([url]) => url === baseUrl);
1710
+ const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1711
+ return result;
1799
1712
  }
1800
1713
  /**
1801
1714
  * Get all providers currently on cooldown
@@ -1809,6 +1722,9 @@ var ProviderManager = class _ProviderManager {
1809
1722
  */
1810
1723
  resetFailedProviders() {
1811
1724
  this.failedProviders.clear();
1725
+ if (this.store) {
1726
+ this.store.getState().setFailedProviders([]);
1727
+ }
1812
1728
  }
1813
1729
  /**
1814
1730
  * Get the last failed timestamp for a provider
@@ -1829,13 +1745,62 @@ var ProviderManager = class _ProviderManager {
1829
1745
  markFailed(baseUrl) {
1830
1746
  const now = Date.now();
1831
1747
  const lastFailure = this.lastFailed.get(baseUrl);
1748
+ console.log(`[markFailed:${this.instanceId}] baseUrl: ${baseUrl}`);
1749
+ console.log(
1750
+ `[markFailed:${this.instanceId}] lastFailure from map: ${lastFailure}`
1751
+ );
1752
+ console.log(
1753
+ `[markFailed:${this.instanceId}] current timestamp (now): ${now}`
1754
+ );
1755
+ console.log(
1756
+ `[markFailed:${this.instanceId}] COOLDOWN_DURATION_MS: ${_ProviderManager.COOLDOWN_DURATION_MS}`
1757
+ );
1758
+ if (lastFailure !== void 0) {
1759
+ const timeSinceLastFailure = now - lastFailure;
1760
+ console.log(
1761
+ `[markFailed:${this.instanceId}] timeSinceLastFailure: ${timeSinceLastFailure}ms`
1762
+ );
1763
+ console.log(
1764
+ `[markFailed:${this.instanceId}] isWithinCooldownWindow: ${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`
1765
+ );
1766
+ }
1832
1767
  this.lastFailed.set(baseUrl, now);
1833
1768
  this.failedProviders.add(baseUrl);
1769
+ if (this.store) {
1770
+ this.store.getState().setLastFailedTimestamp(baseUrl, now);
1771
+ this.store.getState().addFailedProvider(baseUrl);
1772
+ }
1773
+ console.log(
1774
+ `[markFailed:${this.instanceId}] Updated lastFailed map for ${baseUrl} to ${now}`
1775
+ );
1776
+ console.log(
1777
+ `[markFailed:${this.instanceId}] failedProviders set size: ${this.failedProviders.size}`
1778
+ );
1834
1779
  if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1780
+ console.log(
1781
+ `[markFailed:${this.instanceId}] Second failure detected within cooldown window for ${baseUrl}`
1782
+ );
1835
1783
  if (!this.isOnCooldown(baseUrl)) {
1836
1784
  this.providersOnCoolDown.push([baseUrl, now]);
1785
+ if (this.store) {
1786
+ this.store.getState().addProviderOnCooldown(baseUrl, now);
1787
+ }
1788
+ console.log(
1789
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
1790
+ );
1791
+ } else {
1792
+ console.log(
1793
+ `[markFailed:${this.instanceId}] Provider ${baseUrl} is already on cooldown`
1794
+ );
1795
+ }
1796
+ } else {
1797
+ if (lastFailure === void 0) {
1837
1798
  console.log(
1838
- `Provider ${baseUrl} added to cooldown after second failure within 5 minutes`
1799
+ `[markFailed:${this.instanceId}] First failure for ${baseUrl} - not adding to cooldown yet`
1800
+ );
1801
+ } else {
1802
+ console.log(
1803
+ `[markFailed:${this.instanceId}] Failure outside cooldown window for ${baseUrl} (timeSinceLastFailure: ${now - lastFailure}ms)`
1839
1804
  );
1840
1805
  }
1841
1806
  }
@@ -1847,18 +1812,27 @@ var ProviderManager = class _ProviderManager {
1847
1812
  this.providersOnCoolDown = this.providersOnCoolDown.filter(
1848
1813
  ([url]) => url !== baseUrl
1849
1814
  );
1815
+ if (this.store) {
1816
+ this.store.getState().removeProviderFromCooldown(baseUrl);
1817
+ }
1850
1818
  }
1851
1819
  /**
1852
1820
  * Clear all cooldown tracking
1853
1821
  */
1854
1822
  clearCooldowns() {
1855
1823
  this.providersOnCoolDown = [];
1824
+ if (this.store) {
1825
+ this.store.getState().clearProvidersOnCooldown();
1826
+ }
1856
1827
  }
1857
1828
  /**
1858
1829
  * Clear all failure tracking (lastFailed timestamps)
1859
1830
  */
1860
1831
  clearFailureHistory() {
1861
1832
  this.lastFailed.clear();
1833
+ if (this.store) {
1834
+ this.store.getState().setLastFailed({});
1835
+ }
1862
1836
  }
1863
1837
  /**
1864
1838
  * Check if a provider has failed
@@ -2181,38 +2155,54 @@ var createMemoryDriver = (seed) => {
2181
2155
  var isBun = () => {
2182
2156
  return typeof process.versions.bun !== "undefined";
2183
2157
  };
2184
- var createDatabase = (dbPath) => {
2158
+ var cachedDbModule = null;
2159
+ var loadDatabase = async (dbPath) => {
2185
2160
  if (isBun()) {
2186
2161
  throw new Error(
2187
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2162
+ "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
2188
2163
  );
2189
2164
  }
2190
- let Database = null;
2191
2165
  try {
2192
- Database = __require("better-sqlite3");
2166
+ if (!cachedDbModule) {
2167
+ cachedDbModule = (await import('better-sqlite3')).default;
2168
+ }
2169
+ return new cachedDbModule(dbPath);
2193
2170
  } catch (error) {
2194
2171
  throw new Error(
2195
2172
  `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2196
2173
  );
2197
2174
  }
2198
- return new Database(dbPath);
2199
2175
  };
2200
2176
  var createSqliteDriver = (options = {}) => {
2201
2177
  const dbPath = options.dbPath || "routstr.sqlite";
2202
2178
  const tableName = options.tableName || "sdk_storage";
2203
- const db = createDatabase(dbPath);
2204
- db.exec(
2205
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2206
- );
2207
- const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2208
- const upsertStmt = db.prepare(
2209
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2179
+ let db;
2180
+ let selectStmt;
2181
+ let upsertStmt;
2182
+ let deleteStmt;
2183
+ const initDb = async () => {
2184
+ if (!db) {
2185
+ db = await loadDatabase(dbPath);
2186
+ db.exec(
2187
+ `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2188
+ );
2189
+ selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2190
+ upsertStmt = db.prepare(
2191
+ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2210
2192
  ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2211
- );
2212
- const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2193
+ );
2194
+ deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2195
+ }
2196
+ };
2197
+ const ensureInit = async () => {
2198
+ if (!db) {
2199
+ await initDb();
2200
+ }
2201
+ };
2213
2202
  return {
2214
2203
  async getItem(key, defaultValue) {
2215
2204
  try {
2205
+ await ensureInit();
2216
2206
  const row = selectStmt.get(key);
2217
2207
  if (!row || typeof row.value !== "string") return defaultValue;
2218
2208
  try {
@@ -2230,6 +2220,7 @@ var createSqliteDriver = (options = {}) => {
2230
2220
  },
2231
2221
  async setItem(key, value) {
2232
2222
  try {
2223
+ await ensureInit();
2233
2224
  upsertStmt.run(key, JSON.stringify(value));
2234
2225
  } catch (error) {
2235
2226
  console.error(`SQLite setItem failed for key "${key}":`, error);
@@ -2237,6 +2228,7 @@ var createSqliteDriver = (options = {}) => {
2237
2228
  },
2238
2229
  async removeItem(key) {
2239
2230
  try {
2231
+ await ensureInit();
2240
2232
  deleteStmt.run(key);
2241
2233
  } catch (error) {
2242
2234
  console.error(`SQLite removeItem failed for key "${key}":`, error);
@@ -2255,14 +2247,17 @@ var SDK_STORAGE_KEYS = {
2255
2247
  INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
2256
2248
  LAST_MODELS_UPDATE: "lastModelsUpdate",
2257
2249
  LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
2258
- LOCAL_CASHU_TOKENS: "local_cashu_tokens",
2259
2250
  API_KEYS: "api_keys",
2260
2251
  CHILD_KEYS: "child_keys",
2252
+ XCASHU_TOKENS: "xcashu_tokens",
2261
2253
  ROUTSTR21_MODELS: "routstr21Models",
2262
2254
  LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
2263
2255
  CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
2264
2256
  USAGE_TRACKING: "usage_tracking",
2265
- CLIENT_IDS: "client_ids"
2257
+ CLIENT_IDS: "client_ids",
2258
+ FAILED_PROVIDERS: "failed_providers",
2259
+ LAST_FAILED: "last_failed",
2260
+ PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
2266
2261
  };
2267
2262
 
2268
2263
  // storage/usageTracking/indexedDB.ts
@@ -2448,21 +2443,23 @@ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUr
2448
2443
  var isBun2 = () => {
2449
2444
  return typeof process.versions.bun !== "undefined";
2450
2445
  };
2451
- var createDatabase2 = (dbPath) => {
2446
+ var cachedDbModule2 = null;
2447
+ var loadDatabase2 = async (dbPath) => {
2452
2448
  if (isBun2()) {
2453
2449
  throw new Error(
2454
2450
  "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2455
2451
  );
2456
2452
  }
2457
- let Database = null;
2458
2453
  try {
2459
- Database = __require("better-sqlite3");
2454
+ if (!cachedDbModule2) {
2455
+ cachedDbModule2 = (await import('better-sqlite3')).default;
2456
+ }
2457
+ return new cachedDbModule2(dbPath);
2460
2458
  } catch (error) {
2461
2459
  throw new Error(
2462
2460
  `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2463
2461
  );
2464
2462
  }
2465
- return new Database(dbPath);
2466
2463
  };
2467
2464
  var buildWhereClause = (options = {}) => {
2468
2465
  const clauses = [];
@@ -2499,38 +2496,49 @@ var buildWhereClause = (options = {}) => {
2499
2496
  var createSqliteUsageTrackingDriver = (options = {}) => {
2500
2497
  const dbPath = options.dbPath || "routstr.sqlite";
2501
2498
  const tableName = options.tableName || "usage_tracking";
2502
- const db = createDatabase2(dbPath);
2503
2499
  const legacyStorageDriver = options.legacyStorageDriver;
2504
- db.exec(`
2505
- CREATE TABLE IF NOT EXISTS ${tableName} (
2506
- id TEXT PRIMARY KEY,
2507
- timestamp INTEGER NOT NULL,
2508
- model_id TEXT NOT NULL,
2509
- base_url TEXT NOT NULL,
2510
- request_id TEXT NOT NULL,
2511
- cost REAL NOT NULL,
2512
- sats_cost REAL NOT NULL,
2513
- prompt_tokens INTEGER NOT NULL,
2514
- completion_tokens INTEGER NOT NULL,
2515
- total_tokens INTEGER NOT NULL,
2516
- client TEXT,
2517
- session_id TEXT,
2518
- tags TEXT
2519
- );
2520
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2521
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2522
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2523
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2524
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2525
- `);
2526
- const insertStmt = db.prepare(`
2527
- INSERT OR REPLACE INTO ${tableName} (
2528
- id, timestamp, model_id, base_url, request_id,
2529
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2530
- client, session_id, tags
2531
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2532
- `);
2500
+ let db;
2501
+ let insertStmt;
2533
2502
  let migrationComplete = false;
2503
+ const initDb = async () => {
2504
+ if (!db) {
2505
+ db = await loadDatabase2(dbPath);
2506
+ db.exec(`
2507
+ CREATE TABLE IF NOT EXISTS ${tableName} (
2508
+ id TEXT PRIMARY KEY,
2509
+ timestamp INTEGER NOT NULL,
2510
+ model_id TEXT NOT NULL,
2511
+ base_url TEXT NOT NULL,
2512
+ request_id TEXT NOT NULL,
2513
+ cost REAL NOT NULL,
2514
+ sats_cost REAL NOT NULL,
2515
+ prompt_tokens INTEGER NOT NULL,
2516
+ completion_tokens INTEGER NOT NULL,
2517
+ total_tokens INTEGER NOT NULL,
2518
+ client TEXT,
2519
+ session_id TEXT,
2520
+ tags TEXT
2521
+ );
2522
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2523
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2524
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2525
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2526
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2527
+ `);
2528
+ insertStmt = db.prepare(`
2529
+ INSERT OR REPLACE INTO ${tableName} (
2530
+ id, timestamp, model_id, base_url, request_id,
2531
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2532
+ client, session_id, tags
2533
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2534
+ `);
2535
+ }
2536
+ };
2537
+ const ensureInit = async () => {
2538
+ if (!db) {
2539
+ await initDb();
2540
+ }
2541
+ };
2534
2542
  const appendOne = (entry) => {
2535
2543
  insertStmt.run(
2536
2544
  entry.id,
@@ -2588,19 +2596,23 @@ var createSqliteUsageTrackingDriver = (options = {}) => {
2588
2596
  });
2589
2597
  return {
2590
2598
  async migrate() {
2599
+ await ensureInit();
2591
2600
  await ensureMigrated();
2592
2601
  },
2593
2602
  async append(entry) {
2603
+ await ensureInit();
2594
2604
  await ensureMigrated();
2595
2605
  appendOne(entry);
2596
2606
  },
2597
2607
  async appendMany(entries) {
2608
+ await ensureInit();
2598
2609
  await ensureMigrated();
2599
2610
  for (const entry of entries) {
2600
2611
  appendOne(entry);
2601
2612
  }
2602
2613
  },
2603
2614
  async list(options2 = {}) {
2615
+ await ensureInit();
2604
2616
  await ensureMigrated();
2605
2617
  const { sql, params } = buildWhereClause(options2);
2606
2618
  const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
@@ -2613,6 +2625,7 @@ var createSqliteUsageTrackingDriver = (options = {}) => {
2613
2625
  return rows.map(mapRow);
2614
2626
  },
2615
2627
  async count(options2 = {}) {
2628
+ await ensureInit();
2616
2629
  await ensureMigrated();
2617
2630
  const { sql, params } = buildWhereClause(options2);
2618
2631
  const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
@@ -2620,20 +2633,197 @@ var createSqliteUsageTrackingDriver = (options = {}) => {
2620
2633
  return Number(row?.count ?? 0);
2621
2634
  },
2622
2635
  async deleteOlderThan(timestamp) {
2636
+ await ensureInit();
2623
2637
  await ensureMigrated();
2624
2638
  const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2625
2639
  const result = stmt.run(timestamp);
2626
2640
  return result.changes;
2627
2641
  },
2628
2642
  async clear() {
2643
+ await ensureInit();
2629
2644
  await ensureMigrated();
2630
2645
  db.prepare(`DELETE FROM ${tableName}`).run();
2631
2646
  }
2632
2647
  };
2633
2648
  };
2634
2649
 
2635
- // storage/usageTracking/memory.ts
2650
+ // storage/usageTracking/bunSqlite.ts
2651
+ var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
2636
2652
  var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2653
+ var buildWhereClause2 = (options = {}) => {
2654
+ const clauses = [];
2655
+ const params = [];
2656
+ if (typeof options.before === "number") {
2657
+ clauses.push("timestamp < ?");
2658
+ params.push(options.before);
2659
+ }
2660
+ if (typeof options.after === "number") {
2661
+ clauses.push("timestamp > ?");
2662
+ params.push(options.after);
2663
+ }
2664
+ if (options.modelId) {
2665
+ clauses.push("model_id = ?");
2666
+ params.push(options.modelId);
2667
+ }
2668
+ if (options.baseUrl) {
2669
+ clauses.push("base_url = ?");
2670
+ params.push(normalizeBaseUrl3(options.baseUrl));
2671
+ }
2672
+ if (options.sessionId) {
2673
+ clauses.push("session_id = ?");
2674
+ params.push(options.sessionId);
2675
+ }
2676
+ if (options.client) {
2677
+ clauses.push("client = ?");
2678
+ params.push(options.client);
2679
+ }
2680
+ return {
2681
+ sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2682
+ params
2683
+ };
2684
+ };
2685
+ var createBunSqliteUsageTrackingDriver = (options = {}) => {
2686
+ const dbPath = options.dbPath || "routstr.sqlite";
2687
+ const tableName = options.tableName || "usage_tracking";
2688
+ const legacyStorageDriver = options.legacyStorageDriver;
2689
+ const SQLiteDatabase = options.sqlite?.Database;
2690
+ let migrationPromise = null;
2691
+ if (!SQLiteDatabase) {
2692
+ throw new Error(
2693
+ "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
2694
+ );
2695
+ }
2696
+ const db = new SQLiteDatabase(dbPath);
2697
+ db.run(`
2698
+ CREATE TABLE IF NOT EXISTS ${tableName} (
2699
+ id TEXT PRIMARY KEY,
2700
+ timestamp INTEGER NOT NULL,
2701
+ model_id TEXT NOT NULL,
2702
+ base_url TEXT NOT NULL,
2703
+ request_id TEXT NOT NULL,
2704
+ cost REAL NOT NULL,
2705
+ sats_cost REAL NOT NULL,
2706
+ prompt_tokens INTEGER NOT NULL,
2707
+ completion_tokens INTEGER NOT NULL,
2708
+ total_tokens INTEGER NOT NULL,
2709
+ client TEXT,
2710
+ session_id TEXT,
2711
+ tags TEXT
2712
+ )
2713
+ `);
2714
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
2715
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
2716
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
2717
+ const appendOne = (entry) => {
2718
+ db.query(`
2719
+ INSERT OR REPLACE INTO ${tableName} (
2720
+ id, timestamp, model_id, base_url, request_id,
2721
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2722
+ client, session_id, tags
2723
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2724
+ `).run(
2725
+ entry.id,
2726
+ entry.timestamp,
2727
+ entry.modelId,
2728
+ normalizeBaseUrl3(entry.baseUrl),
2729
+ entry.requestId,
2730
+ entry.cost,
2731
+ entry.satsCost,
2732
+ entry.promptTokens,
2733
+ entry.completionTokens,
2734
+ entry.totalTokens,
2735
+ entry.client ?? null,
2736
+ entry.sessionId ?? null,
2737
+ JSON.stringify(entry.tags ?? [])
2738
+ );
2739
+ };
2740
+ const mapRow = (row) => ({
2741
+ id: row.id,
2742
+ timestamp: row.timestamp,
2743
+ modelId: row.model_id,
2744
+ baseUrl: row.base_url,
2745
+ requestId: row.request_id,
2746
+ cost: row.cost,
2747
+ satsCost: row.sats_cost,
2748
+ promptTokens: row.prompt_tokens,
2749
+ completionTokens: row.completion_tokens,
2750
+ totalTokens: row.total_tokens,
2751
+ client: row.client ?? void 0,
2752
+ sessionId: row.session_id ?? void 0,
2753
+ tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2754
+ });
2755
+ const ensureMigrated = async () => {
2756
+ if (!legacyStorageDriver) return;
2757
+ if (!migrationPromise) {
2758
+ migrationPromise = (async () => {
2759
+ const migrated = await legacyStorageDriver.getItem(
2760
+ MIGRATION_MARKER_KEY3,
2761
+ false
2762
+ );
2763
+ if (migrated) return;
2764
+ const legacyEntries = await legacyStorageDriver.getItem(
2765
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2766
+ []
2767
+ );
2768
+ if (legacyEntries.length > 0) {
2769
+ for (const entry of legacyEntries) {
2770
+ appendOne(entry);
2771
+ }
2772
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2773
+ }
2774
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
2775
+ })();
2776
+ }
2777
+ await migrationPromise;
2778
+ };
2779
+ return {
2780
+ async migrate() {
2781
+ await ensureMigrated();
2782
+ },
2783
+ async append(entry) {
2784
+ await ensureMigrated();
2785
+ appendOne(entry);
2786
+ },
2787
+ async appendMany(entries) {
2788
+ await ensureMigrated();
2789
+ for (const entry of entries) {
2790
+ appendOne(entry);
2791
+ }
2792
+ },
2793
+ async list(options2 = {}) {
2794
+ await ensureMigrated();
2795
+ const { sql, params } = buildWhereClause2(options2);
2796
+ const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2797
+ const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
2798
+ let rows;
2799
+ if (typeof options2.limit === "number") {
2800
+ rows = db.query(query).all(...params, options2.limit);
2801
+ } else {
2802
+ rows = db.query(query).all(...params);
2803
+ }
2804
+ return rows.map(mapRow);
2805
+ },
2806
+ async count(options2 = {}) {
2807
+ const { sql, params } = buildWhereClause2(options2);
2808
+ const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
2809
+ const row = db.query(query).get(...params);
2810
+ return Number(row?.count ?? 0);
2811
+ },
2812
+ async deleteOlderThan(timestamp) {
2813
+ await ensureMigrated();
2814
+ const before = timestamp;
2815
+ const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
2816
+ return result.changes ?? 0;
2817
+ },
2818
+ async clear() {
2819
+ await ensureMigrated();
2820
+ db.query(`DELETE FROM ${tableName}`).run();
2821
+ }
2822
+ };
2823
+ };
2824
+
2825
+ // storage/usageTracking/memory.ts
2826
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2637
2827
  var matchesFilters2 = (entry, options = {}) => {
2638
2828
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
2639
2829
  return false;
@@ -2644,7 +2834,7 @@ var matchesFilters2 = (entry, options = {}) => {
2644
2834
  if (options.modelId && entry.modelId !== options.modelId) {
2645
2835
  return false;
2646
2836
  }
2647
- if (options.baseUrl && normalizeBaseUrl3(entry.baseUrl) !== normalizeBaseUrl3(options.baseUrl)) {
2837
+ if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
2648
2838
  return false;
2649
2839
  }
2650
2840
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -2658,18 +2848,18 @@ var matchesFilters2 = (entry, options = {}) => {
2658
2848
  var createMemoryUsageTrackingDriver = (seed = []) => {
2659
2849
  const store = /* @__PURE__ */ new Map();
2660
2850
  for (const entry of seed) {
2661
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2851
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2662
2852
  }
2663
2853
  return {
2664
2854
  async migrate() {
2665
2855
  return;
2666
2856
  },
2667
2857
  async append(entry) {
2668
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2858
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2669
2859
  },
2670
2860
  async appendMany(entries) {
2671
2861
  for (const entry of entries) {
2672
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2862
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2673
2863
  }
2674
2864
  },
2675
2865
  async list(options = {}) {
@@ -2697,20 +2887,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
2697
2887
  }
2698
2888
  };
2699
2889
  };
2700
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2701
- var getCashuTokenBalance = (token) => {
2702
- try {
2703
- const decoded = cashuTs.getDecodedToken(token);
2704
- const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
2705
- let sum = 0;
2706
- for (const proof of decoded.proofs) {
2707
- sum += proof.amount / unitDivisor;
2708
- }
2709
- return sum;
2710
- } catch {
2711
- return 0;
2712
- }
2713
- };
2890
+ var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2714
2891
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2715
2892
  modelsFromAllProviders: {},
2716
2893
  lastUsedModel: null,
@@ -2720,17 +2897,20 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2720
2897
  mintsFromAllProviders: {},
2721
2898
  infoFromAllProviders: {},
2722
2899
  lastModelsUpdate: {},
2723
- cachedTokens: [],
2724
2900
  apiKeys: [],
2725
2901
  childKeys: [],
2902
+ xcashuTokens: {},
2726
2903
  routstr21Models: [],
2727
2904
  lastRoutstr21ModelsUpdate: null,
2728
2905
  cachedReceiveTokens: [],
2729
2906
  clientIds: [],
2907
+ failedProviders: [],
2908
+ lastFailed: {},
2909
+ providersOnCooldown: [],
2730
2910
  setModelsFromAllProviders: (value) => {
2731
2911
  const normalized = {};
2732
2912
  for (const [baseUrl, models] of Object.entries(value)) {
2733
- normalized[normalizeBaseUrl4(baseUrl)] = models;
2913
+ normalized[normalizeBaseUrl5(baseUrl)] = models;
2734
2914
  }
2735
2915
  void driver.setItem(
2736
2916
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -2743,7 +2923,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2743
2923
  set({ lastUsedModel: value });
2744
2924
  },
2745
2925
  setBaseUrlsList: (value) => {
2746
- const normalized = value.map((url) => normalizeBaseUrl4(url));
2926
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
2747
2927
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
2748
2928
  set({ baseUrlsList: normalized });
2749
2929
  },
@@ -2752,14 +2932,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2752
2932
  set({ lastBaseUrlsUpdate: value });
2753
2933
  },
2754
2934
  setDisabledProviders: (value) => {
2755
- const normalized = value.map((url) => normalizeBaseUrl4(url));
2935
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
2756
2936
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
2757
2937
  set({ disabledProviders: normalized });
2758
2938
  },
2759
2939
  setMintsFromAllProviders: (value) => {
2760
2940
  const normalized = {};
2761
2941
  for (const [baseUrl, mints] of Object.entries(value)) {
2762
- normalized[normalizeBaseUrl4(baseUrl)] = mints.map(
2942
+ normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
2763
2943
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
2764
2944
  );
2765
2945
  }
@@ -2772,7 +2952,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2772
2952
  setInfoFromAllProviders: (value) => {
2773
2953
  const normalized = {};
2774
2954
  for (const [baseUrl, info] of Object.entries(value)) {
2775
- normalized[normalizeBaseUrl4(baseUrl)] = info;
2955
+ normalized[normalizeBaseUrl5(baseUrl)] = info;
2776
2956
  }
2777
2957
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
2778
2958
  set({ infoFromAllProviders: normalized });
@@ -2780,30 +2960,17 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2780
2960
  setLastModelsUpdate: (value) => {
2781
2961
  const normalized = {};
2782
2962
  for (const [baseUrl, timestamp] of Object.entries(value)) {
2783
- normalized[normalizeBaseUrl4(baseUrl)] = timestamp;
2963
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2784
2964
  }
2785
2965
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
2786
2966
  set({ lastModelsUpdate: normalized });
2787
2967
  },
2788
- setCachedTokens: (value) => {
2789
- set((state) => {
2790
- const updates = typeof value === "function" ? value(state.cachedTokens) : value;
2791
- const normalized = updates.map((entry) => ({
2792
- ...entry,
2793
- baseUrl: normalizeBaseUrl4(entry.baseUrl),
2794
- balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
2795
- lastUsed: entry.lastUsed ?? null
2796
- }));
2797
- void driver.setItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, normalized);
2798
- return { cachedTokens: normalized };
2799
- });
2800
- },
2801
2968
  setApiKeys: (value) => {
2802
2969
  set((state) => {
2803
2970
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
2804
2971
  const normalized = updates.map((entry) => ({
2805
2972
  ...entry,
2806
- baseUrl: normalizeBaseUrl4(entry.baseUrl),
2973
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
2807
2974
  balance: entry.balance ?? 0,
2808
2975
  lastUsed: entry.lastUsed ?? null
2809
2976
  }));
@@ -2815,7 +2982,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2815
2982
  set((state) => {
2816
2983
  const updates = typeof value === "function" ? value(state.childKeys) : value;
2817
2984
  const normalized = updates.map((entry) => ({
2818
- parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
2985
+ parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2819
2986
  childKey: entry.childKey,
2820
2987
  balance: entry.balance ?? 0,
2821
2988
  balanceLimit: entry.balanceLimit,
@@ -2826,6 +2993,30 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2826
2993
  return { childKeys: normalized };
2827
2994
  });
2828
2995
  },
2996
+ setXcashuTokens: (value) => {
2997
+ const normalized = {};
2998
+ for (const [baseUrl, tokens] of Object.entries(value)) {
2999
+ normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
3000
+ ...entry,
3001
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3002
+ createdAt: entry.createdAt ?? Date.now(),
3003
+ tryCount: entry.tryCount ?? 0
3004
+ }));
3005
+ }
3006
+ void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, normalized);
3007
+ set({ xcashuTokens: normalized });
3008
+ },
3009
+ updateXcashuTokenTryCount: (token, tryCount) => {
3010
+ const currentTokens = get().xcashuTokens;
3011
+ const updatedTokens = {};
3012
+ for (const [baseUrl, tokens] of Object.entries(currentTokens)) {
3013
+ updatedTokens[baseUrl] = tokens.map(
3014
+ (entry) => entry.token === token ? { ...entry, tryCount } : entry
3015
+ );
3016
+ }
3017
+ void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, updatedTokens);
3018
+ set({ xcashuTokens: updatedTokens });
3019
+ },
2829
3020
  setRoutstr21Models: (value) => {
2830
3021
  void driver.setItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, value);
2831
3022
  set({ routstr21Models: value });
@@ -2855,6 +3046,71 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2855
3046
  void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
2856
3047
  return { clientIds: normalized };
2857
3048
  });
3049
+ },
3050
+ // ========== Failure Tracking ==========
3051
+ setFailedProviders: (value) => {
3052
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
3053
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3054
+ set({ failedProviders: normalized });
3055
+ },
3056
+ addFailedProvider: (baseUrl) => {
3057
+ const normalized = normalizeBaseUrl5(baseUrl);
3058
+ const current = get().failedProviders;
3059
+ if (!current.includes(normalized)) {
3060
+ const updated = [...current, normalized];
3061
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3062
+ set({ failedProviders: updated });
3063
+ }
3064
+ },
3065
+ removeFailedProvider: (baseUrl) => {
3066
+ const normalized = normalizeBaseUrl5(baseUrl);
3067
+ const current = get().failedProviders;
3068
+ const updated = current.filter((url) => url !== normalized);
3069
+ void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
3070
+ set({ failedProviders: updated });
3071
+ },
3072
+ setLastFailed: (value) => {
3073
+ const normalized = {};
3074
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
3075
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3076
+ }
3077
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3078
+ set({ lastFailed: normalized });
3079
+ },
3080
+ setLastFailedTimestamp: (baseUrl, timestamp) => {
3081
+ const normalized = normalizeBaseUrl5(baseUrl);
3082
+ const current = get().lastFailed;
3083
+ const updated = { ...current, [normalized]: timestamp };
3084
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
3085
+ set({ lastFailed: updated });
3086
+ },
3087
+ setProvidersOnCooldown: (value) => {
3088
+ const normalized = value.map((entry) => ({
3089
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3090
+ timestamp: entry.timestamp
3091
+ }));
3092
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3093
+ set({ providersOnCooldown: normalized });
3094
+ },
3095
+ addProviderOnCooldown: (baseUrl, timestamp) => {
3096
+ const normalized = normalizeBaseUrl5(baseUrl);
3097
+ const current = get().providersOnCooldown;
3098
+ if (!current.some((entry) => entry.baseUrl === normalized)) {
3099
+ const updated = [...current, { baseUrl: normalized, timestamp }];
3100
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3101
+ set({ providersOnCooldown: updated });
3102
+ }
3103
+ },
3104
+ removeProviderFromCooldown: (baseUrl) => {
3105
+ const normalized = normalizeBaseUrl5(baseUrl);
3106
+ const current = get().providersOnCooldown;
3107
+ const updated = current.filter((entry) => entry.baseUrl !== normalized);
3108
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
3109
+ set({ providersOnCooldown: updated });
3110
+ },
3111
+ clearProvidersOnCooldown: () => {
3112
+ void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, []);
3113
+ set({ providersOnCooldown: [] });
2858
3114
  }
2859
3115
  }));
2860
3116
  var hydrateStoreFromDriver = async (store, driver) => {
@@ -2867,13 +3123,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
2867
3123
  rawMints,
2868
3124
  rawInfo,
2869
3125
  rawLastModelsUpdate,
2870
- rawCachedTokens,
2871
3126
  rawApiKeys,
2872
3127
  rawChildKeys,
3128
+ rawXcashuTokens,
2873
3129
  rawRoutstr21Models,
2874
3130
  rawLastRoutstr21ModelsUpdate,
2875
3131
  rawCachedReceiveTokens,
2876
- rawClientIds
3132
+ rawClientIds,
3133
+ rawFailedProviders,
3134
+ rawLastFailed,
3135
+ rawProvidersOnCooldown
2877
3136
  ] = await Promise.all([
2878
3137
  driver.getItem(
2879
3138
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -2895,65 +3154,73 @@ var hydrateStoreFromDriver = async (store, driver) => {
2895
3154
  SDK_STORAGE_KEYS.LAST_MODELS_UPDATE,
2896
3155
  {}
2897
3156
  ),
2898
- driver.getItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, []),
2899
3157
  driver.getItem(SDK_STORAGE_KEYS.API_KEYS, []),
2900
3158
  driver.getItem(SDK_STORAGE_KEYS.CHILD_KEYS, []),
3159
+ driver.getItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, {}),
2901
3160
  driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
2902
3161
  driver.getItem(
2903
3162
  SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
2904
3163
  null
2905
3164
  ),
2906
3165
  driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
2907
- driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
3166
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
3167
+ driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
3168
+ driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
3169
+ driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
2908
3170
  ]);
2909
3171
  const modelsFromAllProviders = Object.fromEntries(
2910
3172
  Object.entries(rawModels).map(([baseUrl, models]) => [
2911
- normalizeBaseUrl4(baseUrl),
3173
+ normalizeBaseUrl5(baseUrl),
2912
3174
  models
2913
3175
  ])
2914
3176
  );
2915
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl4(url));
3177
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
2916
3178
  const disabledProviders = rawDisabledProviders.map(
2917
- (url) => normalizeBaseUrl4(url)
3179
+ (url) => normalizeBaseUrl5(url)
2918
3180
  );
2919
3181
  const mintsFromAllProviders = Object.fromEntries(
2920
3182
  Object.entries(rawMints).map(([baseUrl, mints]) => [
2921
- normalizeBaseUrl4(baseUrl),
3183
+ normalizeBaseUrl5(baseUrl),
2922
3184
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
2923
3185
  ])
2924
3186
  );
2925
3187
  const infoFromAllProviders = Object.fromEntries(
2926
3188
  Object.entries(rawInfo).map(([baseUrl, info]) => [
2927
- normalizeBaseUrl4(baseUrl),
3189
+ normalizeBaseUrl5(baseUrl),
2928
3190
  info
2929
3191
  ])
2930
3192
  );
2931
3193
  const lastModelsUpdate = Object.fromEntries(
2932
3194
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
2933
- normalizeBaseUrl4(baseUrl),
3195
+ normalizeBaseUrl5(baseUrl),
2934
3196
  timestamp
2935
3197
  ])
2936
3198
  );
2937
- const cachedTokens = rawCachedTokens.map((entry) => ({
2938
- ...entry,
2939
- baseUrl: normalizeBaseUrl4(entry.baseUrl),
2940
- balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
2941
- lastUsed: entry.lastUsed ?? null
2942
- }));
2943
3199
  const apiKeys = rawApiKeys.map((entry) => ({
2944
3200
  ...entry,
2945
- baseUrl: normalizeBaseUrl4(entry.baseUrl),
3201
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
2946
3202
  balance: entry.balance ?? 0,
2947
3203
  lastUsed: entry.lastUsed ?? null
2948
3204
  }));
2949
3205
  const childKeys = rawChildKeys.map((entry) => ({
2950
- parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
3206
+ parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2951
3207
  childKey: entry.childKey,
2952
3208
  balance: entry.balance ?? 0,
2953
3209
  balanceLimit: entry.balanceLimit,
2954
3210
  validityDate: entry.validityDate,
2955
3211
  createdAt: entry.createdAt ?? Date.now()
2956
3212
  }));
3213
+ const xcashuTokens = Object.fromEntries(
3214
+ Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
3215
+ normalizeBaseUrl5(baseUrl),
3216
+ tokens.map((entry) => ({
3217
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3218
+ token: entry.token,
3219
+ createdAt: entry.createdAt ?? Date.now(),
3220
+ tryCount: entry.tryCount ?? 0
3221
+ }))
3222
+ ])
3223
+ );
2957
3224
  const routstr21Models = rawRoutstr21Models;
2958
3225
  const lastRoutstr21ModelsUpdate = rawLastRoutstr21ModelsUpdate;
2959
3226
  const cachedReceiveTokens = rawCachedReceiveTokens?.map((entry) => ({
@@ -2967,6 +3234,17 @@ var hydrateStoreFromDriver = async (store, driver) => {
2967
3234
  createdAt: entry.createdAt ?? Date.now(),
2968
3235
  lastUsed: entry.lastUsed ?? null
2969
3236
  }));
3237
+ const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
3238
+ const lastFailed = Object.fromEntries(
3239
+ Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
3240
+ normalizeBaseUrl5(baseUrl),
3241
+ timestamp
3242
+ ])
3243
+ );
3244
+ const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
3245
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3246
+ timestamp: entry.timestamp
3247
+ }));
2970
3248
  store.setState({
2971
3249
  modelsFromAllProviders,
2972
3250
  lastUsedModel,
@@ -2976,13 +3254,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
2976
3254
  mintsFromAllProviders,
2977
3255
  infoFromAllProviders,
2978
3256
  lastModelsUpdate,
2979
- cachedTokens,
2980
3257
  apiKeys,
2981
3258
  childKeys,
3259
+ xcashuTokens,
2982
3260
  routstr21Models,
2983
3261
  lastRoutstr21ModelsUpdate,
2984
3262
  cachedReceiveTokens,
2985
- clientIds
3263
+ clientIds,
3264
+ failedProviders,
3265
+ lastFailed,
3266
+ providersOnCooldown
2986
3267
  });
2987
3268
  };
2988
3269
  var createSdkStore = ({
@@ -3049,7 +3330,7 @@ var getDefaultUsageTrackingDriver = () => {
3049
3330
  return defaultUsageTrackingDriver;
3050
3331
  }
3051
3332
  if (isBun3()) {
3052
- defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3333
+ defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
3053
3334
  return defaultUsageTrackingDriver;
3054
3335
  }
3055
3336
  if (isNode()) {
@@ -3063,16 +3344,20 @@ var getDefaultUsageTrackingDriver = () => {
3063
3344
  };
3064
3345
  function createSSEParserTransform(onUsage, onResponseId) {
3065
3346
  let buffer = "";
3347
+ let usageCaptured = false;
3348
+ let responseIdCaptured = false;
3066
3349
  const maybeCaptureUsageFromJson = (jsonText) => {
3067
3350
  try {
3068
3351
  const data = JSON.parse(jsonText);
3069
3352
  const responseId = data.id;
3070
3353
  if (typeof responseId === "string" && responseId.trim().length > 0) {
3071
3354
  onResponseId?.(responseId.trim());
3355
+ responseIdCaptured = true;
3072
3356
  }
3073
3357
  const usage = extractUsageFromSSEJson(data);
3074
3358
  if (usage) {
3075
3359
  onUsage(usage);
3360
+ usageCaptured = true;
3076
3361
  }
3077
3362
  } catch {
3078
3363
  }
@@ -3128,7 +3413,7 @@ function createSSEParserTransform(onUsage, onResponseId) {
3128
3413
  }
3129
3414
  var TOPUP_MARGIN = 1.2;
3130
3415
  var RoutstrClient = class {
3131
- constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
3416
+ constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
3132
3417
  this.walletAdapter = walletAdapter;
3133
3418
  this.storageAdapter = storageAdapter;
3134
3419
  this.providerRegistry = providerRegistry;
@@ -3144,15 +3429,11 @@ var RoutstrClient = class {
3144
3429
  this.balanceManager
3145
3430
  );
3146
3431
  this.streamProcessor = new StreamProcessor();
3147
- this.providerManager = new ProviderManager(providerRegistry);
3148
3432
  this.alertLevel = alertLevel;
3149
- if (mode === "lazyrefund") {
3150
- this.mode = "apikeys";
3151
- } else if (mode === "apikeys") {
3152
- this.mode = "lazyrefund";
3153
- } else {
3154
- this.mode = mode;
3155
- }
3433
+ this.mode = mode;
3434
+ this.usageTrackingDriver = options.usageTrackingDriver;
3435
+ this.sdkStore = options.sdkStore;
3436
+ this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore);
3156
3437
  }
3157
3438
  cashuSpender;
3158
3439
  balanceManager;
@@ -3161,6 +3442,8 @@ var RoutstrClient = class {
3161
3442
  alertLevel;
3162
3443
  mode;
3163
3444
  debugLevel = "WARN";
3445
+ usageTrackingDriver;
3446
+ sdkStore;
3164
3447
  /**
3165
3448
  * Get the current client mode
3166
3449
  */
@@ -3230,11 +3513,13 @@ var RoutstrClient = class {
3230
3513
  const satsSpent = await this._handlePostResponseBalanceUpdate({
3231
3514
  token: prepared.tokenUsed,
3232
3515
  baseUrl: prepared.baseUrlUsed,
3516
+ mintUrl: params.mintUrl,
3233
3517
  initialTokenBalance: prepared.tokenBalanceInSats,
3234
3518
  response: prepared.response,
3235
3519
  modelId: prepared.modelId,
3236
3520
  usage: prepared.capturedUsage,
3237
- requestId: prepared.capturedResponseId
3521
+ requestId: prepared.capturedResponseId,
3522
+ clientApiKey: prepared.clientApiKey
3238
3523
  });
3239
3524
  prepared.response.satsSpent = satsSpent;
3240
3525
  prepared.response.usage = prepared.capturedUsage;
@@ -3253,11 +3538,13 @@ var RoutstrClient = class {
3253
3538
  const satsSpent = await this._handlePostResponseBalanceUpdate({
3254
3539
  token: prepared.tokenUsed,
3255
3540
  baseUrl: prepared.baseUrlUsed,
3541
+ mintUrl: params.mintUrl,
3256
3542
  initialTokenBalance: prepared.tokenBalanceInSats,
3257
3543
  response: prepared.response,
3258
3544
  modelId: prepared.modelId,
3259
3545
  usage: prepared.capturedUsage,
3260
- requestId: prepared.capturedResponseId
3546
+ requestId: prepared.capturedResponseId,
3547
+ clientApiKey: prepared.clientApiKey
3261
3548
  });
3262
3549
  prepared.response.satsSpent = satsSpent;
3263
3550
  res.end();
@@ -3273,11 +3560,13 @@ var RoutstrClient = class {
3273
3560
  const satsSpent = await this._handlePostResponseBalanceUpdate({
3274
3561
  token: prepared.tokenUsed,
3275
3562
  baseUrl: prepared.baseUrlUsed,
3563
+ mintUrl: params.mintUrl,
3276
3564
  initialTokenBalance: prepared.tokenBalanceInSats,
3277
3565
  response: prepared.response,
3278
3566
  modelId: prepared.modelId,
3279
3567
  usage: prepared.capturedUsage,
3280
- requestId: prepared.capturedResponseId
3568
+ requestId: prepared.capturedResponseId,
3569
+ clientApiKey: prepared.clientApiKey
3281
3570
  });
3282
3571
  prepared.response.satsSpent = satsSpent;
3283
3572
  prepared.response.usage = prepared.capturedUsage;
@@ -3307,8 +3596,10 @@ var RoutstrClient = class {
3307
3596
  headers = {},
3308
3597
  baseUrl,
3309
3598
  mintUrl,
3310
- modelId
3599
+ modelId,
3600
+ clientApiKey: providedClientApiKey
3311
3601
  } = params;
3602
+ const clientApiKey = providedClientApiKey ?? this._extractClientApiKey(headers);
3312
3603
  await this._checkBalance();
3313
3604
  let requiredSats = 1;
3314
3605
  let selectedModel;
@@ -3330,7 +3621,6 @@ var RoutstrClient = class {
3330
3621
  amount: requiredSats,
3331
3622
  baseUrl
3332
3623
  });
3333
- this._log("DEBUG", token, baseUrl);
3334
3624
  let requestBody = body;
3335
3625
  if (body && typeof body === "object") {
3336
3626
  const bodyObj = body;
@@ -3338,7 +3628,7 @@ var RoutstrClient = class {
3338
3628
  requestBody = { ...bodyObj, stream: false };
3339
3629
  }
3340
3630
  }
3341
- const baseHeaders = this._buildBaseHeaders(headers);
3631
+ const baseHeaders = this._buildBaseHeaders();
3342
3632
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
3343
3633
  const response = await this._makeRequest({
3344
3634
  path,
@@ -3390,9 +3680,21 @@ var RoutstrClient = class {
3390
3680
  tokenBalanceInSats,
3391
3681
  modelId,
3392
3682
  capturedUsage,
3393
- capturedResponseId
3683
+ capturedResponseId,
3684
+ clientApiKey
3394
3685
  };
3395
3686
  }
3687
+ /**
3688
+ * Extract clientApiKey from Authorization Bearer token if present
3689
+ */
3690
+ _extractClientApiKey(headers) {
3691
+ const authHeader = headers["Authorization"] || headers["authorization"];
3692
+ if (authHeader?.startsWith("Bearer ")) {
3693
+ const extractedKey = authHeader.slice(7);
3694
+ return extractedKey;
3695
+ }
3696
+ return void 0;
3697
+ }
3396
3698
  /**
3397
3699
  * Fetch AI response with streaming
3398
3700
  */
@@ -3496,6 +3798,7 @@ var RoutstrClient = class {
3496
3798
  let satsSpent = await this._handlePostResponseBalanceUpdate({
3497
3799
  token,
3498
3800
  baseUrl: baseUrlUsed,
3801
+ mintUrl,
3499
3802
  initialTokenBalance: tokenBalanceInSats,
3500
3803
  fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
3501
3804
  response,
@@ -3534,7 +3837,6 @@ var RoutstrClient = class {
3534
3837
  try {
3535
3838
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
3536
3839
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
3537
- this._log("DEBUG", "HEADERS,", headers);
3538
3840
  const response = await fetch(url, {
3539
3841
  method,
3540
3842
  headers,
@@ -3603,8 +3905,6 @@ var RoutstrClient = class {
3603
3905
  `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
3604
3906
  );
3605
3907
  tryNextProvider = true;
3606
- if (this.mode === "lazyrefund")
3607
- this.storageAdapter.removeToken(baseUrl);
3608
3908
  } else {
3609
3909
  this._log(
3610
3910
  "DEBUG",
@@ -3652,24 +3952,21 @@ var RoutstrClient = class {
3652
3952
  );
3653
3953
  }
3654
3954
  }
3655
- if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
3955
+ if (status === 402 && !tryNextProvider && this.mode === "apikeys") {
3656
3956
  this.storageAdapter.getApiKey(baseUrl);
3657
3957
  let topupAmount = params.requiredSats;
3658
3958
  try {
3659
- let currentBalance = 0;
3660
- if (this.mode === "apikeys") {
3661
- const currentBalanceInfo = await this.balanceManager.getTokenBalance(
3662
- params.token,
3663
- baseUrl
3664
- );
3665
- currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
3666
- } else if (this.mode === "lazyrefund") {
3667
- const distribution = this.storageAdapter.getCachedTokenDistribution();
3668
- const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
3669
- currentBalance = tokenEntry?.amount ?? 0;
3670
- }
3959
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
3960
+ params.token,
3961
+ baseUrl
3962
+ );
3963
+ const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
3671
3964
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
3672
3965
  topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
3966
+ this._log(
3967
+ "DEBUG",
3968
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
3969
+ );
3673
3970
  } catch (e) {
3674
3971
  this._log(
3675
3972
  "WARN",
@@ -3799,61 +4096,49 @@ var RoutstrClient = class {
3799
4096
  tryNextProvider = true;
3800
4097
  }
3801
4098
  }
4099
+ if (status === 401 && this.mode === "apikeys") {
4100
+ this._log(
4101
+ "DEBUG",
4102
+ `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
4103
+ );
4104
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
4105
+ token,
4106
+ baseUrl
4107
+ );
4108
+ if (latestBalanceInfo.isInvalidApiKey) {
4109
+ this.storageAdapter.removeApiKey(baseUrl);
4110
+ tryNextProvider = true;
4111
+ }
4112
+ }
3802
4113
  if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
3803
4114
  this._log(
3804
4115
  "DEBUG",
3805
4116
  `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
3806
4117
  );
3807
- if (this.mode === "lazyrefund") {
3808
- try {
3809
- const refundResult = await this.balanceManager.refund({
3810
- mintUrl,
3811
- baseUrl,
3812
- token: params.token
3813
- });
3814
- this._log(
3815
- "DEBUG",
3816
- `[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
3817
- );
3818
- if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
3819
- else
3820
- throw new ProviderError(
3821
- baseUrl,
3822
- status,
3823
- "refund failed",
3824
- requestId
3825
- );
3826
- } catch (error) {
3827
- throw new ProviderError(
3828
- baseUrl,
3829
- status,
3830
- "Failed to refund token",
3831
- requestId
3832
- );
3833
- }
3834
- } else if (this.mode === "apikeys") {
4118
+ if (this.mode === "apikeys") {
3835
4119
  this._log(
3836
4120
  "DEBUG",
3837
4121
  `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
3838
4122
  );
3839
- const initialBalance = await this.balanceManager.getTokenBalance(
4123
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3840
4124
  token,
3841
4125
  baseUrl
3842
4126
  );
3843
4127
  this._log(
3844
4128
  "DEBUG",
3845
- `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
4129
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
3846
4130
  );
3847
4131
  const refundResult = await this.balanceManager.refundApiKey({
3848
4132
  mintUrl,
3849
4133
  baseUrl,
3850
- apiKey: token
4134
+ apiKey: token,
4135
+ forceRefund: true
3851
4136
  });
3852
4137
  this._log(
3853
4138
  "DEBUG",
3854
4139
  `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
3855
4140
  );
3856
- if (!refundResult.success && initialBalance.amount > 0) {
4141
+ if (!refundResult.success && latestBalanceInfo.amount > 0) {
3857
4142
  throw new ProviderError(
3858
4143
  baseUrl,
3859
4144
  status,
@@ -3929,12 +4214,14 @@ var RoutstrClient = class {
3929
4214
  const {
3930
4215
  token,
3931
4216
  baseUrl,
4217
+ mintUrl,
3932
4218
  initialTokenBalance,
3933
4219
  fallbackSatsSpent,
3934
4220
  response,
3935
4221
  modelId,
3936
4222
  usage,
3937
- requestId
4223
+ requestId,
4224
+ clientApiKey
3938
4225
  } = params;
3939
4226
  let satsSpent = initialTokenBalance;
3940
4227
  if (this.mode === "xcashu" && response) {
@@ -3942,19 +4229,14 @@ var RoutstrClient = class {
3942
4229
  if (refundToken) {
3943
4230
  try {
3944
4231
  const receiveResult = await this.cashuSpender.receiveToken(refundToken);
3945
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
4232
+ if (receiveResult.success) {
4233
+ this.storageAdapter.removeXcashuToken(baseUrl, token);
4234
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
4235
+ }
3946
4236
  } catch (error) {
3947
4237
  this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
3948
4238
  }
3949
4239
  }
3950
- } else if (this.mode === "lazyrefund") {
3951
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3952
- token,
3953
- baseUrl
3954
- );
3955
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3956
- this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
3957
- satsSpent = initialTokenBalance - latestTokenBalance;
3958
4240
  } else if (this.mode === "apikeys") {
3959
4241
  try {
3960
4242
  const latestBalanceInfo = await this.balanceManager.getTokenBalance(
@@ -3989,8 +4271,17 @@ var RoutstrClient = class {
3989
4271
  modelId,
3990
4272
  satsSpent,
3991
4273
  usage,
3992
- requestId
4274
+ requestId,
4275
+ clientApiKey
3993
4276
  });
4277
+ (async () => {
4278
+ try {
4279
+ const xcashuResults = await this.cashuSpender.refundXcashuTokens(mintUrl);
4280
+ this._log("DEBUG", "Refund xcashu tokens results:", xcashuResults);
4281
+ } catch (error) {
4282
+ this._log("ERROR", "Failed to refund providers:", error);
4283
+ }
4284
+ })();
3994
4285
  return satsSpent;
3995
4286
  }
3996
4287
  async _trackResponseUsage(params) {
@@ -4001,7 +4292,8 @@ var RoutstrClient = class {
4001
4292
  modelId,
4002
4293
  satsSpent,
4003
4294
  usage: providedUsage,
4004
- requestId: providedRequestId
4295
+ requestId: providedRequestId,
4296
+ clientApiKey
4005
4297
  } = params;
4006
4298
  if (!response || !modelId) {
4007
4299
  return;
@@ -4028,13 +4320,14 @@ var RoutstrClient = class {
4028
4320
  return;
4029
4321
  }
4030
4322
  const finalRequestId = requestId || "unknown";
4031
- const store = await getDefaultSdkStore();
4323
+ const store = this.sdkStore ?? await getDefaultSdkStore();
4032
4324
  const state = store.getState();
4325
+ const matchKey = clientApiKey ?? token;
4033
4326
  const matchingClient = state.clientIds.find(
4034
- (client) => client.apiKey === token
4327
+ (client) => client.apiKey === matchKey
4035
4328
  );
4036
4329
  const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
4037
- const usageTracking = getDefaultUsageTrackingDriver();
4330
+ const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
4038
4331
  const entry = {
4039
4332
  id: entryId,
4040
4333
  timestamp: Date.now(),
@@ -4109,11 +4402,11 @@ var RoutstrClient = class {
4109
4402
  return estimatedCosts;
4110
4403
  }
4111
4404
  /**
4112
- * Get pending cashu token amount
4405
+ * Get pending API key amount
4113
4406
  */
4114
4407
  _getPendingCashuTokenAmount() {
4115
- const distribution = this.storageAdapter.getCachedTokenDistribution();
4116
- return distribution.reduce((total, item) => total + item.amount, 0);
4408
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4409
+ return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
4117
4410
  }
4118
4411
  /**
4119
4412
  * Handle errors and notify callbacks
@@ -4260,8 +4553,8 @@ var RoutstrClient = class {
4260
4553
  const spendResult = await this.cashuSpender.spend({
4261
4554
  mintUrl,
4262
4555
  amount,
4263
- baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
4264
- reuseToken: this.mode === "lazyrefund"
4556
+ baseUrl: "",
4557
+ reuseToken: false
4265
4558
  });
4266
4559
  if (!spendResult.token) {
4267
4560
  this._log(
@@ -4274,6 +4567,7 @@ var RoutstrClient = class {
4274
4567
  "DEBUG",
4275
4568
  `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4276
4569
  );
4570
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4277
4571
  }
4278
4572
  return {
4279
4573
  token: spendResult.token,