@routstr/sdk 0.1.6 → 0.1.8

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.
@@ -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
  */
@@ -825,17 +846,17 @@ var BalanceManager = class {
825
846
  if (!adjustedAmount || isNaN(adjustedAmount)) {
826
847
  return { success: false, error: "Invalid top up amount" };
827
848
  }
849
+ const balanceState = await this.getBalanceState();
828
850
  const balances = await this.walletAdapter.getBalances();
829
851
  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) {
852
+ const totalMintBalance = Object.values(balanceState.mintBalances).reduce(
853
+ (sum, value) => sum + value,
854
+ 0
855
+ );
856
+ const refundableProviderBalance = Object.entries(
857
+ balanceState.providerBalances
858
+ ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
859
+ if (totalMintBalance < adjustedAmount && totalMintBalance + refundableProviderBalance >= adjustedAmount && retryCount < 1) {
839
860
  await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
840
861
  return this.createProviderToken({
841
862
  ...options,
@@ -957,10 +978,14 @@ var BalanceManager = class {
957
978
  }
958
979
  async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
959
980
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
981
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
960
982
  const toRefund = pendingDistribution.filter(
961
983
  (pending) => pending.baseUrl !== baseUrl
962
984
  );
963
- const refundResults = await Promise.allSettled(
985
+ const apiKeysToRefund = apiKeyDistribution.filter(
986
+ (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
987
+ );
988
+ const tokenRefundResults = await Promise.allSettled(
964
989
  toRefund.map(async (pending) => {
965
990
  const token = this.storageAdapter.getToken(pending.baseUrl);
966
991
  if (!token) {
@@ -978,11 +1003,32 @@ var BalanceManager = class {
978
1003
  return { baseUrl: pending.baseUrl, success: result.success };
979
1004
  })
980
1005
  );
981
- for (const result of refundResults) {
1006
+ for (const result of tokenRefundResults) {
982
1007
  if (result.status === "fulfilled" && result.value.success) {
983
1008
  this.storageAdapter.removeToken(result.value.baseUrl);
984
1009
  }
985
1010
  }
1011
+ const apiKeyRefundResults = await Promise.allSettled(
1012
+ apiKeysToRefund.map(async (apiKeyEntry) => {
1013
+ const fullApiKeyEntry = this.storageAdapter.getApiKey(
1014
+ apiKeyEntry.baseUrl
1015
+ );
1016
+ if (!fullApiKeyEntry) {
1017
+ return { baseUrl: apiKeyEntry.baseUrl, success: false };
1018
+ }
1019
+ const result = await this.refundApiKey({
1020
+ mintUrl,
1021
+ baseUrl: apiKeyEntry.baseUrl,
1022
+ apiKey: fullApiKeyEntry.key
1023
+ });
1024
+ return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1025
+ })
1026
+ );
1027
+ for (const result of apiKeyRefundResults) {
1028
+ if (result.status === "fulfilled" && result.value.success) {
1029
+ this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
1030
+ }
1031
+ }
986
1032
  }
987
1033
  /**
988
1034
  * Fetch refund token from provider API
@@ -1181,6 +1227,14 @@ var BalanceManager = class {
1181
1227
  console.log(response.status);
1182
1228
  const data = await response.json();
1183
1229
  console.log("FAILED ", data);
1230
+ const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
1231
+ return {
1232
+ amount: -1,
1233
+ reserved: data.reserved ?? 0,
1234
+ unit: "msat",
1235
+ apiKey: data.api_key,
1236
+ isInvalidApiKey
1237
+ };
1184
1238
  }
1185
1239
  } catch (error) {
1186
1240
  console.error("ERRORR IN RESTPONSE", error);
@@ -2073,7 +2127,8 @@ var RoutstrClient = class {
2073
2127
  response.status,
2074
2128
  requestId,
2075
2129
  this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
2076
- bodyText
2130
+ bodyText,
2131
+ params.retryCount ?? 0
2077
2132
  );
2078
2133
  }
2079
2134
  return response;
@@ -2082,8 +2137,12 @@ var RoutstrClient = class {
2082
2137
  return await this._handleErrorResponse(
2083
2138
  params,
2084
2139
  token,
2085
- -1
2140
+ -1,
2086
2141
  // just for Network Error to skip all statuses
2142
+ void 0,
2143
+ void 0,
2144
+ void 0,
2145
+ params.retryCount ?? 0
2087
2146
  );
2088
2147
  }
2089
2148
  throw error;
@@ -2092,7 +2151,8 @@ var RoutstrClient = class {
2092
2151
  /**
2093
2152
  * Handle error responses with failover
2094
2153
  */
2095
- async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody) {
2154
+ async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
2155
+ const MAX_RETRIES_PER_PROVIDER = 2;
2096
2156
  const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
2097
2157
  let tryNextProvider = false;
2098
2158
  this._log(
@@ -2163,10 +2223,34 @@ var RoutstrClient = class {
2163
2223
  }
2164
2224
  }
2165
2225
  if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
2226
+ this.storageAdapter.getApiKey(baseUrl);
2227
+ let topupAmount = params.requiredSats;
2228
+ try {
2229
+ let currentBalance = 0;
2230
+ if (this.mode === "apikeys") {
2231
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
2232
+ params.token,
2233
+ baseUrl
2234
+ );
2235
+ currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
2236
+ } else if (this.mode === "lazyrefund") {
2237
+ const distribution = this.storageAdapter.getCachedTokenDistribution();
2238
+ const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
2239
+ currentBalance = tokenEntry?.amount ?? 0;
2240
+ }
2241
+ const shortfall = Math.max(0, params.requiredSats - currentBalance);
2242
+ topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
2243
+ } catch (e) {
2244
+ this._log(
2245
+ "WARN",
2246
+ "Could not get current token balance for topup calculation:",
2247
+ e
2248
+ );
2249
+ }
2166
2250
  const topupResult = await this.balanceManager.topUp({
2167
2251
  mintUrl,
2168
2252
  baseUrl,
2169
- amount: params.requiredSats * TOPUP_MARGIN,
2253
+ amount: topupAmount * TOPUP_MARGIN,
2170
2254
  token: params.token
2171
2255
  });
2172
2256
  this._log(
@@ -2198,12 +2282,26 @@ var RoutstrClient = class {
2198
2282
  `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
2199
2283
  );
2200
2284
  }
2201
- if (!tryNextProvider)
2202
- return this._makeRequest({
2203
- ...params,
2204
- token: params.token,
2205
- headers: this._withAuthHeader(params.baseHeaders, params.token)
2206
- });
2285
+ if (!tryNextProvider) {
2286
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
2287
+ this._log(
2288
+ "DEBUG",
2289
+ `[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
2290
+ );
2291
+ return this._makeRequest({
2292
+ ...params,
2293
+ token: params.token,
2294
+ headers: this._withAuthHeader(params.baseHeaders, params.token),
2295
+ retryCount: retryCount + 1
2296
+ });
2297
+ } else {
2298
+ this._log(
2299
+ "DEBUG",
2300
+ `[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
2301
+ );
2302
+ tryNextProvider = true;
2303
+ }
2304
+ }
2207
2305
  }
2208
2306
  const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
2209
2307
  if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
@@ -2213,19 +2311,31 @@ var RoutstrClient = class {
2213
2311
  params.token,
2214
2312
  baseUrl
2215
2313
  );
2216
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2217
- if (latestBalanceInfo.apiKey) {
2218
- const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
2219
- if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
2220
- if (storedApiKeyEntry) {
2221
- this.storageAdapter.removeApiKey(baseUrl);
2314
+ if (latestBalanceInfo.isInvalidApiKey) {
2315
+ this._log(
2316
+ "DEBUG",
2317
+ `[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
2318
+ );
2319
+ this.storageAdapter.removeApiKey(baseUrl);
2320
+ tryNextProvider = true;
2321
+ } else {
2322
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2323
+ if (latestBalanceInfo.apiKey) {
2324
+ const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
2325
+ if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
2326
+ if (storedApiKeyEntry) {
2327
+ this.storageAdapter.removeApiKey(baseUrl);
2328
+ }
2329
+ this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
2222
2330
  }
2223
- this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
2331
+ retryToken = latestBalanceInfo.apiKey;
2332
+ }
2333
+ if (latestTokenBalance >= 0) {
2334
+ this.storageAdapter.updateApiKeyBalance(
2335
+ baseUrl,
2336
+ latestTokenBalance
2337
+ );
2224
2338
  }
2225
- retryToken = latestBalanceInfo.apiKey;
2226
- }
2227
- if (latestTokenBalance >= 0) {
2228
- this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
2229
2339
  }
2230
2340
  } catch (error) {
2231
2341
  this._log(
@@ -2234,11 +2344,24 @@ var RoutstrClient = class {
2234
2344
  error
2235
2345
  );
2236
2346
  }
2237
- return this._makeRequest({
2238
- ...params,
2239
- token: retryToken,
2240
- headers: this._withAuthHeader(params.baseHeaders, retryToken)
2241
- });
2347
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
2348
+ this._log(
2349
+ "DEBUG",
2350
+ `[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
2351
+ );
2352
+ return this._makeRequest({
2353
+ ...params,
2354
+ token: retryToken,
2355
+ headers: this._withAuthHeader(params.baseHeaders, retryToken),
2356
+ retryCount: retryCount + 1
2357
+ });
2358
+ } else {
2359
+ this._log(
2360
+ "DEBUG",
2361
+ `[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
2362
+ );
2363
+ tryNextProvider = true;
2364
+ }
2242
2365
  }
2243
2366
  if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
2244
2367
  this._log(
@@ -2354,7 +2477,8 @@ var RoutstrClient = class {
2354
2477
  selectedModel: newModel,
2355
2478
  token: spendResult.token,
2356
2479
  requiredSats: newRequiredSats,
2357
- headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
2480
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
2481
+ retryCount: 0
2358
2482
  });
2359
2483
  }
2360
2484
  throw new FailoverError(baseUrl, Array.from(this.providerManager));