@routstr/sdk 0.1.7 → 0.2.0

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.
@@ -2,9 +2,9 @@
2
2
 
3
3
  // core/errors.ts
4
4
  var InsufficientBalanceError = class extends Error {
5
- constructor(required, available, maxMintBalance = 0, maxMintUrl = "") {
5
+ constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
6
6
  super(
7
- `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
7
+ customMessage ?? `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
8
8
  );
9
9
  this.required = required;
10
10
  this.available = available;
@@ -129,6 +129,9 @@ var CashuSpender = class {
129
129
  return result;
130
130
  }
131
131
  async _getBalanceState() {
132
+ if (this.balanceManager) {
133
+ return this.balanceManager.getBalanceState();
134
+ }
132
135
  const mintBalances = await this.walletAdapter.getBalances();
133
136
  const units = this.walletAdapter.getMintUnits();
134
137
  let totalMintBalance = 0;
@@ -144,15 +147,16 @@ var CashuSpender = class {
144
147
  const providerBalances = {};
145
148
  let totalProviderBalance = 0;
146
149
  for (const pending of pendingDistribution) {
147
- providerBalances[pending.baseUrl] = pending.amount;
150
+ providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
148
151
  totalProviderBalance += pending.amount;
149
152
  }
150
153
  const apiKeys = this.storageAdapter.getAllApiKeys();
151
154
  for (const apiKey of apiKeys) {
152
155
  if (!providerBalances[apiKey.baseUrl]) {
153
- providerBalances[apiKey.baseUrl] = apiKey.balance;
154
- totalProviderBalance += apiKey.balance;
156
+ providerBalances[apiKey.baseUrl] = 0;
155
157
  }
158
+ providerBalances[apiKey.baseUrl] += apiKey.balance;
159
+ totalProviderBalance += apiKey.balance;
156
160
  }
157
161
  return {
158
162
  totalBalance: totalMintBalance + totalProviderBalance,
@@ -301,25 +305,12 @@ var CashuSpender = class {
301
305
  `[CashuSpender] _spendInternal: Could not reuse token, will create new token`
302
306
  );
303
307
  }
304
- const balances = await this.walletAdapter.getBalances();
305
- const units = this.walletAdapter.getMintUnits();
306
- let totalBalance = 0;
307
- for (const url in balances) {
308
- const balance = balances[url];
309
- const unit = units[url];
310
- const balanceInSats = getBalanceInSats(balance, unit);
311
- totalBalance += balanceInSats;
312
- }
313
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
314
- const totalPending = pendingDistribution.reduce(
315
- (sum, item) => sum + item.amount,
316
- 0
317
- );
308
+ const balanceState = await this._getBalanceState();
309
+ const totalAvailableBalance = balanceState.totalBalance;
318
310
  this._log(
319
311
  "DEBUG",
320
- `[CashuSpender] _spendInternal: totalBalance=${totalBalance}, totalPending=${totalPending}, adjustedAmount=${adjustedAmount}`
312
+ `[CashuSpender] _spendInternal: totalAvailableBalance=${totalAvailableBalance}, adjustedAmount=${adjustedAmount}`
321
313
  );
322
- const totalAvailableBalance = totalBalance + totalPending;
323
314
  if (totalAvailableBalance < adjustedAmount) {
324
315
  this._log(
325
316
  "ERROR",
@@ -327,8 +318,7 @@ var CashuSpender = class {
327
318
  );
328
319
  return this._createInsufficientBalanceError(
329
320
  adjustedAmount,
330
- balances,
331
- units,
321
+ balanceState.mintBalances,
332
322
  totalAvailableBalance
333
323
  );
334
324
  }
@@ -348,8 +338,7 @@ var CashuSpender = class {
348
338
  if ((tokenResult.error || "").includes("Insufficient balance")) {
349
339
  return this._createInsufficientBalanceError(
350
340
  adjustedAmount,
351
- balances,
352
- units,
341
+ balanceState.mintBalances,
353
342
  totalAvailableBalance
354
343
  );
355
344
  }
@@ -394,6 +383,7 @@ var CashuSpender = class {
394
383
  "DEBUG",
395
384
  `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
396
385
  );
386
+ const units = this.walletAdapter.getMintUnits();
397
387
  return {
398
388
  token,
399
389
  status: "success",
@@ -531,13 +521,11 @@ var CashuSpender = class {
531
521
  /**
532
522
  * Create an insufficient balance error result
533
523
  */
534
- _createInsufficientBalanceError(required, balances, units, availableBalance) {
524
+ _createInsufficientBalanceError(required, normalizedBalances, availableBalance) {
535
525
  let maxBalance = 0;
536
526
  let maxMintUrl = "";
537
- for (const mintUrl in balances) {
538
- const balance = balances[mintUrl];
539
- const unit = units[mintUrl];
540
- const balanceInSats = getBalanceInSats(balance, unit);
527
+ for (const mintUrl in normalizedBalances) {
528
+ const balanceInSats = normalizedBalances[mintUrl];
541
529
  if (balanceInSats > maxBalance) {
542
530
  maxBalance = balanceInSats;
543
531
  maxMintUrl = mintUrl;
@@ -598,6 +586,39 @@ var BalanceManager = class {
598
586
  }
599
587
  }
600
588
  cashuSpender;
589
+ async getBalanceState() {
590
+ const mintBalances = await this.walletAdapter.getBalances();
591
+ const units = this.walletAdapter.getMintUnits();
592
+ let totalMintBalance = 0;
593
+ const normalizedMintBalances = {};
594
+ for (const url in mintBalances) {
595
+ const balance = mintBalances[url];
596
+ const unit = units[url];
597
+ const balanceInSats = getBalanceInSats(balance, unit);
598
+ normalizedMintBalances[url] = balanceInSats;
599
+ totalMintBalance += balanceInSats;
600
+ }
601
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
602
+ const providerBalances = {};
603
+ let totalProviderBalance = 0;
604
+ for (const pending of pendingDistribution) {
605
+ providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
606
+ totalProviderBalance += pending.amount;
607
+ }
608
+ const apiKeys = this.storageAdapter.getAllApiKeys();
609
+ for (const apiKey of apiKeys) {
610
+ if (!providerBalances[apiKey.baseUrl]) {
611
+ providerBalances[apiKey.baseUrl] = 0;
612
+ }
613
+ providerBalances[apiKey.baseUrl] += apiKey.balance;
614
+ totalProviderBalance += apiKey.balance;
615
+ }
616
+ return {
617
+ totalBalance: totalMintBalance + totalProviderBalance,
618
+ providerBalances,
619
+ mintBalances: normalizedMintBalances
620
+ };
621
+ }
601
622
  /**
602
623
  * Unified refund - handles both NIP-60 and legacy wallet refunds
603
624
  */
@@ -808,6 +829,10 @@ var BalanceManager = class {
808
829
  requestId
809
830
  };
810
831
  } catch (error) {
832
+ console.log(
833
+ "DEBUG",
834
+ `[TopuPU] topup: Topup result for ${baseUrl}: error=${error}`
835
+ );
811
836
  if (cashuToken) {
812
837
  await this._recoverFailedTopUp(cashuToken);
813
838
  }
@@ -827,23 +852,42 @@ var BalanceManager = class {
827
852
  if (!adjustedAmount || isNaN(adjustedAmount)) {
828
853
  return { success: false, error: "Invalid top up amount" };
829
854
  }
855
+ const balanceState = await this.getBalanceState();
830
856
  const balances = await this.walletAdapter.getBalances();
831
857
  const units = this.walletAdapter.getMintUnits();
832
- let totalMintBalance = 0;
833
- for (const url in balances) {
834
- const unit = units[url];
835
- const balanceInSats = getBalanceInSats(balances[url], unit);
836
- totalMintBalance += balanceInSats;
837
- }
838
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
839
- const refundablePending = pendingDistribution.filter((entry) => entry.baseUrl !== baseUrl).reduce((sum, entry) => sum + entry.amount, 0);
840
- if (totalMintBalance < adjustedAmount && totalMintBalance + refundablePending >= adjustedAmount && retryCount < 1) {
858
+ const totalMintBalance = Object.values(balanceState.mintBalances).reduce(
859
+ (sum, value) => sum + value,
860
+ 0
861
+ );
862
+ const targetProviderBalance = balanceState.providerBalances[baseUrl] || 0;
863
+ const refundableProviderBalance = Object.entries(
864
+ balanceState.providerBalances
865
+ ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
866
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 1) {
841
867
  await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
842
868
  return this.createProviderToken({
843
869
  ...options,
844
870
  retryCount: retryCount + 1
845
871
  });
846
872
  }
873
+ if (totalMintBalance + targetProviderBalance < adjustedAmount) {
874
+ const error = new InsufficientBalanceError(
875
+ adjustedAmount,
876
+ totalMintBalance + targetProviderBalance,
877
+ totalMintBalance,
878
+ Object.entries(balanceState.mintBalances).reduce(
879
+ (max, [url, balance]) => balance > max.balance ? { url, balance } : max,
880
+ { url: "", balance: 0 }
881
+ ).url
882
+ );
883
+ return { success: false, error: error.message };
884
+ }
885
+ if (targetProviderBalance >= adjustedAmount) {
886
+ return {
887
+ success: true,
888
+ amountSpent: 0
889
+ };
890
+ }
847
891
  const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
848
892
  let requiredAmount = adjustedAmount;
849
893
  const supportedMintsOnly = providerMints.length > 0;
@@ -959,10 +1003,14 @@ var BalanceManager = class {
959
1003
  }
960
1004
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
961
1005
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1006
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
962
1007
  const toRefund = pendingDistribution.filter(
963
1008
  (pending) => pending.baseUrl !== baseUrl
964
1009
  );
965
- const refundResults = await Promise.allSettled(
1010
+ const apiKeysToRefund = apiKeyDistribution.filter(
1011
+ (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1012
+ );
1013
+ const tokenRefundResults = await Promise.allSettled(
966
1014
  toRefund.map(async (pending) => {
967
1015
  const token = this.storageAdapter.getToken(pending.baseUrl);
968
1016
  if (!token) {
@@ -980,11 +1028,32 @@ var BalanceManager = class {
980
1028
  return { baseUrl: pending.baseUrl, success: result.success };
981
1029
  })
982
1030
  );
983
- for (const result of refundResults) {
1031
+ for (const result of tokenRefundResults) {
984
1032
  if (result.status === "fulfilled" && result.value.success) {
985
1033
  this.storageAdapter.removeToken(result.value.baseUrl);
986
1034
  }
987
1035
  }
1036
+ const apiKeyRefundResults = await Promise.allSettled(
1037
+ apiKeysToRefund.map(async (apiKeyEntry) => {
1038
+ const fullApiKeyEntry = this.storageAdapter.getApiKey(
1039
+ apiKeyEntry.baseUrl
1040
+ );
1041
+ if (!fullApiKeyEntry) {
1042
+ return { baseUrl: apiKeyEntry.baseUrl, success: false };
1043
+ }
1044
+ const result = await this.refundApiKey({
1045
+ mintUrl,
1046
+ baseUrl: apiKeyEntry.baseUrl,
1047
+ apiKey: fullApiKeyEntry.key
1048
+ });
1049
+ return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1050
+ })
1051
+ );
1052
+ for (const result of apiKeyRefundResults) {
1053
+ if (result.status === "fulfilled" && result.value.success) {
1054
+ this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
1055
+ }
1056
+ }
988
1057
  }
989
1058
  /**
990
1059
  * Fetch refund token from provider API
@@ -2179,10 +2248,34 @@ var RoutstrClient = class {
2179
2248
  }
2180
2249
  }
2181
2250
  if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
2251
+ this.storageAdapter.getApiKey(baseUrl);
2252
+ let topupAmount = params.requiredSats;
2253
+ try {
2254
+ let currentBalance = 0;
2255
+ if (this.mode === "apikeys") {
2256
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
2257
+ params.token,
2258
+ baseUrl
2259
+ );
2260
+ currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
2261
+ } else if (this.mode === "lazyrefund") {
2262
+ const distribution = this.storageAdapter.getCachedTokenDistribution();
2263
+ const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
2264
+ currentBalance = tokenEntry?.amount ?? 0;
2265
+ }
2266
+ const shortfall = Math.max(0, params.requiredSats - currentBalance);
2267
+ topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
2268
+ } catch (e) {
2269
+ this._log(
2270
+ "WARN",
2271
+ "Could not get current token balance for topup calculation:",
2272
+ e
2273
+ );
2274
+ }
2182
2275
  const topupResult = await this.balanceManager.topUp({
2183
2276
  mintUrl,
2184
2277
  baseUrl,
2185
- amount: params.requiredSats * TOPUP_MARGIN,
2278
+ amount: topupAmount * TOPUP_MARGIN,
2186
2279
  token: params.token
2187
2280
  });
2188
2281
  this._log(
@@ -2200,7 +2293,7 @@ var RoutstrClient = class {
2200
2293
  "DEBUG",
2201
2294
  `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
2202
2295
  );
2203
- throw new InsufficientBalanceError(required, available);
2296
+ throw new InsufficientBalanceError(required, available, 0, "", message);
2204
2297
  } else {
2205
2298
  this._log(
2206
2299
  "DEBUG",
@@ -2263,7 +2356,10 @@ var RoutstrClient = class {
2263
2356
  retryToken = latestBalanceInfo.apiKey;
2264
2357
  }
2265
2358
  if (latestTokenBalance >= 0) {
2266
- this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
2359
+ this.storageAdapter.updateApiKeyBalance(
2360
+ baseUrl,
2361
+ latestTokenBalance
2362
+ );
2267
2363
  }
2268
2364
  }
2269
2365
  } catch (error) {