@routstr/sdk 0.3.5 → 0.3.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.
Files changed (39) hide show
  1. package/package.json +2 -2
  2. package/dist/client/index.d.mts +0 -411
  3. package/dist/client/index.d.ts +0 -411
  4. package/dist/client/index.js +0 -4819
  5. package/dist/client/index.js.map +0 -1
  6. package/dist/client/index.mjs +0 -4813
  7. package/dist/client/index.mjs.map +0 -1
  8. package/dist/discovery/index.d.mts +0 -196
  9. package/dist/discovery/index.d.ts +0 -196
  10. package/dist/discovery/index.js +0 -616
  11. package/dist/discovery/index.js.map +0 -1
  12. package/dist/discovery/index.mjs +0 -613
  13. package/dist/discovery/index.mjs.map +0 -1
  14. package/dist/index.d.mts +0 -190
  15. package/dist/index.d.ts +0 -190
  16. package/dist/index.js +0 -6114
  17. package/dist/index.js.map +0 -1
  18. package/dist/index.mjs +0 -6065
  19. package/dist/index.mjs.map +0 -1
  20. package/dist/interfaces-Bp0Ngmqv.d.mts +0 -176
  21. package/dist/interfaces-CIfd_phZ.d.ts +0 -112
  22. package/dist/interfaces-Cxi8R4TT.d.mts +0 -112
  23. package/dist/interfaces-D2FDCLyP.d.ts +0 -176
  24. package/dist/storage/index.d.mts +0 -87
  25. package/dist/storage/index.d.ts +0 -87
  26. package/dist/storage/index.js +0 -1734
  27. package/dist/storage/index.js.map +0 -1
  28. package/dist/storage/index.mjs +0 -1712
  29. package/dist/storage/index.mjs.map +0 -1
  30. package/dist/store-BD5zF9Hp.d.ts +0 -172
  31. package/dist/store-CBSyK2qg.d.mts +0 -172
  32. package/dist/types-DPQM6tIG.d.mts +0 -234
  33. package/dist/types-DPQM6tIG.d.ts +0 -234
  34. package/dist/wallet/index.d.mts +0 -245
  35. package/dist/wallet/index.d.ts +0 -245
  36. package/dist/wallet/index.js +0 -1329
  37. package/dist/wallet/index.js.map +0 -1
  38. package/dist/wallet/index.mjs +0 -1326
  39. package/dist/wallet/index.mjs.map +0 -1
@@ -1,1326 +0,0 @@
1
- import { getDecodedToken } from '@cashu/cashu-ts';
2
-
3
- // core/types.ts
4
- function makeConsoleLogger(prefix) {
5
- const fmt = (args) => prefix ? [prefix, ...args] : args;
6
- return {
7
- log: (...args) => console.log(...fmt(args)),
8
- warn: (...args) => console.warn(...fmt(args)),
9
- error: (...args) => console.error(...fmt(args)),
10
- debug: (...args) => console.log(...fmt(args)),
11
- child: (p) => makeConsoleLogger(prefix ? `${prefix}:${p}` : p)
12
- };
13
- }
14
- var consoleLogger = makeConsoleLogger();
15
-
16
- // core/errors.ts
17
- var InsufficientBalanceError = class extends Error {
18
- constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
19
- super(
20
- customMessage ?? `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
21
- );
22
- this.required = required;
23
- this.available = available;
24
- this.maxMintBalance = maxMintBalance;
25
- this.maxMintUrl = maxMintUrl;
26
- this.name = "InsufficientBalanceError";
27
- }
28
- };
29
-
30
- // wallet/AuditLogger.ts
31
- var AuditLogger = class _AuditLogger {
32
- static instance = null;
33
- static getInstance() {
34
- if (!_AuditLogger.instance) {
35
- _AuditLogger.instance = new _AuditLogger();
36
- }
37
- return _AuditLogger.instance;
38
- }
39
- async log(entry) {
40
- const fullEntry = {
41
- ...entry,
42
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
43
- };
44
- const logLine = JSON.stringify(fullEntry) + "\n";
45
- if (typeof window === "undefined") {
46
- try {
47
- const fs = await import('fs');
48
- const path = await import('path');
49
- const logPath = path.join(process.cwd(), "audit.log");
50
- fs.appendFileSync(logPath, logLine);
51
- } catch (error) {
52
- console.error("[AuditLogger] Failed to write to file:", error);
53
- }
54
- } else {
55
- console.log("[AUDIT]", logLine.trim());
56
- }
57
- }
58
- async logBalanceSnapshot(action, amounts, options) {
59
- await this.log({
60
- action,
61
- totalBalance: amounts.totalBalance,
62
- providerBalances: amounts.providerBalances,
63
- mintBalances: amounts.mintBalances,
64
- amount: options?.amount,
65
- mintUrl: options?.mintUrl,
66
- baseUrl: options?.baseUrl,
67
- status: options?.status ?? "success",
68
- details: options?.details
69
- });
70
- }
71
- };
72
- var auditLogger = AuditLogger.getInstance();
73
-
74
- // wallet/tokenUtils.ts
75
- function isNetworkErrorMessage(message) {
76
- return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed") || message.includes("ERR_TLS_CERT_ALTNAME_INVALID") || message.includes("ERR_TLS_CERT_NOT_YET_VALID") || message.includes("ERR_TLS_CERT_EXPIRED") || message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE") || message.includes("SELF_SIGNED_CERT_IN_CHAIN");
77
- }
78
- function getBalanceInSats(balance, unit) {
79
- return unit === "msat" ? balance / 1e3 : balance;
80
- }
81
- function selectMintWithBalance(balances, units, amount, excludeMints = []) {
82
- for (const mintUrl in balances) {
83
- if (excludeMints.includes(mintUrl)) {
84
- continue;
85
- }
86
- const balanceInSats = getBalanceInSats(balances[mintUrl], units[mintUrl]);
87
- if (balanceInSats >= amount) {
88
- return { selectedMintUrl: mintUrl, selectedMintBalance: balanceInSats };
89
- }
90
- }
91
- return { selectedMintUrl: null, selectedMintBalance: 0 };
92
- }
93
- var CashuSpender = class {
94
- constructor(walletAdapter, storageAdapter, _providerRegistry, balanceManager, logger) {
95
- this.walletAdapter = walletAdapter;
96
- this.storageAdapter = storageAdapter;
97
- this._providerRegistry = _providerRegistry;
98
- this.balanceManager = balanceManager;
99
- this.logger = (logger ?? consoleLogger).child("CashuSpender");
100
- }
101
- _isBusy = false;
102
- debugLevel = "WARN";
103
- logger;
104
- async receiveToken(token) {
105
- try {
106
- const result = await this.walletAdapter.receiveToken(token);
107
- return result;
108
- } catch (error) {
109
- const errorMessage = error instanceof Error ? error.message : String(error);
110
- if (errorMessage.includes("Failed to fetch mint")) {
111
- const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
112
- const existingIndex = cachedTokens.findIndex((t) => t.token === token);
113
- if (existingIndex === -1) {
114
- const { amount: amount2, unit: unit2 } = this._decodeTokenAmount(token);
115
- this.storageAdapter.setCachedReceiveTokens([
116
- ...cachedTokens,
117
- {
118
- token,
119
- amount: amount2,
120
- unit: unit2,
121
- createdAt: Date.now()
122
- }
123
- ]);
124
- }
125
- }
126
- const { amount, unit } = this._decodeTokenAmount(token);
127
- return { success: false, amount, unit, message: errorMessage };
128
- }
129
- }
130
- _decodeTokenAmount(token) {
131
- try {
132
- const decoded = getDecodedToken(token);
133
- const amount = decoded.proofs.reduce(
134
- (acc, proof) => acc + proof.amount,
135
- 0
136
- );
137
- const unit = decoded.unit || "sat";
138
- return { amount, unit };
139
- } catch {
140
- return { amount: 0, unit: "sat" };
141
- }
142
- }
143
- async _getBalanceState() {
144
- if (this.balanceManager) {
145
- return this.balanceManager.getBalanceState();
146
- }
147
- const mintBalances = await this.walletAdapter.getBalances();
148
- const units = this.walletAdapter.getMintUnits();
149
- let totalMintBalance = 0;
150
- const normalizedMintBalances = {};
151
- for (const url in mintBalances) {
152
- const balance = mintBalances[url];
153
- const unit = units[url];
154
- const balanceInSats = getBalanceInSats(balance, unit);
155
- normalizedMintBalances[url] = balanceInSats;
156
- totalMintBalance += balanceInSats;
157
- }
158
- const providerBalances = {};
159
- let totalProviderBalance = 0;
160
- const apiKeys = this.storageAdapter.getAllApiKeys();
161
- for (const apiKey of apiKeys) {
162
- if (!providerBalances[apiKey.baseUrl]) {
163
- providerBalances[apiKey.baseUrl] = 0;
164
- }
165
- providerBalances[apiKey.baseUrl] += apiKey.balance;
166
- totalProviderBalance += apiKey.balance;
167
- }
168
- return {
169
- totalBalance: totalMintBalance + totalProviderBalance,
170
- providerBalances,
171
- mintBalances: normalizedMintBalances
172
- };
173
- }
174
- async _logTransaction(action, options) {
175
- const balanceState = await this._getBalanceState();
176
- await auditLogger.logBalanceSnapshot(action, balanceState, options);
177
- }
178
- /**
179
- * Check if the spender is currently in a critical operation
180
- */
181
- get isBusy() {
182
- return this._isBusy;
183
- }
184
- getDebugLevel() {
185
- return this.debugLevel;
186
- }
187
- setDebugLevel(level) {
188
- this.debugLevel = level;
189
- }
190
- _log(level, ...args) {
191
- const levelPriority = {
192
- DEBUG: 0,
193
- WARN: 1,
194
- ERROR: 2
195
- };
196
- if (levelPriority[level] >= levelPriority[this.debugLevel]) {
197
- switch (level) {
198
- case "DEBUG":
199
- this.logger.log(...args);
200
- break;
201
- case "WARN":
202
- this.logger.warn(...args);
203
- break;
204
- case "ERROR":
205
- this.logger.error(...args);
206
- break;
207
- }
208
- }
209
- }
210
- /**
211
- * Spend Cashu tokens with automatic mint selection and retry logic
212
- * Throws errors on failure instead of returning failed SpendResult
213
- */
214
- async spend(options) {
215
- const {
216
- mintUrl,
217
- amount,
218
- baseUrl,
219
- reuseToken = false,
220
- p2pkPubkey,
221
- excludeMints = [],
222
- retryCount = 0
223
- } = options;
224
- this._isBusy = true;
225
- try {
226
- const result = await this._spendInternal({
227
- mintUrl,
228
- amount,
229
- baseUrl,
230
- reuseToken,
231
- p2pkPubkey,
232
- excludeMints,
233
- retryCount
234
- });
235
- if (result.status === "failed" || !result.token) {
236
- const errorMsg = result.error || `Insufficient balance. Need ${amount} sats.`;
237
- if (this._isNetworkError(errorMsg)) {
238
- throw new Error(
239
- `Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
240
- );
241
- }
242
- if (result.errorDetails) {
243
- throw new InsufficientBalanceError(
244
- result.errorDetails.required,
245
- result.errorDetails.available,
246
- result.errorDetails.maxMintBalance,
247
- result.errorDetails.maxMintUrl
248
- );
249
- }
250
- throw new Error(errorMsg);
251
- }
252
- return result;
253
- } finally {
254
- this._isBusy = false;
255
- }
256
- }
257
- /**
258
- * Check if error message indicates a network error
259
- */
260
- _isNetworkError(message) {
261
- return isNetworkErrorMessage(message) || message.includes("Your mint") && message.includes("unreachable");
262
- }
263
- /**
264
- * Internal spending logic
265
- */
266
- async _spendInternal(options) {
267
- let {
268
- mintUrl,
269
- amount,
270
- baseUrl,
271
- reuseToken,
272
- p2pkPubkey,
273
- excludeMints,
274
- retryCount
275
- } = options;
276
- this._log(
277
- "DEBUG",
278
- `[CashuSpender] _spendInternal: amount=${amount}, mintUrl=${mintUrl}, baseUrl=${baseUrl}, reuseToken=${reuseToken}`
279
- );
280
- let adjustedAmount = Math.ceil(amount);
281
- if (!adjustedAmount || isNaN(adjustedAmount)) {
282
- this._log(
283
- "ERROR",
284
- `[CashuSpender] _spendInternal: Invalid amount: ${amount}`
285
- );
286
- return {
287
- token: null,
288
- status: "failed",
289
- balance: 0,
290
- error: "Please enter a valid amount"
291
- };
292
- }
293
- if (reuseToken && baseUrl) {
294
- this._log(
295
- "DEBUG",
296
- `[CashuSpender] _spendInternal: Attempting to reuse token for ${baseUrl}`
297
- );
298
- const existingResult = await this._tryReuseToken(
299
- baseUrl,
300
- adjustedAmount,
301
- mintUrl
302
- );
303
- if (existingResult) {
304
- this._log(
305
- "DEBUG",
306
- `[CashuSpender] _spendInternal: Successfully reused token, balance: ${existingResult.balance}`
307
- );
308
- return existingResult;
309
- }
310
- this._log(
311
- "DEBUG",
312
- `[CashuSpender] _spendInternal: Could not reuse token, will create new token`
313
- );
314
- }
315
- const balanceState = await this._getBalanceState();
316
- const totalAvailableBalance = balanceState.totalBalance;
317
- this._log(
318
- "DEBUG",
319
- `[CashuSpender] _spendInternal: totalAvailableBalance=${totalAvailableBalance}, adjustedAmount=${adjustedAmount}`
320
- );
321
- if (totalAvailableBalance < adjustedAmount) {
322
- this._log(
323
- "ERROR",
324
- `[CashuSpender] _spendInternal: Insufficient balance, have=${totalAvailableBalance}, need=${adjustedAmount}`
325
- );
326
- return this._createInsufficientBalanceError(
327
- adjustedAmount,
328
- balanceState.mintBalances,
329
- totalAvailableBalance
330
- );
331
- }
332
- let token = null;
333
- let selectedMintUrl;
334
- let spentAmount = adjustedAmount;
335
- if (this.balanceManager) {
336
- const tokenResult = await this.balanceManager.createProviderToken({
337
- mintUrl,
338
- baseUrl,
339
- amount: adjustedAmount,
340
- p2pkPubkey,
341
- excludeMints,
342
- retryCount
343
- });
344
- if (!tokenResult.success || !tokenResult.token) {
345
- if ((tokenResult.error || "").includes("Insufficient balance")) {
346
- return this._createInsufficientBalanceError(
347
- adjustedAmount,
348
- balanceState.mintBalances,
349
- totalAvailableBalance
350
- );
351
- }
352
- return {
353
- token: null,
354
- status: "failed",
355
- balance: 0,
356
- error: tokenResult.error || "Failed to create token"
357
- };
358
- }
359
- token = tokenResult.token;
360
- selectedMintUrl = tokenResult.selectedMintUrl;
361
- spentAmount = tokenResult.amountSpent || adjustedAmount;
362
- } else {
363
- try {
364
- token = await this.walletAdapter.sendToken(
365
- mintUrl,
366
- adjustedAmount,
367
- p2pkPubkey
368
- );
369
- selectedMintUrl = mintUrl;
370
- } catch (error) {
371
- const errorMsg = error instanceof Error ? error.message : String(error);
372
- return {
373
- token: null,
374
- status: "failed",
375
- balance: 0,
376
- error: `Error generating token: ${errorMsg}`
377
- };
378
- }
379
- }
380
- if (token) {
381
- this._log(
382
- "DEBUG",
383
- `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
384
- );
385
- }
386
- this._logTransaction("spend", {
387
- amount: spentAmount,
388
- mintUrl: selectedMintUrl || mintUrl,
389
- baseUrl,
390
- status: "success"
391
- });
392
- this._log(
393
- "DEBUG",
394
- `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
395
- );
396
- const units = this.walletAdapter.getMintUnits();
397
- return {
398
- token,
399
- status: "success",
400
- balance: spentAmount,
401
- unit: (selectedMintUrl ? units[selectedMintUrl] : units[mintUrl]) || "sat"
402
- };
403
- }
404
- /**
405
- * Try to reuse an existing API key
406
- */
407
- async _tryReuseToken(baseUrl, amount, mintUrl) {
408
- const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
409
- if (!apiKeyEntry) return null;
410
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
411
- const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
412
- this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
413
- if (balanceForBaseUrl > amount) {
414
- const units = this.walletAdapter.getMintUnits();
415
- const unit = units[mintUrl] || "sat";
416
- return {
417
- token: apiKeyEntry.key,
418
- status: "success",
419
- balance: balanceForBaseUrl,
420
- unit
421
- };
422
- }
423
- if (this.balanceManager) {
424
- const topUpAmount = Math.ceil(amount * 1.2 - balanceForBaseUrl);
425
- const topUpResult = await this.balanceManager.topUp({
426
- mintUrl,
427
- baseUrl,
428
- amount: topUpAmount,
429
- token: apiKeyEntry.key
430
- });
431
- this._log("DEBUG", "TOPUP ", topUpResult);
432
- if (topUpResult.success && topUpResult.toppedUpAmount) {
433
- const newBalance = balanceForBaseUrl + topUpResult.toppedUpAmount;
434
- const units = this.walletAdapter.getMintUnits();
435
- const unit = units[mintUrl] || "sat";
436
- this._logTransaction("topup", {
437
- amount: topUpResult.toppedUpAmount,
438
- mintUrl,
439
- baseUrl,
440
- status: "success"
441
- });
442
- return {
443
- token: apiKeyEntry.key,
444
- status: "success",
445
- balance: newBalance,
446
- unit
447
- };
448
- }
449
- const providerBalance = await this._getProviderTokenBalance(
450
- baseUrl,
451
- apiKeyEntry.key
452
- );
453
- this._log("DEBUG", providerBalance);
454
- if (providerBalance <= 0) {
455
- this.storageAdapter.removeApiKey(baseUrl);
456
- }
457
- }
458
- return null;
459
- }
460
- /**
461
- * Refund all xcashu tokens from storage by calling the provider's refund endpoint.
462
- * The xcashu token acts as an API key to claim the refund, and the response contains
463
- * the actual refunded Cashu token which is then received into the wallet.
464
- * @param mintUrl - The mint URL for receiving tokens
465
- * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
466
- * @returns Results for each xcashu token refund attempt
467
- */
468
- async refundXcashuTokens(mintUrl, excludeBaseUrls) {
469
- const results = [];
470
- const xcashuTokens = this.storageAdapter.getXcashuTokens();
471
- const excludedUrls = new Set(excludeBaseUrls || []);
472
- for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
473
- if (excludedUrls.has(baseUrl)) continue;
474
- for (const xcashuToken of tokens) {
475
- try {
476
- if (!this.balanceManager) {
477
- throw new Error("BalanceManager not available for xcashu refund");
478
- }
479
- const fetchResult = await this.balanceManager.fetchRefundToken(
480
- baseUrl,
481
- xcashuToken.token,
482
- true
483
- );
484
- if (!fetchResult.success || !fetchResult.token) {
485
- throw new Error(
486
- fetchResult.error || "Failed to fetch refund token from provider"
487
- );
488
- }
489
- const receiveResult = await this.receiveToken(fetchResult.token);
490
- if (receiveResult.success) {
491
- this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
492
- results.push({
493
- baseUrl,
494
- token: xcashuToken.token,
495
- success: true
496
- });
497
- this._log(
498
- "DEBUG",
499
- `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
500
- );
501
- } else {
502
- const currentTryCount = xcashuToken.tryCount ?? 0;
503
- const newTryCount = currentTryCount + 1;
504
- this.storageAdapter.updateXcashuTokenTryCount(
505
- xcashuToken.token,
506
- newTryCount
507
- );
508
- results.push({
509
- baseUrl,
510
- token: xcashuToken.token,
511
- success: false,
512
- error: receiveResult.message ?? "Refund failed"
513
- });
514
- this._log(
515
- "DEBUG",
516
- `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}: ${receiveResult.message}`
517
- );
518
- }
519
- } catch (error) {
520
- const currentTryCount = xcashuToken.tryCount ?? 0;
521
- const newTryCount = currentTryCount + 1;
522
- this.storageAdapter.updateXcashuTokenTryCount(
523
- xcashuToken.token,
524
- newTryCount
525
- );
526
- const errorMessage = error instanceof Error ? error.message : String(error);
527
- results.push({
528
- baseUrl,
529
- token: xcashuToken.token,
530
- success: false,
531
- error: errorMessage
532
- });
533
- this._log(
534
- "ERROR",
535
- `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
536
- );
537
- }
538
- }
539
- }
540
- return results;
541
- }
542
- /**
543
- * Refund specific providers without retrying spend
544
- */
545
- async refundProviders(mintUrl, forceRefund) {
546
- const results = [];
547
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
548
- for (const apiKeyEntry of apiKeyDistribution) {
549
- const apiKeyEntryFull = this.storageAdapter.getApiKey(
550
- apiKeyEntry.baseUrl
551
- );
552
- if (apiKeyEntryFull && this.balanceManager) {
553
- try {
554
- const balanceResult = await this.balanceManager.getTokenBalance(
555
- apiKeyEntryFull.key,
556
- apiKeyEntry.baseUrl
557
- );
558
- if (balanceResult.isInvalidApiKey) {
559
- this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
560
- results.push({
561
- baseUrl: apiKeyEntry.baseUrl,
562
- success: true
563
- });
564
- continue;
565
- }
566
- if (balanceResult.amount >= 0) {
567
- const balanceSat = balanceResult.unit === "msat" ? Math.floor(balanceResult.amount / 1e3) : balanceResult.amount;
568
- this.storageAdapter.updateApiKeyBalance(
569
- apiKeyEntry.baseUrl,
570
- balanceSat
571
- );
572
- }
573
- } catch {
574
- }
575
- const refreshedEntry = this.storageAdapter.getApiKey(
576
- apiKeyEntry.baseUrl
577
- );
578
- if (!refreshedEntry) continue;
579
- const refundResult = await this.balanceManager.refundApiKey({
580
- mintUrl,
581
- baseUrl: apiKeyEntry.baseUrl,
582
- apiKey: refreshedEntry.key,
583
- forceRefund
584
- });
585
- if (refundResult.success) {
586
- this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
587
- } else {
588
- const currentEntry = this.storageAdapter.getApiKey(
589
- apiKeyEntry.baseUrl
590
- );
591
- if (currentEntry) {
592
- this.storageAdapter.updateApiKeyBalance(
593
- apiKeyEntry.baseUrl,
594
- currentEntry.balance
595
- );
596
- }
597
- }
598
- results.push({
599
- baseUrl: apiKeyEntry.baseUrl,
600
- success: refundResult.success
601
- });
602
- } else {
603
- results.push({
604
- baseUrl: apiKeyEntry.baseUrl,
605
- success: false
606
- });
607
- }
608
- }
609
- return results;
610
- }
611
- /**
612
- * Create an insufficient balance error result
613
- */
614
- _createInsufficientBalanceError(required, normalizedBalances, availableBalance) {
615
- let maxBalance = 0;
616
- let maxMintUrl = "";
617
- for (const mintUrl in normalizedBalances) {
618
- const balanceInSats = normalizedBalances[mintUrl];
619
- if (balanceInSats > maxBalance) {
620
- maxBalance = balanceInSats;
621
- maxMintUrl = mintUrl;
622
- }
623
- }
624
- const error = new InsufficientBalanceError(
625
- required,
626
- availableBalance ?? maxBalance,
627
- maxBalance,
628
- maxMintUrl
629
- );
630
- return {
631
- token: null,
632
- status: "failed",
633
- balance: 0,
634
- error: error.message,
635
- errorDetails: {
636
- required,
637
- available: availableBalance ?? maxBalance,
638
- maxMintBalance: maxBalance,
639
- maxMintUrl
640
- }
641
- };
642
- }
643
- async _getProviderTokenBalance(baseUrl, token) {
644
- try {
645
- const response = await fetch(`${baseUrl}v1/wallet/info`, {
646
- headers: {
647
- Authorization: `Bearer ${token}`
648
- }
649
- });
650
- if (response.ok) {
651
- const data = await response.json();
652
- return data.balance / 1e3;
653
- }
654
- } catch {
655
- return 0;
656
- }
657
- return 0;
658
- }
659
- };
660
-
661
- // wallet/BalanceManager.ts
662
- var BalanceManager = class _BalanceManager {
663
- constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender, logger) {
664
- this.walletAdapter = walletAdapter;
665
- this.storageAdapter = storageAdapter;
666
- this.providerRegistry = providerRegistry;
667
- this.logger = (logger ?? consoleLogger).child("BalanceManager");
668
- if (cashuSpender) {
669
- this.cashuSpender = cashuSpender;
670
- } else {
671
- this.cashuSpender = new CashuSpender(
672
- walletAdapter,
673
- storageAdapter,
674
- providerRegistry,
675
- this
676
- );
677
- }
678
- }
679
- cashuSpender;
680
- /** In-memory guard for per-provider wallet mutations (topup / refund) */
681
- providerWalletOps = /* @__PURE__ */ new Map();
682
- /** Cooldown (ms) between opposite operations on the same provider */
683
- static PROVIDER_WALLET_COOLDOWN_MS = 1e4;
684
- logger;
685
- /**
686
- * Check whether a wallet operation (topup/refund) may run for a provider.
687
- * Returns the reason when blocked.
688
- */
689
- _canRunProviderWalletOperation(baseUrl, type) {
690
- const existing = this.providerWalletOps.get(baseUrl);
691
- if (!existing) {
692
- return { allowed: true };
693
- }
694
- if (existing.type === type) {
695
- return { allowed: true };
696
- }
697
- if (!existing.endTime) {
698
- return {
699
- allowed: false,
700
- reason: `Provider wallet operation locked; ${existing.type} in progress`
701
- };
702
- }
703
- const elapsed = Date.now() - existing.endTime;
704
- if (elapsed < _BalanceManager.PROVIDER_WALLET_COOLDOWN_MS) {
705
- return {
706
- allowed: false,
707
- reason: `Provider wallet operation locked; recent ${existing.type} completed ${Math.round(elapsed / 1e3)}s ago`
708
- };
709
- }
710
- this.providerWalletOps.delete(baseUrl);
711
- return { allowed: true };
712
- }
713
- _beginProviderWalletOperation(baseUrl, type) {
714
- this.providerWalletOps.set(baseUrl, { type, startTime: Date.now() });
715
- }
716
- _endProviderWalletOperation(baseUrl, type) {
717
- const existing = this.providerWalletOps.get(baseUrl);
718
- if (existing && existing.type === type) {
719
- existing.endTime = Date.now();
720
- }
721
- }
722
- async getBalanceState() {
723
- const mintBalances = await this.walletAdapter.getBalances();
724
- const units = this.walletAdapter.getMintUnits();
725
- let totalMintBalance = 0;
726
- const normalizedMintBalances = {};
727
- for (const url in mintBalances) {
728
- const balance = mintBalances[url];
729
- const unit = units[url];
730
- const balanceInSats = getBalanceInSats(balance, unit);
731
- normalizedMintBalances[url] = balanceInSats;
732
- totalMintBalance += balanceInSats;
733
- }
734
- const providerBalances = {};
735
- let totalProviderBalance = 0;
736
- const apiKeys = this.storageAdapter.getAllApiKeys();
737
- for (const apiKey of apiKeys) {
738
- if (!providerBalances[apiKey.baseUrl]) {
739
- providerBalances[apiKey.baseUrl] = 0;
740
- }
741
- providerBalances[apiKey.baseUrl] += apiKey.balance;
742
- totalProviderBalance += apiKey.balance;
743
- }
744
- return {
745
- totalBalance: totalMintBalance + totalProviderBalance,
746
- providerBalances,
747
- mintBalances: normalizedMintBalances
748
- };
749
- }
750
- /**
751
- * Refund API key balance - convert remaining API key balance to cashu token
752
- * @param options - Refund options including forceRefund flag
753
- * @returns Refund result
754
- */
755
- async refundApiKey(options) {
756
- const { mintUrl, baseUrl, apiKey, forceRefund } = options;
757
- const guard = this._canRunProviderWalletOperation(baseUrl, "refund");
758
- if (!guard.allowed) {
759
- this.logger.log(`Skipping refund for ${baseUrl} - ${guard.reason}`);
760
- return { success: false, message: guard.reason };
761
- }
762
- this._beginProviderWalletOperation(baseUrl, "refund");
763
- try {
764
- return await this._refundApiKeyImpl({ mintUrl, baseUrl, apiKey, forceRefund });
765
- } finally {
766
- this._endProviderWalletOperation(baseUrl, "refund");
767
- }
768
- }
769
- async _refundApiKeyImpl(options) {
770
- const { mintUrl, baseUrl, apiKey, forceRefund } = options;
771
- if (!apiKey) {
772
- return { success: false, message: "No API key to refund" };
773
- }
774
- if (!forceRefund) {
775
- const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
776
- if (apiKeyEntry?.lastUsed) {
777
- const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
778
- if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
779
- this.logger.log(`Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`);
780
- return {
781
- success: false,
782
- message: "API key was used recently, skipping refund"
783
- };
784
- }
785
- }
786
- }
787
- let fetchResult;
788
- try {
789
- fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
790
- if (!fetchResult.success) {
791
- return {
792
- success: false,
793
- message: fetchResult.error || "API key refund failed",
794
- requestId: fetchResult.requestId
795
- };
796
- }
797
- if (!fetchResult.token) {
798
- return {
799
- success: false,
800
- message: "No token received from API key refund",
801
- requestId: fetchResult.requestId
802
- };
803
- }
804
- if (fetchResult.error === "No balance to refund") {
805
- this.storageAdapter.removeApiKey(baseUrl);
806
- return { success: true, message: "No balance to refund, key cleaned up" };
807
- }
808
- const receiveResult = await this.cashuSpender.receiveToken(
809
- fetchResult.token
810
- );
811
- const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
812
- if (receiveResult.success) {
813
- this.storageAdapter.removeApiKey(baseUrl);
814
- }
815
- return {
816
- success: receiveResult.success,
817
- refundedAmount: totalAmountMsat,
818
- message: receiveResult.message,
819
- requestId: fetchResult.requestId
820
- };
821
- } catch (error) {
822
- this.logger.error("API key refund error", error);
823
- return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
824
- }
825
- }
826
- /**
827
- * Fetch refund token from provider API using API key (or xcashu token) authentication
828
- */
829
- async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
830
- if (!baseUrl) {
831
- return {
832
- success: false,
833
- error: "No base URL configured"
834
- };
835
- }
836
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
837
- const url = `${normalizedBaseUrl}v1/wallet/refund`;
838
- const controller = new AbortController();
839
- const timeoutId = setTimeout(() => {
840
- controller.abort();
841
- }, 6e4);
842
- try {
843
- const headers = {
844
- "Content-Type": "application/json"
845
- };
846
- if (xCashu) {
847
- headers["X-Cashu"] = apiKeyOrToken;
848
- } else {
849
- headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
850
- }
851
- const response = await fetch(url, {
852
- method: "POST",
853
- headers,
854
- signal: controller.signal
855
- });
856
- clearTimeout(timeoutId);
857
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
858
- if (!response.ok) {
859
- const errorData = await response.json().catch(() => ({}));
860
- return {
861
- success: false,
862
- requestId,
863
- error: `API key refund failed: ${errorData?.detail || response.statusText}`
864
- };
865
- }
866
- const data = await response.json();
867
- return {
868
- success: true,
869
- token: data.token,
870
- requestId
871
- };
872
- } catch (error) {
873
- clearTimeout(timeoutId);
874
- this.logger.error("fetchRefundToken fetch error", error);
875
- if (error instanceof Error) {
876
- if (error.name === "AbortError") {
877
- return {
878
- success: false,
879
- error: "Request timed out after 1 minute"
880
- };
881
- }
882
- return {
883
- success: false,
884
- error: error.message
885
- };
886
- }
887
- return {
888
- success: false,
889
- error: "Unknown error occurred during API key refund request"
890
- };
891
- }
892
- }
893
- /**
894
- * Top up API key balance with a cashu token
895
- */
896
- async topUp(options) {
897
- const { mintUrl, baseUrl, amount, token: providedToken } = options;
898
- const guard = this._canRunProviderWalletOperation(baseUrl, "topup");
899
- if (!guard.allowed) {
900
- this.logger.log(`Skipping topup for ${baseUrl} - ${guard.reason}`);
901
- return { success: false, message: guard.reason };
902
- }
903
- this._beginProviderWalletOperation(baseUrl, "topup");
904
- try {
905
- return await this._topUpImpl({ mintUrl, baseUrl, amount, token: providedToken });
906
- } finally {
907
- this._endProviderWalletOperation(baseUrl, "topup");
908
- }
909
- }
910
- async _topUpImpl(options) {
911
- const { mintUrl, baseUrl, amount, token: providedToken } = options;
912
- if (!amount || amount <= 0) {
913
- return { success: false, message: "Invalid top up amount" };
914
- }
915
- const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
916
- const apiKey = providedToken || apiKeyEntry?.key;
917
- if (!apiKey) {
918
- return { success: false, message: "No API key available for top up" };
919
- }
920
- let cashuToken = null;
921
- let requestId;
922
- try {
923
- const tokenResult = await this.createProviderToken({
924
- mintUrl,
925
- baseUrl,
926
- amount
927
- });
928
- if (!tokenResult.success || !tokenResult.token) {
929
- return {
930
- success: false,
931
- message: tokenResult.error || "Unable to create top up token"
932
- };
933
- }
934
- cashuToken = tokenResult.token;
935
- const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
936
- requestId = topUpResult.requestId;
937
- this.logger.log("topUpResult:", topUpResult);
938
- if (!topUpResult.success) {
939
- await this._recoverFailedTopUp(cashuToken);
940
- return {
941
- success: false,
942
- message: topUpResult.error || "Top up failed",
943
- requestId,
944
- recoveredToken: true
945
- };
946
- }
947
- return {
948
- success: true,
949
- toppedUpAmount: amount,
950
- requestId
951
- };
952
- } catch (error) {
953
- this.logger.log(`topup error for ${baseUrl}: ${error}`);
954
- if (cashuToken) {
955
- await this._recoverFailedTopUp(cashuToken);
956
- }
957
- return this._handleTopUpError(error, mintUrl, requestId);
958
- }
959
- }
960
- async createProviderToken(options) {
961
- const {
962
- mintUrl,
963
- baseUrl,
964
- amount,
965
- retryCount = 0,
966
- excludeMints = [],
967
- p2pkPubkey
968
- } = options;
969
- const adjustedAmount = Math.ceil(amount);
970
- this.logger.log(`createProviderToken: baseUrl=${baseUrl} mintUrl=${mintUrl} amount=${amount} adjustedAmount=${adjustedAmount} retryCount=${retryCount}`);
971
- if (!adjustedAmount || isNaN(adjustedAmount)) {
972
- this.logger.error(`createProviderToken: invalid amount=${amount}`);
973
- return { success: false, error: "Invalid top up amount" };
974
- }
975
- const balanceState = await this.getBalanceState();
976
- const balances = await this.walletAdapter.getBalances();
977
- const units = this.walletAdapter.getMintUnits();
978
- const totalMintBalance = Object.values(balanceState.mintBalances).reduce(
979
- (sum, value) => sum + value,
980
- 0
981
- );
982
- const targetProviderBalance = balanceState.providerBalances[baseUrl] || 0;
983
- const refundableProviderBalance = Object.entries(
984
- balanceState.providerBalances
985
- ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
986
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
987
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
988
- return this.createProviderToken({
989
- ...options,
990
- retryCount: retryCount + 1
991
- });
992
- }
993
- if (totalMintBalance + targetProviderBalance < adjustedAmount) {
994
- const error = new InsufficientBalanceError(
995
- adjustedAmount,
996
- totalMintBalance + targetProviderBalance,
997
- totalMintBalance,
998
- Object.entries(balanceState.mintBalances).reduce(
999
- (max, [url, balance]) => balance > max.balance ? { url, balance } : max,
1000
- { url: "", balance: 0 }
1001
- ).url
1002
- );
1003
- this.logger.error(`createProviderToken: insufficient balance required=${adjustedAmount} available=${totalMintBalance + targetProviderBalance} totalMint=${totalMintBalance} targetProvider=${targetProviderBalance}`);
1004
- return { success: false, error: error.message };
1005
- }
1006
- const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
1007
- let requiredAmount = adjustedAmount;
1008
- const supportedMintsOnly = providerMints.length > 0;
1009
- let candidates = this._selectCandidateMints({
1010
- balances,
1011
- units,
1012
- amount: requiredAmount,
1013
- preferredMintUrl: mintUrl,
1014
- excludeMints,
1015
- allowedMints: supportedMintsOnly ? providerMints : void 0
1016
- });
1017
- if (candidates.length === 0 && supportedMintsOnly) {
1018
- requiredAmount += 2;
1019
- candidates = this._selectCandidateMints({
1020
- balances,
1021
- units,
1022
- amount: requiredAmount,
1023
- preferredMintUrl: mintUrl,
1024
- excludeMints
1025
- });
1026
- }
1027
- if (candidates.length === 0) {
1028
- let maxBalance = 0;
1029
- let maxMintUrl = "";
1030
- for (const mintUrl2 in balances) {
1031
- const balance = balances[mintUrl2];
1032
- const unit = units[mintUrl2];
1033
- const balanceInSats = getBalanceInSats(balance, unit);
1034
- if (balanceInSats > maxBalance) {
1035
- maxBalance = balanceInSats;
1036
- maxMintUrl = mintUrl2;
1037
- }
1038
- }
1039
- this.logger.error(`createProviderToken: no candidate mints required=${requiredAmount} totalMint=${totalMintBalance} maxBalance=${maxBalance} maxMint=${maxMintUrl}`);
1040
- const error = new InsufficientBalanceError(
1041
- adjustedAmount,
1042
- totalMintBalance,
1043
- maxBalance,
1044
- maxMintUrl
1045
- );
1046
- return { success: false, error: error.message };
1047
- }
1048
- let lastError;
1049
- for (const candidateMint of candidates) {
1050
- try {
1051
- this.logger.log(`createProviderToken: attempting mint=${candidateMint} amount=${requiredAmount}`);
1052
- const token = await this.walletAdapter.sendToken(
1053
- candidateMint,
1054
- requiredAmount,
1055
- p2pkPubkey
1056
- );
1057
- this.logger.log(`createProviderToken: success from mint=${candidateMint}`);
1058
- return {
1059
- success: true,
1060
- token,
1061
- selectedMintUrl: candidateMint,
1062
- amountSpent: requiredAmount
1063
- };
1064
- } catch (error) {
1065
- const errorMsg = error instanceof Error ? error.message : String(error);
1066
- this.logger.error(`createProviderToken: mint=${candidateMint} failed: ${errorMsg}`);
1067
- if (error instanceof Error) {
1068
- lastError = errorMsg;
1069
- if (isNetworkErrorMessage(error.message)) {
1070
- this.logger.warn(`createProviderToken: network error from ${candidateMint}, trying next mint...`);
1071
- continue;
1072
- }
1073
- }
1074
- return {
1075
- success: false,
1076
- error: lastError || "Failed to create top up token"
1077
- };
1078
- }
1079
- }
1080
- this.logger.error(`createProviderToken: all candidate mints exhausted lastError=${lastError}`);
1081
- return {
1082
- success: false,
1083
- error: lastError || "All candidate mints failed while creating top up token"
1084
- };
1085
- }
1086
- _selectCandidateMints(options) {
1087
- const {
1088
- balances,
1089
- units,
1090
- amount,
1091
- preferredMintUrl,
1092
- excludeMints,
1093
- allowedMints
1094
- } = options;
1095
- const candidates = [];
1096
- const { selectedMintUrl: firstMint } = selectMintWithBalance(
1097
- balances,
1098
- units,
1099
- amount,
1100
- excludeMints
1101
- );
1102
- if (firstMint && (!allowedMints || allowedMints.length === 0 || allowedMints.includes(firstMint))) {
1103
- candidates.push(firstMint);
1104
- }
1105
- const canUseMint = (mint) => {
1106
- if (excludeMints.includes(mint)) return false;
1107
- if (allowedMints && allowedMints.length > 0 && !allowedMints.includes(mint)) {
1108
- return false;
1109
- }
1110
- const rawBalance = balances[mint] || 0;
1111
- const unit = units[mint];
1112
- const balanceInSats = getBalanceInSats(rawBalance, unit);
1113
- return balanceInSats >= amount;
1114
- };
1115
- if (preferredMintUrl && canUseMint(preferredMintUrl) && !candidates.includes(preferredMintUrl)) {
1116
- candidates.push(preferredMintUrl);
1117
- }
1118
- for (const mint in balances) {
1119
- if (mint === preferredMintUrl || candidates.includes(mint)) continue;
1120
- if (canUseMint(mint)) {
1121
- candidates.push(mint);
1122
- }
1123
- }
1124
- return candidates;
1125
- }
1126
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1127
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1128
- const forceRefund = retryCount >= 2;
1129
- const apiKeysToRefund = apiKeyDistribution.filter(
1130
- (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1131
- );
1132
- const apiKeyRefundResults = await Promise.allSettled(
1133
- apiKeysToRefund.map(async (apiKeyEntry) => {
1134
- const fullApiKeyEntry = this.storageAdapter.getApiKey(
1135
- apiKeyEntry.baseUrl
1136
- );
1137
- if (!fullApiKeyEntry) {
1138
- return { baseUrl: apiKeyEntry.baseUrl, success: false };
1139
- }
1140
- const result = await this.refundApiKey({
1141
- mintUrl,
1142
- baseUrl: apiKeyEntry.baseUrl,
1143
- apiKey: fullApiKeyEntry.key,
1144
- forceRefund
1145
- });
1146
- return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1147
- })
1148
- );
1149
- for (const result of apiKeyRefundResults) {
1150
- if (result.status === "fulfilled" && result.value.success) {
1151
- this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
1152
- }
1153
- }
1154
- }
1155
- /**
1156
- * Post topup request to provider API
1157
- */
1158
- async _postTopUp(baseUrl, storedToken, cashuToken) {
1159
- if (!baseUrl) {
1160
- return {
1161
- success: false,
1162
- error: "No base URL configured"
1163
- };
1164
- }
1165
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1166
- const url = `${normalizedBaseUrl}v1/wallet/topup?cashu_token=${encodeURIComponent(
1167
- cashuToken
1168
- )}`;
1169
- const controller = new AbortController();
1170
- const timeoutId = setTimeout(() => {
1171
- controller.abort();
1172
- }, 6e4);
1173
- try {
1174
- const response = await fetch(url, {
1175
- method: "POST",
1176
- headers: {
1177
- Authorization: `Bearer ${storedToken}`,
1178
- "Content-Type": "application/json"
1179
- },
1180
- signal: controller.signal
1181
- });
1182
- clearTimeout(timeoutId);
1183
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
1184
- if (!response.ok) {
1185
- const errorData = await response.json().catch(() => ({}));
1186
- return {
1187
- success: false,
1188
- requestId,
1189
- error: errorData?.detail || `Top up failed with status ${response.status}`
1190
- };
1191
- }
1192
- return { success: true, requestId };
1193
- } catch (error) {
1194
- clearTimeout(timeoutId);
1195
- this.logger.error("_postTopUp fetch error", error);
1196
- if (error instanceof Error) {
1197
- if (error.name === "AbortError") {
1198
- return {
1199
- success: false,
1200
- error: "Request timed out after 1 minute"
1201
- };
1202
- }
1203
- return {
1204
- success: false,
1205
- error: error.message
1206
- };
1207
- }
1208
- return {
1209
- success: false,
1210
- error: "Unknown error occurred during top up request"
1211
- };
1212
- }
1213
- }
1214
- /**
1215
- * Attempt to receive token back after failed top up
1216
- */
1217
- async _recoverFailedTopUp(cashuToken) {
1218
- try {
1219
- await this.cashuSpender.receiveToken(cashuToken);
1220
- } catch (error) {
1221
- this.logger.error("_recoverFailedTopUp: failed to recover token", error);
1222
- }
1223
- }
1224
- /**
1225
- * Handle refund errors with specific error types
1226
- */
1227
- _handleRefundError(error, mintUrl, requestId) {
1228
- if (error instanceof Error) {
1229
- if (isNetworkErrorMessage(error.message)) {
1230
- return {
1231
- success: false,
1232
- message: `Failed to connect to the mint: ${mintUrl}`,
1233
- requestId
1234
- };
1235
- }
1236
- if (error.message.includes("Wallet not found")) {
1237
- return {
1238
- success: false,
1239
- message: `Wallet couldn't be loaded. Please save this refunded cashu token manually.`,
1240
- requestId
1241
- };
1242
- }
1243
- return {
1244
- success: false,
1245
- message: error.message,
1246
- requestId
1247
- };
1248
- }
1249
- return {
1250
- success: false,
1251
- message: "Refund failed",
1252
- requestId
1253
- };
1254
- }
1255
- /**
1256
- * Get token balance from provider
1257
- */
1258
- async getTokenBalance(token, baseUrl) {
1259
- try {
1260
- const response = await fetch(`${baseUrl}v1/wallet/info`, {
1261
- headers: {
1262
- Authorization: `Bearer ${token}`
1263
- }
1264
- });
1265
- if (response.ok) {
1266
- const data = await response.json();
1267
- return {
1268
- amount: data.balance,
1269
- reserved: data.reserved ?? 0,
1270
- unit: "msat",
1271
- apiKey: data.api_key
1272
- };
1273
- } else {
1274
- this.logger.warn(`getTokenBalance: status=${response.status}`);
1275
- const data = await response.json();
1276
- this.logger.warn("getTokenBalance: FAILED", data);
1277
- const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1278
- return {
1279
- amount: -1,
1280
- reserved: data.reserved ?? 0,
1281
- unit: "msat",
1282
- apiKey: data.api_key,
1283
- isInvalidApiKey
1284
- };
1285
- }
1286
- } catch (error) {
1287
- this.logger.error("getTokenBalance error", error);
1288
- }
1289
- return { amount: -1, reserved: 0, unit: "sat", apiKey: "" };
1290
- }
1291
- /**
1292
- * Handle topup errors with specific error types
1293
- */
1294
- _handleTopUpError(error, mintUrl, requestId) {
1295
- if (error instanceof Error) {
1296
- if (isNetworkErrorMessage(error.message)) {
1297
- return {
1298
- success: false,
1299
- message: `Failed to connect to the mint: ${mintUrl}`,
1300
- requestId
1301
- };
1302
- }
1303
- if (error.message.includes("Wallet not found")) {
1304
- return {
1305
- success: false,
1306
- message: "Wallet couldn't be loaded. The cashu token was recovered locally.",
1307
- requestId
1308
- };
1309
- }
1310
- return {
1311
- success: false,
1312
- message: error.message,
1313
- requestId
1314
- };
1315
- }
1316
- return {
1317
- success: false,
1318
- message: "Top up failed",
1319
- requestId
1320
- };
1321
- }
1322
- };
1323
-
1324
- export { BalanceManager, CashuSpender };
1325
- //# sourceMappingURL=index.mjs.map
1326
- //# sourceMappingURL=index.mjs.map