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