@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.
@@ -120,13 +120,8 @@ var CashuSpender = class {
120
120
  normalizedMintBalances[url] = balanceInSats;
121
121
  totalMintBalance += balanceInSats;
122
122
  }
123
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
124
123
  const providerBalances = {};
125
124
  let totalProviderBalance = 0;
126
- for (const pending of pendingDistribution) {
127
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
128
- totalProviderBalance += pending.amount;
129
- }
130
125
  const apiKeys = this.storageAdapter.getAllApiKeys();
131
126
  for (const apiKey of apiKeys) {
132
127
  if (!providerBalances[apiKey.baseUrl]) {
@@ -347,27 +342,11 @@ var CashuSpender = class {
347
342
  };
348
343
  }
349
344
  }
350
- if (token && baseUrl) {
351
- try {
352
- this.storageAdapter.setToken(baseUrl, token);
353
- } catch (error) {
354
- if (error instanceof Error && error.message.includes("Token already exists")) {
355
- this._log(
356
- "DEBUG",
357
- `[CashuSpender] _spendInternal: Token already exists for ${baseUrl}, receiving newly created token and using existing`
358
- );
359
- const receiveResult = await this.receiveToken(token);
360
- if (receiveResult.success) {
361
- this._log(
362
- "DEBUG",
363
- `[CashuSpender] _spendInternal: Token restored successfully, amount=${receiveResult.amount}`
364
- );
365
- }
366
- token = this.storageAdapter.getToken(baseUrl);
367
- } else {
368
- throw error;
369
- }
370
- }
345
+ if (token) {
346
+ this._log(
347
+ "DEBUG",
348
+ `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
349
+ );
371
350
  }
372
351
  this._logTransaction("spend", {
373
352
  amount: spentAmount,
@@ -388,19 +367,19 @@ var CashuSpender = class {
388
367
  };
389
368
  }
390
369
  /**
391
- * Try to reuse an existing token
370
+ * Try to reuse an existing API key
392
371
  */
393
372
  async _tryReuseToken(baseUrl, amount, mintUrl) {
394
- const storedToken = this.storageAdapter.getToken(baseUrl);
395
- if (!storedToken) return null;
396
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
397
- const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
398
- this._log("DEBUG", "RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
373
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
374
+ if (!apiKeyEntry) return null;
375
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
376
+ const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
377
+ this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
399
378
  if (balanceForBaseUrl > amount) {
400
379
  const units = this.walletAdapter.getMintUnits();
401
380
  const unit = units[mintUrl] || "sat";
402
381
  return {
403
- token: storedToken,
382
+ token: apiKeyEntry.key,
404
383
  status: "success",
405
384
  balance: balanceForBaseUrl,
406
385
  unit
@@ -411,7 +390,8 @@ var CashuSpender = class {
411
390
  const topUpResult = await this.balanceManager.topUp({
412
391
  mintUrl,
413
392
  baseUrl,
414
- amount: topUpAmount
393
+ amount: topUpAmount,
394
+ token: apiKeyEntry.key
415
395
  });
416
396
  this._log("DEBUG", "TOPUP ", topUpResult);
417
397
  if (topUpResult.success && topUpResult.toppedUpAmount) {
@@ -425,7 +405,7 @@ var CashuSpender = class {
425
405
  status: "success"
426
406
  });
427
407
  return {
428
- token: storedToken,
408
+ token: apiKeyEntry.key,
429
409
  status: "success",
430
410
  balance: newBalance,
431
411
  unit
@@ -433,84 +413,131 @@ var CashuSpender = class {
433
413
  }
434
414
  const providerBalance = await this._getProviderTokenBalance(
435
415
  baseUrl,
436
- storedToken
416
+ apiKeyEntry.key
437
417
  );
438
418
  this._log("DEBUG", providerBalance);
439
419
  if (providerBalance <= 0) {
440
- this.storageAdapter.removeToken(baseUrl);
420
+ this.storageAdapter.removeApiKey(baseUrl);
441
421
  }
442
422
  }
443
423
  return null;
444
424
  }
445
425
  /**
446
- * Refund specific providers without retrying spend
426
+ * Refund all xcashu tokens from storage by calling the provider's refund endpoint.
427
+ * The xcashu token acts as an API key to claim the refund, and the response contains
428
+ * the actual refunded Cashu token which is then received into the wallet.
429
+ * @param mintUrl - The mint URL for receiving tokens
430
+ * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
431
+ * @returns Results for each xcashu token refund attempt
447
432
  */
448
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
433
+ async refundXcashuTokens(mintUrl, excludeBaseUrls) {
449
434
  const results = [];
450
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
451
- const toRefund = pendingDistribution.filter(
452
- (p) => baseUrls.includes(p.baseUrl)
453
- );
454
- const refundResults = await Promise.allSettled(
455
- toRefund.map(async (pending) => {
456
- const token = this.storageAdapter.getToken(pending.baseUrl);
457
- this._log("DEBUG", token, this.balanceManager);
458
- if (!token || !this.balanceManager) {
459
- return { baseUrl: pending.baseUrl, success: false };
460
- }
461
- const tokenBalance = await this.balanceManager.getTokenBalance(
462
- token,
463
- pending.baseUrl
464
- );
465
- if (tokenBalance.reserved > 0) {
466
- return { baseUrl: pending.baseUrl, success: false };
467
- }
468
- const result = await this.balanceManager.refund({
469
- mintUrl,
470
- baseUrl: pending.baseUrl,
471
- token
472
- });
473
- this._log("DEBUG", result);
474
- if (result.success) {
475
- this.storageAdapter.removeToken(pending.baseUrl);
476
- }
477
- return { baseUrl: pending.baseUrl, success: result.success };
478
- })
479
- );
480
- results.push(
481
- ...refundResults.map(
482
- (r) => r.status === "fulfilled" ? r.value : { baseUrl: "", success: false }
483
- )
484
- );
485
- if (refundApiKeys) {
486
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
487
- const apiKeysToRefund = apiKeyDistribution.filter(
488
- (p) => baseUrls.includes(p.baseUrl)
489
- );
490
- for (const apiKeyEntry of apiKeysToRefund) {
491
- const apiKeyEntryFull = this.storageAdapter.getApiKey(
492
- apiKeyEntry.baseUrl
493
- );
494
- if (apiKeyEntryFull && this.balanceManager) {
495
- const refundResult = await this.balanceManager.refundApiKey({
496
- mintUrl,
497
- baseUrl: apiKeyEntry.baseUrl,
498
- apiKey: apiKeyEntryFull.key,
499
- forceRefund
500
- });
501
- if (refundResult.success) {
502
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
435
+ const xcashuTokens = this.storageAdapter.getXcashuTokens();
436
+ const excludedUrls = new Set(excludeBaseUrls || []);
437
+ for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
438
+ if (excludedUrls.has(baseUrl)) continue;
439
+ for (const xcashuToken of tokens) {
440
+ try {
441
+ if (!this.balanceManager) {
442
+ throw new Error("BalanceManager not available for xcashu refund");
443
+ }
444
+ const fetchResult = await this.balanceManager.fetchRefundToken(
445
+ baseUrl,
446
+ xcashuToken.token,
447
+ true
448
+ );
449
+ if (!fetchResult.success || !fetchResult.token) {
450
+ throw new Error(
451
+ fetchResult.error || "Failed to fetch refund token from provider"
452
+ );
453
+ }
454
+ const receiveResult = await this.receiveToken(fetchResult.token);
455
+ if (receiveResult.success) {
456
+ this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
457
+ results.push({
458
+ baseUrl,
459
+ token: xcashuToken.token,
460
+ success: true
461
+ });
462
+ this._log(
463
+ "DEBUG",
464
+ `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
465
+ );
466
+ } else {
467
+ const currentTryCount = xcashuToken.tryCount ?? 0;
468
+ const newTryCount = currentTryCount + 1;
469
+ this.storageAdapter.updateXcashuTokenTryCount(
470
+ xcashuToken.token,
471
+ newTryCount
472
+ );
473
+ results.push({
474
+ baseUrl,
475
+ token: xcashuToken.token,
476
+ success: false,
477
+ error: receiveResult.message ?? "Refund failed"
478
+ });
479
+ this._log(
480
+ "DEBUG",
481
+ `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}`
482
+ );
503
483
  }
484
+ } catch (error) {
485
+ const currentTryCount = xcashuToken.tryCount ?? 0;
486
+ const newTryCount = currentTryCount + 1;
487
+ this.storageAdapter.updateXcashuTokenTryCount(
488
+ xcashuToken.token,
489
+ newTryCount
490
+ );
491
+ const errorMessage = error instanceof Error ? error.message : String(error);
504
492
  results.push({
505
- baseUrl: apiKeyEntry.baseUrl,
506
- success: refundResult.success
493
+ baseUrl,
494
+ token: xcashuToken.token,
495
+ success: false,
496
+ error: errorMessage
507
497
  });
498
+ this._log(
499
+ "ERROR",
500
+ `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
501
+ );
502
+ }
503
+ }
504
+ }
505
+ return results;
506
+ }
507
+ /**
508
+ * Refund specific providers without retrying spend
509
+ */
510
+ async refundProviders(mintUrl, forceRefund) {
511
+ const results = [];
512
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
513
+ for (const apiKeyEntry of apiKeyDistribution) {
514
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
515
+ apiKeyEntry.baseUrl
516
+ );
517
+ if (apiKeyEntryFull && this.balanceManager) {
518
+ const refundResult = await this.balanceManager.refundApiKey({
519
+ mintUrl,
520
+ baseUrl: apiKeyEntry.baseUrl,
521
+ apiKey: apiKeyEntryFull.key,
522
+ forceRefund
523
+ });
524
+ if (refundResult.success) {
525
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
508
526
  } else {
509
- results.push({
510
- baseUrl: apiKeyEntry.baseUrl,
511
- success: false
512
- });
527
+ this.storageAdapter.updateApiKeyBalance(
528
+ apiKeyEntry.baseUrl,
529
+ apiKeyEntry.amount
530
+ );
513
531
  }
532
+ results.push({
533
+ baseUrl: apiKeyEntry.baseUrl,
534
+ success: refundResult.success
535
+ });
536
+ } else {
537
+ results.push({
538
+ baseUrl: apiKeyEntry.baseUrl,
539
+ success: false
540
+ });
514
541
  }
515
542
  }
516
543
  return results;
@@ -595,13 +622,8 @@ var BalanceManager = class {
595
622
  normalizedMintBalances[url] = balanceInSats;
596
623
  totalMintBalance += balanceInSats;
597
624
  }
598
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
599
625
  const providerBalances = {};
600
626
  let totalProviderBalance = 0;
601
- for (const pending of pendingDistribution) {
602
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
603
- totalProviderBalance += pending.amount;
604
- }
605
627
  const apiKeys = this.storageAdapter.getAllApiKeys();
606
628
  for (const apiKey of apiKeys) {
607
629
  if (!providerBalances[apiKey.baseUrl]) {
@@ -616,57 +638,6 @@ var BalanceManager = class {
616
638
  mintBalances: normalizedMintBalances
617
639
  };
618
640
  }
619
- /**
620
- * Unified refund - handles both NIP-60 and legacy wallet refunds
621
- */
622
- async refund(options) {
623
- const { mintUrl, baseUrl, token: providedToken } = options;
624
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
625
- if (!storedToken) {
626
- console.log("[BalanceManager] No token to refund, returning early");
627
- return { success: true, message: "No API key to refund" };
628
- }
629
- let fetchResult;
630
- try {
631
- fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
632
- if (!fetchResult.success) {
633
- return {
634
- success: false,
635
- message: fetchResult.error || "Refund failed",
636
- requestId: fetchResult.requestId
637
- };
638
- }
639
- if (!fetchResult.token) {
640
- return {
641
- success: false,
642
- message: "No token received from refund",
643
- requestId: fetchResult.requestId
644
- };
645
- }
646
- if (fetchResult.error === "No balance to refund") {
647
- console.log(
648
- "[BalanceManager] No balance to refund, removing stored token"
649
- );
650
- this.storageAdapter.removeToken(baseUrl);
651
- return { success: true, message: "No balance to refund" };
652
- }
653
- const receiveResult = await this.cashuSpender.receiveToken(
654
- fetchResult.token
655
- );
656
- const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
657
- if (!providedToken) {
658
- this.storageAdapter.removeToken(baseUrl);
659
- }
660
- return {
661
- success: receiveResult.success,
662
- refundedAmount: totalAmountMsat,
663
- requestId: fetchResult.requestId
664
- };
665
- } catch (error) {
666
- console.error("[BalanceManager] Refund error", error);
667
- return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
668
- }
669
- }
670
641
  /**
671
642
  * Refund API key balance - convert remaining API key balance to cashu token
672
643
  * @param options - Refund options including forceRefund flag
@@ -694,7 +665,7 @@ var BalanceManager = class {
694
665
  }
695
666
  let fetchResult;
696
667
  try {
697
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
668
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
698
669
  if (!fetchResult.success) {
699
670
  return {
700
671
  success: false,
@@ -730,9 +701,9 @@ var BalanceManager = class {
730
701
  }
731
702
  }
732
703
  /**
733
- * Fetch refund token from provider API using API key authentication
704
+ * Fetch refund token from provider API using API key (or xcashu token) authentication
734
705
  */
735
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
706
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
736
707
  if (!baseUrl) {
737
708
  return {
738
709
  success: false,
@@ -746,12 +717,17 @@ var BalanceManager = class {
746
717
  controller.abort();
747
718
  }, 6e4);
748
719
  try {
720
+ const headers = {
721
+ "Content-Type": "application/json"
722
+ };
723
+ if (xCashu) {
724
+ headers["X-Cashu"] = apiKeyOrToken;
725
+ } else {
726
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
727
+ }
749
728
  const response = await fetch(url, {
750
729
  method: "POST",
751
- headers: {
752
- Authorization: `Bearer ${apiKey}`,
753
- "Content-Type": "application/json"
754
- },
730
+ headers,
755
731
  signal: controller.signal
756
732
  });
757
733
  clearTimeout(timeoutId);
@@ -772,10 +748,7 @@ var BalanceManager = class {
772
748
  };
773
749
  } catch (error) {
774
750
  clearTimeout(timeoutId);
775
- console.error(
776
- "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
777
- error
778
- );
751
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
779
752
  if (error instanceof Error) {
780
753
  if (error.name === "AbortError") {
781
754
  return {
@@ -802,8 +775,9 @@ var BalanceManager = class {
802
775
  if (!amount || amount <= 0) {
803
776
  return { success: false, message: "Invalid top up amount" };
804
777
  }
805
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
806
- if (!storedToken) {
778
+ const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
779
+ const apiKey = providedToken || apiKeyEntry?.key;
780
+ if (!apiKey) {
807
781
  return { success: false, message: "No API key available for top up" };
808
782
  }
809
783
  let cashuToken = null;
@@ -821,11 +795,7 @@ var BalanceManager = class {
821
795
  };
822
796
  }
823
797
  cashuToken = tokenResult.token;
824
- const topUpResult = await this._postTopUp(
825
- baseUrl,
826
- storedToken,
827
- cashuToken
828
- );
798
+ const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
829
799
  requestId = topUpResult.requestId;
830
800
  console.log(topUpResult);
831
801
  if (!topUpResult.success) {
@@ -1038,38 +1008,11 @@ var BalanceManager = class {
1038
1008
  return candidates;
1039
1009
  }
1040
1010
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1041
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1042
1011
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1043
1012
  const forceRefund = retryCount >= 2;
1044
- const toRefund = pendingDistribution.filter(
1045
- (pending) => pending.baseUrl !== baseUrl
1046
- );
1047
1013
  const apiKeysToRefund = apiKeyDistribution.filter(
1048
1014
  (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1049
1015
  );
1050
- const tokenRefundResults = await Promise.allSettled(
1051
- toRefund.map(async (pending) => {
1052
- const token = this.storageAdapter.getToken(pending.baseUrl);
1053
- if (!token) {
1054
- return { baseUrl: pending.baseUrl, success: false };
1055
- }
1056
- const tokenBalance = await this.getTokenBalance(token, pending.baseUrl);
1057
- if (tokenBalance.reserved > 0) {
1058
- return { baseUrl: pending.baseUrl, success: false };
1059
- }
1060
- const result = await this.refund({
1061
- mintUrl,
1062
- baseUrl: pending.baseUrl,
1063
- token
1064
- });
1065
- return { baseUrl: pending.baseUrl, success: result.success };
1066
- })
1067
- );
1068
- for (const result of tokenRefundResults) {
1069
- if (result.status === "fulfilled" && result.value.success) {
1070
- this.storageAdapter.removeToken(result.value.baseUrl);
1071
- }
1072
- }
1073
1016
  const apiKeyRefundResults = await Promise.allSettled(
1074
1017
  apiKeysToRefund.map(async (apiKeyEntry) => {
1075
1018
  const fullApiKeyEntry = this.storageAdapter.getApiKey(
@@ -1093,77 +1036,6 @@ var BalanceManager = class {
1093
1036
  }
1094
1037
  }
1095
1038
  }
1096
- /**
1097
- * Fetch refund token from provider API
1098
- */
1099
- async _fetchRefundToken(baseUrl, storedToken) {
1100
- if (!baseUrl) {
1101
- return {
1102
- success: false,
1103
- error: "No base URL configured"
1104
- };
1105
- }
1106
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1107
- const url = `${normalizedBaseUrl}v1/wallet/refund`;
1108
- const controller = new AbortController();
1109
- const timeoutId = setTimeout(() => {
1110
- controller.abort();
1111
- }, 6e4);
1112
- try {
1113
- const response = await fetch(url, {
1114
- method: "POST",
1115
- headers: {
1116
- Authorization: `Bearer ${storedToken}`,
1117
- "Content-Type": "application/json"
1118
- },
1119
- signal: controller.signal
1120
- });
1121
- clearTimeout(timeoutId);
1122
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
1123
- if (!response.ok) {
1124
- const errorData = await response.json().catch(() => ({}));
1125
- if (response.status === 400 && errorData?.detail === "No balance to refund") {
1126
- this.storageAdapter.removeToken(baseUrl);
1127
- return {
1128
- success: false,
1129
- requestId,
1130
- error: "No balance to refund"
1131
- };
1132
- }
1133
- return {
1134
- success: false,
1135
- requestId,
1136
- error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1137
- };
1138
- }
1139
- const data = await response.json();
1140
- console.log("refund rsule", data);
1141
- return {
1142
- success: true,
1143
- token: data.token,
1144
- requestId
1145
- };
1146
- } catch (error) {
1147
- clearTimeout(timeoutId);
1148
- console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1149
- if (error instanceof Error) {
1150
- if (error.name === "AbortError") {
1151
- return {
1152
- success: false,
1153
- error: "Request timed out after 1 minute"
1154
- };
1155
- }
1156
- return {
1157
- success: false,
1158
- error: error.message
1159
- };
1160
- }
1161
- return {
1162
- success: false,
1163
- error: "Unknown error occurred during refund request"
1164
- };
1165
- }
1166
- }
1167
1039
  /**
1168
1040
  * Post topup request to provider API
1169
1041
  */
@@ -1289,7 +1161,7 @@ var BalanceManager = class {
1289
1161
  console.log(response.status);
1290
1162
  const data = await response.json();
1291
1163
  console.log("FAILED ", data);
1292
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1164
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1293
1165
  return {
1294
1166
  amount: -1,
1295
1167
  reserved: data.reserved ?? 0,