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