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