@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.
@@ -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,131 @@ 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 by calling the provider's refund endpoint.
429
+ * The xcashu token acts as an API key to claim the refund, and the response contains
430
+ * the actual refunded Cashu token which is then received into the wallet.
431
+ * @param mintUrl - The mint URL for receiving tokens
432
+ * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
433
+ * @returns Results for each xcashu token refund attempt
449
434
  */
450
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
435
+ async refundXcashuTokens(mintUrl, excludeBaseUrls) {
451
436
  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);
437
+ const xcashuTokens = this.storageAdapter.getXcashuTokens();
438
+ const excludedUrls = new Set(excludeBaseUrls || []);
439
+ for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
440
+ if (excludedUrls.has(baseUrl)) continue;
441
+ for (const xcashuToken of tokens) {
442
+ try {
443
+ if (!this.balanceManager) {
444
+ throw new Error("BalanceManager not available for xcashu refund");
445
+ }
446
+ const fetchResult = await this.balanceManager.fetchRefundToken(
447
+ baseUrl,
448
+ xcashuToken.token,
449
+ true
450
+ );
451
+ if (!fetchResult.success || !fetchResult.token) {
452
+ throw new Error(
453
+ fetchResult.error || "Failed to fetch refund token from provider"
454
+ );
455
+ }
456
+ const receiveResult = await this.receiveToken(fetchResult.token);
457
+ if (receiveResult.success) {
458
+ this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
459
+ results.push({
460
+ baseUrl,
461
+ token: xcashuToken.token,
462
+ success: true
463
+ });
464
+ this._log(
465
+ "DEBUG",
466
+ `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
467
+ );
468
+ } else {
469
+ const currentTryCount = xcashuToken.tryCount ?? 0;
470
+ const newTryCount = currentTryCount + 1;
471
+ this.storageAdapter.updateXcashuTokenTryCount(
472
+ xcashuToken.token,
473
+ newTryCount
474
+ );
475
+ results.push({
476
+ baseUrl,
477
+ token: xcashuToken.token,
478
+ success: false,
479
+ error: receiveResult.message ?? "Refund failed"
480
+ });
481
+ this._log(
482
+ "DEBUG",
483
+ `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}`
484
+ );
505
485
  }
486
+ } catch (error) {
487
+ const currentTryCount = xcashuToken.tryCount ?? 0;
488
+ const newTryCount = currentTryCount + 1;
489
+ this.storageAdapter.updateXcashuTokenTryCount(
490
+ xcashuToken.token,
491
+ newTryCount
492
+ );
493
+ const errorMessage = error instanceof Error ? error.message : String(error);
506
494
  results.push({
507
- baseUrl: apiKeyEntry.baseUrl,
508
- success: refundResult.success
495
+ baseUrl,
496
+ token: xcashuToken.token,
497
+ success: false,
498
+ error: errorMessage
509
499
  });
500
+ this._log(
501
+ "ERROR",
502
+ `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
503
+ );
504
+ }
505
+ }
506
+ }
507
+ return results;
508
+ }
509
+ /**
510
+ * Refund specific providers without retrying spend
511
+ */
512
+ async refundProviders(mintUrl, forceRefund) {
513
+ const results = [];
514
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
515
+ for (const apiKeyEntry of apiKeyDistribution) {
516
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
517
+ apiKeyEntry.baseUrl
518
+ );
519
+ if (apiKeyEntryFull && this.balanceManager) {
520
+ const refundResult = await this.balanceManager.refundApiKey({
521
+ mintUrl,
522
+ baseUrl: apiKeyEntry.baseUrl,
523
+ apiKey: apiKeyEntryFull.key,
524
+ forceRefund
525
+ });
526
+ if (refundResult.success) {
527
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
510
528
  } else {
511
- results.push({
512
- baseUrl: apiKeyEntry.baseUrl,
513
- success: false
514
- });
529
+ this.storageAdapter.updateApiKeyBalance(
530
+ apiKeyEntry.baseUrl,
531
+ apiKeyEntry.amount
532
+ );
515
533
  }
534
+ results.push({
535
+ baseUrl: apiKeyEntry.baseUrl,
536
+ success: refundResult.success
537
+ });
538
+ } else {
539
+ results.push({
540
+ baseUrl: apiKeyEntry.baseUrl,
541
+ success: false
542
+ });
516
543
  }
517
544
  }
518
545
  return results;
@@ -597,13 +624,8 @@ var BalanceManager = class {
597
624
  normalizedMintBalances[url] = balanceInSats;
598
625
  totalMintBalance += balanceInSats;
599
626
  }
600
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
601
627
  const providerBalances = {};
602
628
  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
629
  const apiKeys = this.storageAdapter.getAllApiKeys();
608
630
  for (const apiKey of apiKeys) {
609
631
  if (!providerBalances[apiKey.baseUrl]) {
@@ -618,57 +640,6 @@ var BalanceManager = class {
618
640
  mintBalances: normalizedMintBalances
619
641
  };
620
642
  }
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
643
  /**
673
644
  * Refund API key balance - convert remaining API key balance to cashu token
674
645
  * @param options - Refund options including forceRefund flag
@@ -696,7 +667,7 @@ var BalanceManager = class {
696
667
  }
697
668
  let fetchResult;
698
669
  try {
699
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
670
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
700
671
  if (!fetchResult.success) {
701
672
  return {
702
673
  success: false,
@@ -732,9 +703,9 @@ var BalanceManager = class {
732
703
  }
733
704
  }
734
705
  /**
735
- * Fetch refund token from provider API using API key authentication
706
+ * Fetch refund token from provider API using API key (or xcashu token) authentication
736
707
  */
737
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
708
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
738
709
  if (!baseUrl) {
739
710
  return {
740
711
  success: false,
@@ -748,12 +719,17 @@ var BalanceManager = class {
748
719
  controller.abort();
749
720
  }, 6e4);
750
721
  try {
722
+ const headers = {
723
+ "Content-Type": "application/json"
724
+ };
725
+ if (xCashu) {
726
+ headers["X-Cashu"] = apiKeyOrToken;
727
+ } else {
728
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
729
+ }
751
730
  const response = await fetch(url, {
752
731
  method: "POST",
753
- headers: {
754
- Authorization: `Bearer ${apiKey}`,
755
- "Content-Type": "application/json"
756
- },
732
+ headers,
757
733
  signal: controller.signal
758
734
  });
759
735
  clearTimeout(timeoutId);
@@ -774,10 +750,7 @@ var BalanceManager = class {
774
750
  };
775
751
  } catch (error) {
776
752
  clearTimeout(timeoutId);
777
- console.error(
778
- "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
779
- error
780
- );
753
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
781
754
  if (error instanceof Error) {
782
755
  if (error.name === "AbortError") {
783
756
  return {
@@ -804,8 +777,9 @@ var BalanceManager = class {
804
777
  if (!amount || amount <= 0) {
805
778
  return { success: false, message: "Invalid top up amount" };
806
779
  }
807
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
808
- if (!storedToken) {
780
+ const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
781
+ const apiKey = providedToken || apiKeyEntry?.key;
782
+ if (!apiKey) {
809
783
  return { success: false, message: "No API key available for top up" };
810
784
  }
811
785
  let cashuToken = null;
@@ -823,11 +797,7 @@ var BalanceManager = class {
823
797
  };
824
798
  }
825
799
  cashuToken = tokenResult.token;
826
- const topUpResult = await this._postTopUp(
827
- baseUrl,
828
- storedToken,
829
- cashuToken
830
- );
800
+ const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
831
801
  requestId = topUpResult.requestId;
832
802
  console.log(topUpResult);
833
803
  if (!topUpResult.success) {
@@ -1040,38 +1010,11 @@ var BalanceManager = class {
1040
1010
  return candidates;
1041
1011
  }
1042
1012
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1043
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1044
1013
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1045
1014
  const forceRefund = retryCount >= 2;
1046
- const toRefund = pendingDistribution.filter(
1047
- (pending) => pending.baseUrl !== baseUrl
1048
- );
1049
1015
  const apiKeysToRefund = apiKeyDistribution.filter(
1050
1016
  (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1051
1017
  );
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
1018
  const apiKeyRefundResults = await Promise.allSettled(
1076
1019
  apiKeysToRefund.map(async (apiKeyEntry) => {
1077
1020
  const fullApiKeyEntry = this.storageAdapter.getApiKey(
@@ -1095,77 +1038,6 @@ var BalanceManager = class {
1095
1038
  }
1096
1039
  }
1097
1040
  }
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
1041
  /**
1170
1042
  * Post topup request to provider API
1171
1043
  */
@@ -1291,7 +1163,7 @@ var BalanceManager = class {
1291
1163
  console.log(response.status);
1292
1164
  const data = await response.json();
1293
1165
  console.log("FAILED ", data);
1294
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1166
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1295
1167
  return {
1296
1168
  amount: -1,
1297
1169
  reserved: data.reserved ?? 0,