@routstr/sdk 0.2.5 → 0.2.6

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.
@@ -122,13 +122,8 @@ var CashuSpender = class {
122
122
  normalizedMintBalances[url] = balanceInSats;
123
123
  totalMintBalance += balanceInSats;
124
124
  }
125
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
126
125
  const providerBalances = {};
127
126
  let totalProviderBalance = 0;
128
- for (const pending of pendingDistribution) {
129
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
130
- totalProviderBalance += pending.amount;
131
- }
132
127
  const apiKeys = this.storageAdapter.getAllApiKeys();
133
128
  for (const apiKey of apiKeys) {
134
129
  if (!providerBalances[apiKey.baseUrl]) {
@@ -349,27 +344,11 @@ var CashuSpender = class {
349
344
  };
350
345
  }
351
346
  }
352
- if (token && baseUrl) {
353
- try {
354
- this.storageAdapter.setToken(baseUrl, token);
355
- } catch (error) {
356
- if (error instanceof Error && error.message.includes("Token already exists")) {
357
- this._log(
358
- "DEBUG",
359
- `[CashuSpender] _spendInternal: Token already exists for ${baseUrl}, receiving newly created token and using existing`
360
- );
361
- const receiveResult = await this.receiveToken(token);
362
- if (receiveResult.success) {
363
- this._log(
364
- "DEBUG",
365
- `[CashuSpender] _spendInternal: Token restored successfully, amount=${receiveResult.amount}`
366
- );
367
- }
368
- token = this.storageAdapter.getToken(baseUrl);
369
- } else {
370
- throw error;
371
- }
372
- }
347
+ if (token) {
348
+ this._log(
349
+ "DEBUG",
350
+ `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
351
+ );
373
352
  }
374
353
  this._logTransaction("spend", {
375
354
  amount: spentAmount,
@@ -390,19 +369,19 @@ var CashuSpender = class {
390
369
  };
391
370
  }
392
371
  /**
393
- * Try to reuse an existing token
372
+ * Try to reuse an existing API key
394
373
  */
395
374
  async _tryReuseToken(baseUrl, amount, mintUrl) {
396
- const storedToken = this.storageAdapter.getToken(baseUrl);
397
- if (!storedToken) return null;
398
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
399
- const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
400
- this._log("DEBUG", "RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
375
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
376
+ if (!apiKeyEntry) return null;
377
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
378
+ const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
379
+ this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
401
380
  if (balanceForBaseUrl > amount) {
402
381
  const units = this.walletAdapter.getMintUnits();
403
382
  const unit = units[mintUrl] || "sat";
404
383
  return {
405
- token: storedToken,
384
+ token: apiKeyEntry.key,
406
385
  status: "success",
407
386
  balance: balanceForBaseUrl,
408
387
  unit
@@ -413,7 +392,8 @@ var CashuSpender = class {
413
392
  const topUpResult = await this.balanceManager.topUp({
414
393
  mintUrl,
415
394
  baseUrl,
416
- amount: topUpAmount
395
+ amount: topUpAmount,
396
+ token: apiKeyEntry.key
417
397
  });
418
398
  this._log("DEBUG", "TOPUP ", topUpResult);
419
399
  if (topUpResult.success && topUpResult.toppedUpAmount) {
@@ -427,7 +407,7 @@ var CashuSpender = class {
427
407
  status: "success"
428
408
  });
429
409
  return {
430
- token: storedToken,
410
+ token: apiKeyEntry.key,
431
411
  status: "success",
432
412
  balance: newBalance,
433
413
  unit
@@ -435,84 +415,108 @@ var CashuSpender = class {
435
415
  }
436
416
  const providerBalance = await this._getProviderTokenBalance(
437
417
  baseUrl,
438
- storedToken
418
+ apiKeyEntry.key
439
419
  );
440
420
  this._log("DEBUG", providerBalance);
441
421
  if (providerBalance <= 0) {
442
- this.storageAdapter.removeToken(baseUrl);
422
+ this.storageAdapter.removeApiKey(baseUrl);
443
423
  }
444
424
  }
445
425
  return null;
446
426
  }
447
427
  /**
448
- * Refund specific providers without retrying spend
428
+ * Refund all xcashu tokens from storage and increment tryCounts on failure.
429
+ * Reuses receiveToken from BalanceManager/CashuSpender for receiving refunds.
430
+ * @param mintUrl - The mint URL for receiving tokens
431
+ * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
432
+ * @returns Results for each xcashu token refund attempt
449
433
  */
450
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
434
+ async refundXcashuTokens(mintUrl, excludeBaseUrls) {
451
435
  const results = [];
452
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
453
- const toRefund = pendingDistribution.filter(
454
- (p) => baseUrls.includes(p.baseUrl)
455
- );
456
- const refundResults = await Promise.allSettled(
457
- toRefund.map(async (pending) => {
458
- const token = this.storageAdapter.getToken(pending.baseUrl);
459
- this._log("DEBUG", token, this.balanceManager);
460
- if (!token || !this.balanceManager) {
461
- return { baseUrl: pending.baseUrl, success: false };
462
- }
463
- const tokenBalance = await this.balanceManager.getTokenBalance(
464
- token,
465
- pending.baseUrl
466
- );
467
- if (tokenBalance.reserved > 0) {
468
- return { baseUrl: pending.baseUrl, success: false };
469
- }
470
- const result = await this.balanceManager.refund({
471
- mintUrl,
472
- baseUrl: pending.baseUrl,
473
- token
474
- });
475
- this._log("DEBUG", result);
476
- if (result.success) {
477
- this.storageAdapter.removeToken(pending.baseUrl);
478
- }
479
- return { baseUrl: pending.baseUrl, success: result.success };
480
- })
481
- );
482
- results.push(
483
- ...refundResults.map(
484
- (r) => r.status === "fulfilled" ? r.value : { baseUrl: "", success: false }
485
- )
486
- );
487
- if (refundApiKeys) {
488
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
489
- const apiKeysToRefund = apiKeyDistribution.filter(
490
- (p) => baseUrls.includes(p.baseUrl)
491
- );
492
- for (const apiKeyEntry of apiKeysToRefund) {
493
- const apiKeyEntryFull = this.storageAdapter.getApiKey(
494
- apiKeyEntry.baseUrl
495
- );
496
- if (apiKeyEntryFull && this.balanceManager) {
497
- const refundResult = await this.balanceManager.refundApiKey({
498
- mintUrl,
499
- baseUrl: apiKeyEntry.baseUrl,
500
- apiKey: apiKeyEntryFull.key,
501
- forceRefund
502
- });
503
- if (refundResult.success) {
504
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
436
+ const xcashuTokens = this.storageAdapter.getXcashuTokens();
437
+ const excludedUrls = new Set(excludeBaseUrls || []);
438
+ for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
439
+ if (excludedUrls.has(baseUrl)) continue;
440
+ for (const xcashuToken of tokens) {
441
+ try {
442
+ const receiveResult = await this.receiveToken(xcashuToken.token);
443
+ if (receiveResult.success) {
444
+ this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
445
+ results.push({
446
+ baseUrl,
447
+ token: xcashuToken.token,
448
+ success: true
449
+ });
450
+ this._log(
451
+ "DEBUG",
452
+ `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
453
+ );
454
+ } else {
455
+ const currentTryCount = xcashuToken.tryCount ?? 0;
456
+ const newTryCount = currentTryCount + 1;
457
+ this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
458
+ results.push({
459
+ baseUrl,
460
+ token: xcashuToken.token,
461
+ success: false,
462
+ error: receiveResult.message ?? "Refund failed"
463
+ });
464
+ this._log(
465
+ "DEBUG",
466
+ `[CashuSpender] refundXcashuTokens: Failed to refund xcashu token for ${baseUrl}, incremented tryCount to ${newTryCount}`
467
+ );
505
468
  }
469
+ } catch (error) {
470
+ const currentTryCount = xcashuToken.tryCount ?? 0;
471
+ const newTryCount = currentTryCount + 1;
472
+ this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
473
+ const errorMessage = error instanceof Error ? error.message : String(error);
506
474
  results.push({
507
- baseUrl: apiKeyEntry.baseUrl,
508
- success: refundResult.success
475
+ baseUrl,
476
+ token: xcashuToken.token,
477
+ success: false,
478
+ error: errorMessage
509
479
  });
480
+ this._log(
481
+ "ERROR",
482
+ `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
483
+ );
484
+ }
485
+ }
486
+ }
487
+ return results;
488
+ }
489
+ /**
490
+ * Refund specific providers without retrying spend
491
+ */
492
+ async refundProviders(mintUrl, forceRefund) {
493
+ const results = [];
494
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
495
+ for (const apiKeyEntry of apiKeyDistribution) {
496
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
497
+ apiKeyEntry.baseUrl
498
+ );
499
+ if (apiKeyEntryFull && this.balanceManager) {
500
+ const refundResult = await this.balanceManager.refundApiKey({
501
+ mintUrl,
502
+ baseUrl: apiKeyEntry.baseUrl,
503
+ apiKey: apiKeyEntryFull.key,
504
+ forceRefund
505
+ });
506
+ if (refundResult.success) {
507
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
510
508
  } else {
511
- results.push({
512
- baseUrl: apiKeyEntry.baseUrl,
513
- success: false
514
- });
509
+ this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, apiKeyEntry.amount);
515
510
  }
511
+ results.push({
512
+ baseUrl: apiKeyEntry.baseUrl,
513
+ success: refundResult.success
514
+ });
515
+ } else {
516
+ results.push({
517
+ baseUrl: apiKeyEntry.baseUrl,
518
+ success: false
519
+ });
516
520
  }
517
521
  }
518
522
  return results;
@@ -597,13 +601,8 @@ var BalanceManager = class {
597
601
  normalizedMintBalances[url] = balanceInSats;
598
602
  totalMintBalance += balanceInSats;
599
603
  }
600
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
601
604
  const providerBalances = {};
602
605
  let totalProviderBalance = 0;
603
- for (const pending of pendingDistribution) {
604
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
605
- totalProviderBalance += pending.amount;
606
- }
607
606
  const apiKeys = this.storageAdapter.getAllApiKeys();
608
607
  for (const apiKey of apiKeys) {
609
608
  if (!providerBalances[apiKey.baseUrl]) {
@@ -618,57 +617,6 @@ var BalanceManager = class {
618
617
  mintBalances: normalizedMintBalances
619
618
  };
620
619
  }
621
- /**
622
- * Unified refund - handles both NIP-60 and legacy wallet refunds
623
- */
624
- async refund(options) {
625
- const { mintUrl, baseUrl, token: providedToken } = options;
626
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
627
- if (!storedToken) {
628
- console.log("[BalanceManager] No token to refund, returning early");
629
- return { success: true, message: "No API key to refund" };
630
- }
631
- let fetchResult;
632
- try {
633
- fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
634
- if (!fetchResult.success) {
635
- return {
636
- success: false,
637
- message: fetchResult.error || "Refund failed",
638
- requestId: fetchResult.requestId
639
- };
640
- }
641
- if (!fetchResult.token) {
642
- return {
643
- success: false,
644
- message: "No token received from refund",
645
- requestId: fetchResult.requestId
646
- };
647
- }
648
- if (fetchResult.error === "No balance to refund") {
649
- console.log(
650
- "[BalanceManager] No balance to refund, removing stored token"
651
- );
652
- this.storageAdapter.removeToken(baseUrl);
653
- return { success: true, message: "No balance to refund" };
654
- }
655
- const receiveResult = await this.cashuSpender.receiveToken(
656
- fetchResult.token
657
- );
658
- const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
659
- if (!providedToken) {
660
- this.storageAdapter.removeToken(baseUrl);
661
- }
662
- return {
663
- success: receiveResult.success,
664
- refundedAmount: totalAmountMsat,
665
- requestId: fetchResult.requestId
666
- };
667
- } catch (error) {
668
- console.error("[BalanceManager] Refund error", error);
669
- return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
670
- }
671
- }
672
620
  /**
673
621
  * Refund API key balance - convert remaining API key balance to cashu token
674
622
  * @param options - Refund options including forceRefund flag
@@ -804,8 +752,9 @@ var BalanceManager = class {
804
752
  if (!amount || amount <= 0) {
805
753
  return { success: false, message: "Invalid top up amount" };
806
754
  }
807
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
808
- if (!storedToken) {
755
+ const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
756
+ const apiKey = providedToken || apiKeyEntry?.key;
757
+ if (!apiKey) {
809
758
  return { success: false, message: "No API key available for top up" };
810
759
  }
811
760
  let cashuToken = null;
@@ -825,7 +774,7 @@ var BalanceManager = class {
825
774
  cashuToken = tokenResult.token;
826
775
  const topUpResult = await this._postTopUp(
827
776
  baseUrl,
828
- storedToken,
777
+ apiKey,
829
778
  cashuToken
830
779
  );
831
780
  requestId = topUpResult.requestId;
@@ -1040,38 +989,11 @@ var BalanceManager = class {
1040
989
  return candidates;
1041
990
  }
1042
991
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1043
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1044
992
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1045
993
  const forceRefund = retryCount >= 2;
1046
- const toRefund = pendingDistribution.filter(
1047
- (pending) => pending.baseUrl !== baseUrl
1048
- );
1049
994
  const apiKeysToRefund = apiKeyDistribution.filter(
1050
995
  (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1051
996
  );
1052
- const tokenRefundResults = await Promise.allSettled(
1053
- toRefund.map(async (pending) => {
1054
- const token = this.storageAdapter.getToken(pending.baseUrl);
1055
- if (!token) {
1056
- return { baseUrl: pending.baseUrl, success: false };
1057
- }
1058
- const tokenBalance = await this.getTokenBalance(token, pending.baseUrl);
1059
- if (tokenBalance.reserved > 0) {
1060
- return { baseUrl: pending.baseUrl, success: false };
1061
- }
1062
- const result = await this.refund({
1063
- mintUrl,
1064
- baseUrl: pending.baseUrl,
1065
- token
1066
- });
1067
- return { baseUrl: pending.baseUrl, success: result.success };
1068
- })
1069
- );
1070
- for (const result of tokenRefundResults) {
1071
- if (result.status === "fulfilled" && result.value.success) {
1072
- this.storageAdapter.removeToken(result.value.baseUrl);
1073
- }
1074
- }
1075
997
  const apiKeyRefundResults = await Promise.allSettled(
1076
998
  apiKeysToRefund.map(async (apiKeyEntry) => {
1077
999
  const fullApiKeyEntry = this.storageAdapter.getApiKey(
@@ -1095,77 +1017,6 @@ var BalanceManager = class {
1095
1017
  }
1096
1018
  }
1097
1019
  }
1098
- /**
1099
- * Fetch refund token from provider API
1100
- */
1101
- async _fetchRefundToken(baseUrl, storedToken) {
1102
- if (!baseUrl) {
1103
- return {
1104
- success: false,
1105
- error: "No base URL configured"
1106
- };
1107
- }
1108
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1109
- const url = `${normalizedBaseUrl}v1/wallet/refund`;
1110
- const controller = new AbortController();
1111
- const timeoutId = setTimeout(() => {
1112
- controller.abort();
1113
- }, 6e4);
1114
- try {
1115
- const response = await fetch(url, {
1116
- method: "POST",
1117
- headers: {
1118
- Authorization: `Bearer ${storedToken}`,
1119
- "Content-Type": "application/json"
1120
- },
1121
- signal: controller.signal
1122
- });
1123
- clearTimeout(timeoutId);
1124
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
1125
- if (!response.ok) {
1126
- const errorData = await response.json().catch(() => ({}));
1127
- if (response.status === 400 && errorData?.detail === "No balance to refund") {
1128
- this.storageAdapter.removeToken(baseUrl);
1129
- return {
1130
- success: false,
1131
- requestId,
1132
- error: "No balance to refund"
1133
- };
1134
- }
1135
- return {
1136
- success: false,
1137
- requestId,
1138
- error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1139
- };
1140
- }
1141
- const data = await response.json();
1142
- console.log("refund rsule", data);
1143
- return {
1144
- success: true,
1145
- token: data.token,
1146
- requestId
1147
- };
1148
- } catch (error) {
1149
- clearTimeout(timeoutId);
1150
- console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1151
- if (error instanceof Error) {
1152
- if (error.name === "AbortError") {
1153
- return {
1154
- success: false,
1155
- error: "Request timed out after 1 minute"
1156
- };
1157
- }
1158
- return {
1159
- success: false,
1160
- error: error.message
1161
- };
1162
- }
1163
- return {
1164
- success: false,
1165
- error: "Unknown error occurred during refund request"
1166
- };
1167
- }
1168
- }
1169
1020
  /**
1170
1021
  * Post topup request to provider API
1171
1022
  */