@routstr/sdk 0.1.0 → 0.1.2

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.
@@ -0,0 +1,2659 @@
1
+ // core/errors.ts
2
+ var InsufficientBalanceError = class extends Error {
3
+ constructor(required, available, maxMintBalance = 0, maxMintUrl = "") {
4
+ super(
5
+ `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
6
+ );
7
+ this.required = required;
8
+ this.available = available;
9
+ this.maxMintBalance = maxMintBalance;
10
+ this.maxMintUrl = maxMintUrl;
11
+ this.name = "InsufficientBalanceError";
12
+ }
13
+ };
14
+ var ProviderError = class extends Error {
15
+ constructor(baseUrl, statusCode, message, requestId) {
16
+ super(
17
+ `Provider ${baseUrl} returned ${statusCode}: ${message}` + (requestId ? ` (Request ID: ${requestId})` : "")
18
+ );
19
+ this.baseUrl = baseUrl;
20
+ this.statusCode = statusCode;
21
+ this.requestId = requestId;
22
+ this.name = "ProviderError";
23
+ }
24
+ };
25
+ var FailoverError = class extends Error {
26
+ constructor(originalProvider, failedProviders, message) {
27
+ super(
28
+ message || `All providers failed. Original: ${originalProvider}, Failed: ${failedProviders.join(", ")}`
29
+ );
30
+ this.originalProvider = originalProvider;
31
+ this.failedProviders = failedProviders;
32
+ this.name = "FailoverError";
33
+ }
34
+ };
35
+
36
+ // wallet/AuditLogger.ts
37
+ var AuditLogger = class _AuditLogger {
38
+ static instance = null;
39
+ static getInstance() {
40
+ if (!_AuditLogger.instance) {
41
+ _AuditLogger.instance = new _AuditLogger();
42
+ }
43
+ return _AuditLogger.instance;
44
+ }
45
+ async log(entry) {
46
+ const fullEntry = {
47
+ ...entry,
48
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
49
+ };
50
+ const logLine = JSON.stringify(fullEntry) + "\n";
51
+ if (typeof window === "undefined") {
52
+ try {
53
+ const fs = await import('fs');
54
+ const path = await import('path');
55
+ const logPath = path.join(process.cwd(), "audit.log");
56
+ fs.appendFileSync(logPath, logLine);
57
+ } catch (error) {
58
+ console.error("[AuditLogger] Failed to write to file:", error);
59
+ }
60
+ } else {
61
+ console.log("[AUDIT]", logLine.trim());
62
+ }
63
+ }
64
+ async logBalanceSnapshot(action, amounts, options) {
65
+ await this.log({
66
+ action,
67
+ totalBalance: amounts.totalBalance,
68
+ providerBalances: amounts.providerBalances,
69
+ mintBalances: amounts.mintBalances,
70
+ amount: options?.amount,
71
+ mintUrl: options?.mintUrl,
72
+ baseUrl: options?.baseUrl,
73
+ status: options?.status ?? "success",
74
+ details: options?.details
75
+ });
76
+ }
77
+ };
78
+ var auditLogger = AuditLogger.getInstance();
79
+
80
+ // wallet/tokenUtils.ts
81
+ function isNetworkErrorMessage(message) {
82
+ return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
83
+ }
84
+ function getBalanceInSats(balance, unit) {
85
+ return unit === "msat" ? balance / 1e3 : balance;
86
+ }
87
+ function selectMintWithBalance(balances, units, amount, excludeMints = []) {
88
+ for (const mintUrl in balances) {
89
+ if (excludeMints.includes(mintUrl)) {
90
+ continue;
91
+ }
92
+ const balanceInSats = getBalanceInSats(balances[mintUrl], units[mintUrl]);
93
+ if (balanceInSats >= amount) {
94
+ return { selectedMintUrl: mintUrl, selectedMintBalance: balanceInSats };
95
+ }
96
+ }
97
+ return { selectedMintUrl: null, selectedMintBalance: 0 };
98
+ }
99
+
100
+ // wallet/CashuSpender.ts
101
+ var CashuSpender = class {
102
+ constructor(walletAdapter, storageAdapter, _providerRegistry, balanceManager) {
103
+ this.walletAdapter = walletAdapter;
104
+ this.storageAdapter = storageAdapter;
105
+ this._providerRegistry = _providerRegistry;
106
+ this.balanceManager = balanceManager;
107
+ }
108
+ _isBusy = false;
109
+ debugLevel = "WARN";
110
+ async receiveToken(token) {
111
+ const result = await this.walletAdapter.receiveToken(token);
112
+ if (!result.success && result.message?.includes("Failed to fetch mint")) {
113
+ const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
114
+ const existingIndex = cachedTokens.findIndex((t) => t.token === token);
115
+ if (existingIndex === -1) {
116
+ this.storageAdapter.setCachedReceiveTokens([
117
+ ...cachedTokens,
118
+ {
119
+ token,
120
+ amount: result.amount,
121
+ unit: result.unit,
122
+ createdAt: Date.now()
123
+ }
124
+ ]);
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ async _getBalanceState() {
130
+ const mintBalances = await this.walletAdapter.getBalances();
131
+ const units = this.walletAdapter.getMintUnits();
132
+ let totalMintBalance = 0;
133
+ const normalizedMintBalances = {};
134
+ for (const url in mintBalances) {
135
+ const balance = mintBalances[url];
136
+ const unit = units[url];
137
+ const balanceInSats = getBalanceInSats(balance, unit);
138
+ normalizedMintBalances[url] = balanceInSats;
139
+ totalMintBalance += balanceInSats;
140
+ }
141
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
142
+ const providerBalances = {};
143
+ let totalProviderBalance = 0;
144
+ for (const pending of pendingDistribution) {
145
+ providerBalances[pending.baseUrl] = pending.amount;
146
+ totalProviderBalance += pending.amount;
147
+ }
148
+ const apiKeys = this.storageAdapter.getAllApiKeys();
149
+ for (const apiKey of apiKeys) {
150
+ if (!providerBalances[apiKey.baseUrl]) {
151
+ providerBalances[apiKey.baseUrl] = apiKey.balance;
152
+ totalProviderBalance += apiKey.balance;
153
+ }
154
+ }
155
+ return {
156
+ totalBalance: totalMintBalance + totalProviderBalance,
157
+ providerBalances,
158
+ mintBalances: normalizedMintBalances
159
+ };
160
+ }
161
+ async _logTransaction(action, options) {
162
+ const balanceState = await this._getBalanceState();
163
+ await auditLogger.logBalanceSnapshot(action, balanceState, options);
164
+ }
165
+ /**
166
+ * Check if the spender is currently in a critical operation
167
+ */
168
+ get isBusy() {
169
+ return this._isBusy;
170
+ }
171
+ getDebugLevel() {
172
+ return this.debugLevel;
173
+ }
174
+ setDebugLevel(level) {
175
+ this.debugLevel = level;
176
+ }
177
+ _log(level, ...args) {
178
+ const levelPriority = {
179
+ DEBUG: 0,
180
+ WARN: 1,
181
+ ERROR: 2
182
+ };
183
+ if (levelPriority[level] >= levelPriority[this.debugLevel]) {
184
+ switch (level) {
185
+ case "DEBUG":
186
+ console.log(...args);
187
+ break;
188
+ case "WARN":
189
+ console.warn(...args);
190
+ break;
191
+ case "ERROR":
192
+ console.error(...args);
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Spend Cashu tokens with automatic mint selection and retry logic
199
+ * Throws errors on failure instead of returning failed SpendResult
200
+ */
201
+ async spend(options) {
202
+ const {
203
+ mintUrl,
204
+ amount,
205
+ baseUrl,
206
+ reuseToken = false,
207
+ p2pkPubkey,
208
+ excludeMints = [],
209
+ retryCount = 0
210
+ } = options;
211
+ this._isBusy = true;
212
+ try {
213
+ const result = await this._spendInternal({
214
+ mintUrl,
215
+ amount,
216
+ baseUrl,
217
+ reuseToken,
218
+ p2pkPubkey,
219
+ excludeMints,
220
+ retryCount
221
+ });
222
+ if (result.status === "failed" || !result.token) {
223
+ const errorMsg = result.error || `Insufficient balance. Need ${amount} sats.`;
224
+ if (this._isNetworkError(errorMsg)) {
225
+ throw new Error(
226
+ `Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
227
+ );
228
+ }
229
+ if (result.errorDetails) {
230
+ throw new InsufficientBalanceError(
231
+ result.errorDetails.required,
232
+ result.errorDetails.available,
233
+ result.errorDetails.maxMintBalance,
234
+ result.errorDetails.maxMintUrl
235
+ );
236
+ }
237
+ throw new Error(errorMsg);
238
+ }
239
+ return result;
240
+ } finally {
241
+ this._isBusy = false;
242
+ }
243
+ }
244
+ /**
245
+ * Check if error message indicates a network error
246
+ */
247
+ _isNetworkError(message) {
248
+ return isNetworkErrorMessage(message) || message.includes("Your mint") && message.includes("unreachable");
249
+ }
250
+ /**
251
+ * Internal spending logic
252
+ */
253
+ async _spendInternal(options) {
254
+ let {
255
+ mintUrl,
256
+ amount,
257
+ baseUrl,
258
+ reuseToken,
259
+ p2pkPubkey,
260
+ excludeMints,
261
+ retryCount
262
+ } = options;
263
+ this._log(
264
+ "DEBUG",
265
+ `[CashuSpender] _spendInternal: amount=${amount}, mintUrl=${mintUrl}, baseUrl=${baseUrl}, reuseToken=${reuseToken}`
266
+ );
267
+ let adjustedAmount = Math.ceil(amount);
268
+ if (!adjustedAmount || isNaN(adjustedAmount)) {
269
+ this._log(
270
+ "ERROR",
271
+ `[CashuSpender] _spendInternal: Invalid amount: ${amount}`
272
+ );
273
+ return {
274
+ token: null,
275
+ status: "failed",
276
+ balance: 0,
277
+ error: "Please enter a valid amount"
278
+ };
279
+ }
280
+ if (reuseToken && baseUrl) {
281
+ this._log(
282
+ "DEBUG",
283
+ `[CashuSpender] _spendInternal: Attempting to reuse token for ${baseUrl}`
284
+ );
285
+ const existingResult = await this._tryReuseToken(
286
+ baseUrl,
287
+ adjustedAmount,
288
+ mintUrl
289
+ );
290
+ if (existingResult) {
291
+ this._log(
292
+ "DEBUG",
293
+ `[CashuSpender] _spendInternal: Successfully reused token, balance: ${existingResult.balance}`
294
+ );
295
+ return existingResult;
296
+ }
297
+ this._log(
298
+ "DEBUG",
299
+ `[CashuSpender] _spendInternal: Could not reuse token, will create new token`
300
+ );
301
+ }
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
+ );
316
+ this._log(
317
+ "DEBUG",
318
+ `[CashuSpender] _spendInternal: totalBalance=${totalBalance}, totalPending=${totalPending}, adjustedAmount=${adjustedAmount}`
319
+ );
320
+ const totalAvailableBalance = totalBalance + totalPending;
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
+ balances,
329
+ units,
330
+ totalAvailableBalance
331
+ );
332
+ }
333
+ let token = null;
334
+ let selectedMintUrl;
335
+ let spentAmount = adjustedAmount;
336
+ if (this.balanceManager) {
337
+ const tokenResult = await this.balanceManager.createProviderToken({
338
+ mintUrl,
339
+ baseUrl,
340
+ amount: adjustedAmount,
341
+ p2pkPubkey,
342
+ excludeMints,
343
+ retryCount
344
+ });
345
+ if (!tokenResult.success || !tokenResult.token) {
346
+ if ((tokenResult.error || "").includes("Insufficient balance")) {
347
+ return this._createInsufficientBalanceError(
348
+ adjustedAmount,
349
+ balances,
350
+ units,
351
+ totalAvailableBalance
352
+ );
353
+ }
354
+ return {
355
+ token: null,
356
+ status: "failed",
357
+ balance: 0,
358
+ error: tokenResult.error || "Failed to create token"
359
+ };
360
+ }
361
+ token = tokenResult.token;
362
+ selectedMintUrl = tokenResult.selectedMintUrl;
363
+ spentAmount = tokenResult.amountSpent || adjustedAmount;
364
+ } else {
365
+ try {
366
+ token = await this.walletAdapter.sendToken(
367
+ mintUrl,
368
+ adjustedAmount,
369
+ p2pkPubkey
370
+ );
371
+ selectedMintUrl = mintUrl;
372
+ } catch (error) {
373
+ const errorMsg = error instanceof Error ? error.message : String(error);
374
+ return {
375
+ token: null,
376
+ status: "failed",
377
+ balance: 0,
378
+ error: `Error generating token: ${errorMsg}`
379
+ };
380
+ }
381
+ }
382
+ if (token && baseUrl) {
383
+ this.storageAdapter.setToken(baseUrl, token);
384
+ }
385
+ this._logTransaction("spend", {
386
+ amount: spentAmount,
387
+ mintUrl: selectedMintUrl || mintUrl,
388
+ baseUrl,
389
+ status: "success"
390
+ });
391
+ this._log(
392
+ "DEBUG",
393
+ `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
394
+ );
395
+ return {
396
+ token,
397
+ status: "success",
398
+ balance: spentAmount,
399
+ unit: (selectedMintUrl ? units[selectedMintUrl] : units[mintUrl]) || "sat"
400
+ };
401
+ }
402
+ /**
403
+ * Try to reuse an existing token
404
+ */
405
+ async _tryReuseToken(baseUrl, amount, mintUrl) {
406
+ const storedToken = this.storageAdapter.getToken(baseUrl);
407
+ if (!storedToken) return null;
408
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
409
+ const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
410
+ this._log("DEBUG", "RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
411
+ if (balanceForBaseUrl > amount) {
412
+ const units = this.walletAdapter.getMintUnits();
413
+ const unit = units[mintUrl] || "sat";
414
+ return {
415
+ token: storedToken,
416
+ status: "success",
417
+ balance: balanceForBaseUrl,
418
+ unit
419
+ };
420
+ }
421
+ if (this.balanceManager) {
422
+ const topUpAmount = Math.ceil(amount * 1.2 - balanceForBaseUrl);
423
+ const topUpResult = await this.balanceManager.topUp({
424
+ mintUrl,
425
+ baseUrl,
426
+ amount: topUpAmount
427
+ });
428
+ this._log("DEBUG", "TOPUP ", topUpResult);
429
+ if (topUpResult.success && topUpResult.toppedUpAmount) {
430
+ const newBalance = balanceForBaseUrl + topUpResult.toppedUpAmount;
431
+ const units = this.walletAdapter.getMintUnits();
432
+ const unit = units[mintUrl] || "sat";
433
+ this._logTransaction("topup", {
434
+ amount: topUpResult.toppedUpAmount,
435
+ mintUrl,
436
+ baseUrl,
437
+ status: "success"
438
+ });
439
+ return {
440
+ token: storedToken,
441
+ status: "success",
442
+ balance: newBalance,
443
+ unit
444
+ };
445
+ }
446
+ const providerBalance = await this._getProviderTokenBalance(
447
+ baseUrl,
448
+ storedToken
449
+ );
450
+ this._log("DEBUG", providerBalance);
451
+ if (providerBalance <= 0) {
452
+ this.storageAdapter.removeToken(baseUrl);
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ /**
458
+ * Refund specific providers without retrying spend
459
+ */
460
+ async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
461
+ const results = [];
462
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
463
+ const toRefund = pendingDistribution.filter(
464
+ (p) => baseUrls.includes(p.baseUrl)
465
+ );
466
+ const refundResults = await Promise.allSettled(
467
+ toRefund.map(async (pending) => {
468
+ const token = this.storageAdapter.getToken(pending.baseUrl);
469
+ this._log("DEBUG", token, this.balanceManager);
470
+ if (!token || !this.balanceManager) {
471
+ return { baseUrl: pending.baseUrl, success: false };
472
+ }
473
+ const tokenBalance = await this.balanceManager.getTokenBalance(
474
+ token,
475
+ pending.baseUrl
476
+ );
477
+ if (tokenBalance.reserved > 0) {
478
+ return { baseUrl: pending.baseUrl, success: false };
479
+ }
480
+ const result = await this.balanceManager.refund({
481
+ mintUrl,
482
+ baseUrl: pending.baseUrl,
483
+ token
484
+ });
485
+ this._log("DEBUG", result);
486
+ if (result.success) {
487
+ this.storageAdapter.removeToken(pending.baseUrl);
488
+ }
489
+ return { baseUrl: pending.baseUrl, success: result.success };
490
+ })
491
+ );
492
+ results.push(
493
+ ...refundResults.map(
494
+ (r) => r.status === "fulfilled" ? r.value : { baseUrl: "", success: false }
495
+ )
496
+ );
497
+ if (refundApiKeys) {
498
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
499
+ const apiKeysToRefund = apiKeyDistribution.filter(
500
+ (p) => baseUrls.includes(p.baseUrl)
501
+ );
502
+ for (const apiKeyEntry of apiKeysToRefund) {
503
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
504
+ apiKeyEntry.baseUrl
505
+ );
506
+ if (apiKeyEntryFull && this.balanceManager) {
507
+ const refundResult = await this.balanceManager.refundApiKey({
508
+ mintUrl,
509
+ baseUrl: apiKeyEntry.baseUrl,
510
+ apiKey: apiKeyEntryFull.key
511
+ });
512
+ if (refundResult.success) {
513
+ this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
514
+ }
515
+ results.push({
516
+ baseUrl: apiKeyEntry.baseUrl,
517
+ success: refundResult.success
518
+ });
519
+ } else {
520
+ results.push({
521
+ baseUrl: apiKeyEntry.baseUrl,
522
+ success: false
523
+ });
524
+ }
525
+ }
526
+ }
527
+ return results;
528
+ }
529
+ /**
530
+ * Create an insufficient balance error result
531
+ */
532
+ _createInsufficientBalanceError(required, balances, units, availableBalance) {
533
+ let maxBalance = 0;
534
+ let maxMintUrl = "";
535
+ for (const mintUrl in balances) {
536
+ const balance = balances[mintUrl];
537
+ const unit = units[mintUrl];
538
+ const balanceInSats = getBalanceInSats(balance, unit);
539
+ if (balanceInSats > maxBalance) {
540
+ maxBalance = balanceInSats;
541
+ maxMintUrl = mintUrl;
542
+ }
543
+ }
544
+ const error = new InsufficientBalanceError(
545
+ required,
546
+ availableBalance ?? maxBalance,
547
+ maxBalance,
548
+ maxMintUrl
549
+ );
550
+ return {
551
+ token: null,
552
+ status: "failed",
553
+ balance: 0,
554
+ error: error.message,
555
+ errorDetails: {
556
+ required,
557
+ available: availableBalance ?? maxBalance,
558
+ maxMintBalance: maxBalance,
559
+ maxMintUrl
560
+ }
561
+ };
562
+ }
563
+ async _getProviderTokenBalance(baseUrl, token) {
564
+ try {
565
+ const response = await fetch(`${baseUrl}v1/wallet/info`, {
566
+ headers: {
567
+ Authorization: `Bearer ${token}`
568
+ }
569
+ });
570
+ if (response.ok) {
571
+ const data = await response.json();
572
+ return data.balance / 1e3;
573
+ }
574
+ } catch {
575
+ return 0;
576
+ }
577
+ return 0;
578
+ }
579
+ };
580
+
581
+ // wallet/BalanceManager.ts
582
+ var BalanceManager = class {
583
+ constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender) {
584
+ this.walletAdapter = walletAdapter;
585
+ this.storageAdapter = storageAdapter;
586
+ this.providerRegistry = providerRegistry;
587
+ if (cashuSpender) {
588
+ this.cashuSpender = cashuSpender;
589
+ } else {
590
+ this.cashuSpender = new CashuSpender(
591
+ walletAdapter,
592
+ storageAdapter,
593
+ providerRegistry,
594
+ this
595
+ );
596
+ }
597
+ }
598
+ cashuSpender;
599
+ /**
600
+ * Unified refund - handles both NIP-60 and legacy wallet refunds
601
+ */
602
+ async refund(options) {
603
+ const { mintUrl, baseUrl, token: providedToken } = options;
604
+ const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
605
+ if (!storedToken) {
606
+ console.log("[BalanceManager] No token to refund, returning early");
607
+ return { success: true, message: "No API key to refund" };
608
+ }
609
+ let fetchResult;
610
+ try {
611
+ fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
612
+ if (!fetchResult.success) {
613
+ return {
614
+ success: false,
615
+ message: fetchResult.error || "Refund failed",
616
+ requestId: fetchResult.requestId
617
+ };
618
+ }
619
+ if (!fetchResult.token) {
620
+ return {
621
+ success: false,
622
+ message: "No token received from refund",
623
+ requestId: fetchResult.requestId
624
+ };
625
+ }
626
+ if (fetchResult.error === "No balance to refund") {
627
+ console.log(
628
+ "[BalanceManager] No balance to refund, removing stored token"
629
+ );
630
+ this.storageAdapter.removeToken(baseUrl);
631
+ return { success: true, message: "No balance to refund" };
632
+ }
633
+ const receiveResult = await this.cashuSpender.receiveToken(
634
+ fetchResult.token
635
+ );
636
+ const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
637
+ if (!providedToken) {
638
+ this.storageAdapter.removeToken(baseUrl);
639
+ }
640
+ return {
641
+ success: receiveResult.success,
642
+ refundedAmount: totalAmountMsat,
643
+ requestId: fetchResult.requestId
644
+ };
645
+ } catch (error) {
646
+ console.error("[BalanceManager] Refund error", error);
647
+ return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
648
+ }
649
+ }
650
+ /**
651
+ * Refund API key balance - convert remaining API key balance to cashu token
652
+ */
653
+ async refundApiKey(options) {
654
+ const { mintUrl, baseUrl, apiKey } = options;
655
+ if (!apiKey) {
656
+ return { success: false, message: "No API key to refund" };
657
+ }
658
+ let fetchResult;
659
+ try {
660
+ fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
661
+ if (!fetchResult.success) {
662
+ return {
663
+ success: false,
664
+ message: fetchResult.error || "API key refund failed",
665
+ requestId: fetchResult.requestId
666
+ };
667
+ }
668
+ if (!fetchResult.token) {
669
+ return {
670
+ success: false,
671
+ message: "No token received from API key refund",
672
+ requestId: fetchResult.requestId
673
+ };
674
+ }
675
+ if (fetchResult.error === "No balance to refund") {
676
+ return { success: false, message: "No balance to refund" };
677
+ }
678
+ const receiveResult = await this.cashuSpender.receiveToken(
679
+ fetchResult.token
680
+ );
681
+ const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
682
+ return {
683
+ success: receiveResult.success,
684
+ refundedAmount: totalAmountMsat,
685
+ requestId: fetchResult.requestId
686
+ };
687
+ } catch (error) {
688
+ console.error("[BalanceManager] API key refund error", error);
689
+ return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
690
+ }
691
+ }
692
+ /**
693
+ * Fetch refund token from provider API using API key authentication
694
+ */
695
+ async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
696
+ if (!baseUrl) {
697
+ return {
698
+ success: false,
699
+ error: "No base URL configured"
700
+ };
701
+ }
702
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
703
+ const url = `${normalizedBaseUrl}v1/wallet/refund`;
704
+ const controller = new AbortController();
705
+ const timeoutId = setTimeout(() => {
706
+ controller.abort();
707
+ }, 6e4);
708
+ try {
709
+ const response = await fetch(url, {
710
+ method: "POST",
711
+ headers: {
712
+ Authorization: `Bearer ${apiKey}`,
713
+ "Content-Type": "application/json"
714
+ },
715
+ signal: controller.signal
716
+ });
717
+ clearTimeout(timeoutId);
718
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
719
+ if (!response.ok) {
720
+ const errorData = await response.json().catch(() => ({}));
721
+ return {
722
+ success: false,
723
+ requestId,
724
+ error: `API key refund failed: ${errorData?.detail || response.statusText}`
725
+ };
726
+ }
727
+ const data = await response.json();
728
+ return {
729
+ success: true,
730
+ token: data.token,
731
+ requestId
732
+ };
733
+ } catch (error) {
734
+ clearTimeout(timeoutId);
735
+ console.error(
736
+ "[BalanceManager._fetchRefundTokenWithApiKey] Fetch error",
737
+ error
738
+ );
739
+ if (error instanceof Error) {
740
+ if (error.name === "AbortError") {
741
+ return {
742
+ success: false,
743
+ error: "Request timed out after 1 minute"
744
+ };
745
+ }
746
+ return {
747
+ success: false,
748
+ error: error.message
749
+ };
750
+ }
751
+ return {
752
+ success: false,
753
+ error: "Unknown error occurred during API key refund request"
754
+ };
755
+ }
756
+ }
757
+ /**
758
+ * Top up API key balance with a cashu token
759
+ */
760
+ async topUp(options) {
761
+ const { mintUrl, baseUrl, amount, token: providedToken } = options;
762
+ if (!amount || amount <= 0) {
763
+ return { success: false, message: "Invalid top up amount" };
764
+ }
765
+ const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
766
+ if (!storedToken) {
767
+ return { success: false, message: "No API key available for top up" };
768
+ }
769
+ let cashuToken = null;
770
+ let requestId;
771
+ try {
772
+ const tokenResult = await this.createProviderToken({
773
+ mintUrl,
774
+ baseUrl,
775
+ amount
776
+ });
777
+ if (!tokenResult.success || !tokenResult.token) {
778
+ return {
779
+ success: false,
780
+ message: tokenResult.error || "Unable to create top up token"
781
+ };
782
+ }
783
+ cashuToken = tokenResult.token;
784
+ const topUpResult = await this._postTopUp(
785
+ baseUrl,
786
+ storedToken,
787
+ cashuToken
788
+ );
789
+ requestId = topUpResult.requestId;
790
+ console.log(topUpResult);
791
+ if (!topUpResult.success) {
792
+ await this._recoverFailedTopUp(cashuToken);
793
+ return {
794
+ success: false,
795
+ message: topUpResult.error || "Top up failed",
796
+ requestId,
797
+ recoveredToken: true
798
+ };
799
+ }
800
+ return {
801
+ success: true,
802
+ toppedUpAmount: amount,
803
+ requestId
804
+ };
805
+ } catch (error) {
806
+ if (cashuToken) {
807
+ await this._recoverFailedTopUp(cashuToken);
808
+ }
809
+ return this._handleTopUpError(error, mintUrl, requestId);
810
+ }
811
+ }
812
+ async createProviderToken(options) {
813
+ const {
814
+ mintUrl,
815
+ baseUrl,
816
+ amount,
817
+ retryCount = 0,
818
+ excludeMints = [],
819
+ p2pkPubkey
820
+ } = options;
821
+ const adjustedAmount = Math.ceil(amount);
822
+ if (!adjustedAmount || isNaN(adjustedAmount)) {
823
+ return { success: false, error: "Invalid top up amount" };
824
+ }
825
+ const balances = await this.walletAdapter.getBalances();
826
+ const units = this.walletAdapter.getMintUnits();
827
+ let totalMintBalance = 0;
828
+ for (const url in balances) {
829
+ const unit = units[url];
830
+ const balanceInSats = getBalanceInSats(balances[url], unit);
831
+ totalMintBalance += balanceInSats;
832
+ }
833
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
834
+ const refundablePending = pendingDistribution.filter((entry) => entry.baseUrl !== baseUrl).reduce((sum, entry) => sum + entry.amount, 0);
835
+ if (totalMintBalance < adjustedAmount && totalMintBalance + refundablePending >= adjustedAmount && retryCount < 1) {
836
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
837
+ return this.createProviderToken({
838
+ ...options,
839
+ retryCount: retryCount + 1
840
+ });
841
+ }
842
+ const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
843
+ let requiredAmount = adjustedAmount;
844
+ const supportedMintsOnly = providerMints.length > 0;
845
+ let candidates = this._selectCandidateMints({
846
+ balances,
847
+ units,
848
+ amount: requiredAmount,
849
+ preferredMintUrl: mintUrl,
850
+ excludeMints,
851
+ allowedMints: supportedMintsOnly ? providerMints : void 0
852
+ });
853
+ if (candidates.length === 0 && supportedMintsOnly) {
854
+ requiredAmount += 2;
855
+ candidates = this._selectCandidateMints({
856
+ balances,
857
+ units,
858
+ amount: requiredAmount,
859
+ preferredMintUrl: mintUrl,
860
+ excludeMints
861
+ });
862
+ }
863
+ if (candidates.length === 0) {
864
+ let maxBalance = 0;
865
+ let maxMintUrl = "";
866
+ for (const mintUrl2 in balances) {
867
+ const balance = balances[mintUrl2];
868
+ const unit = units[mintUrl2];
869
+ const balanceInSats = getBalanceInSats(balance, unit);
870
+ if (balanceInSats > maxBalance) {
871
+ maxBalance = balanceInSats;
872
+ maxMintUrl = mintUrl2;
873
+ }
874
+ }
875
+ const error = new InsufficientBalanceError(
876
+ adjustedAmount,
877
+ totalMintBalance,
878
+ maxBalance,
879
+ maxMintUrl
880
+ );
881
+ return { success: false, error: error.message };
882
+ }
883
+ let lastError;
884
+ for (const candidateMint of candidates) {
885
+ try {
886
+ const token = await this.walletAdapter.sendToken(
887
+ candidateMint,
888
+ requiredAmount,
889
+ p2pkPubkey
890
+ );
891
+ return {
892
+ success: true,
893
+ token,
894
+ selectedMintUrl: candidateMint,
895
+ amountSpent: requiredAmount
896
+ };
897
+ } catch (error) {
898
+ if (error instanceof Error) {
899
+ lastError = error.message;
900
+ if (isNetworkErrorMessage(error.message)) {
901
+ continue;
902
+ }
903
+ }
904
+ return {
905
+ success: false,
906
+ error: lastError || "Failed to create top up token"
907
+ };
908
+ }
909
+ }
910
+ return {
911
+ success: false,
912
+ error: lastError || "All candidate mints failed while creating top up token"
913
+ };
914
+ }
915
+ _selectCandidateMints(options) {
916
+ const {
917
+ balances,
918
+ units,
919
+ amount,
920
+ preferredMintUrl,
921
+ excludeMints,
922
+ allowedMints
923
+ } = options;
924
+ const candidates = [];
925
+ const { selectedMintUrl: firstMint } = selectMintWithBalance(
926
+ balances,
927
+ units,
928
+ amount,
929
+ excludeMints
930
+ );
931
+ if (firstMint && (!allowedMints || allowedMints.length === 0 || allowedMints.includes(firstMint))) {
932
+ candidates.push(firstMint);
933
+ }
934
+ const canUseMint = (mint) => {
935
+ if (excludeMints.includes(mint)) return false;
936
+ if (allowedMints && allowedMints.length > 0 && !allowedMints.includes(mint)) {
937
+ return false;
938
+ }
939
+ const rawBalance = balances[mint] || 0;
940
+ const unit = units[mint];
941
+ const balanceInSats = getBalanceInSats(rawBalance, unit);
942
+ return balanceInSats >= amount;
943
+ };
944
+ if (preferredMintUrl && canUseMint(preferredMintUrl) && !candidates.includes(preferredMintUrl)) {
945
+ candidates.push(preferredMintUrl);
946
+ }
947
+ for (const mint in balances) {
948
+ if (mint === preferredMintUrl || candidates.includes(mint)) continue;
949
+ if (canUseMint(mint)) {
950
+ candidates.push(mint);
951
+ }
952
+ }
953
+ return candidates;
954
+ }
955
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
956
+ const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
957
+ const toRefund = pendingDistribution.filter(
958
+ (pending) => pending.baseUrl !== baseUrl
959
+ );
960
+ const refundResults = await Promise.allSettled(
961
+ toRefund.map(async (pending) => {
962
+ const token = this.storageAdapter.getToken(pending.baseUrl);
963
+ if (!token) {
964
+ return { baseUrl: pending.baseUrl, success: false };
965
+ }
966
+ const tokenBalance = await this.getTokenBalance(token, pending.baseUrl);
967
+ if (tokenBalance.reserved > 0) {
968
+ return { baseUrl: pending.baseUrl, success: false };
969
+ }
970
+ const result = await this.refund({
971
+ mintUrl,
972
+ baseUrl: pending.baseUrl,
973
+ token
974
+ });
975
+ return { baseUrl: pending.baseUrl, success: result.success };
976
+ })
977
+ );
978
+ for (const result of refundResults) {
979
+ if (result.status === "fulfilled" && result.value.success) {
980
+ this.storageAdapter.removeToken(result.value.baseUrl);
981
+ }
982
+ }
983
+ }
984
+ /**
985
+ * Fetch refund token from provider API
986
+ */
987
+ async _fetchRefundToken(baseUrl, storedToken) {
988
+ if (!baseUrl) {
989
+ return {
990
+ success: false,
991
+ error: "No base URL configured"
992
+ };
993
+ }
994
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
995
+ const url = `${normalizedBaseUrl}v1/wallet/refund`;
996
+ const controller = new AbortController();
997
+ const timeoutId = setTimeout(() => {
998
+ controller.abort();
999
+ }, 6e4);
1000
+ try {
1001
+ const response = await fetch(url, {
1002
+ method: "POST",
1003
+ headers: {
1004
+ Authorization: `Bearer ${storedToken}`,
1005
+ "Content-Type": "application/json"
1006
+ },
1007
+ signal: controller.signal
1008
+ });
1009
+ clearTimeout(timeoutId);
1010
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
1011
+ if (!response.ok) {
1012
+ const errorData = await response.json().catch(() => ({}));
1013
+ if (response.status === 400 && errorData?.detail === "No balance to refund") {
1014
+ this.storageAdapter.removeToken(baseUrl);
1015
+ return {
1016
+ success: false,
1017
+ requestId,
1018
+ error: "No balance to refund"
1019
+ };
1020
+ }
1021
+ return {
1022
+ success: false,
1023
+ requestId,
1024
+ error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1025
+ };
1026
+ }
1027
+ const data = await response.json();
1028
+ console.log("refund rsule", data);
1029
+ return {
1030
+ success: true,
1031
+ token: data.token,
1032
+ requestId
1033
+ };
1034
+ } catch (error) {
1035
+ clearTimeout(timeoutId);
1036
+ console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1037
+ if (error instanceof Error) {
1038
+ if (error.name === "AbortError") {
1039
+ return {
1040
+ success: false,
1041
+ error: "Request timed out after 1 minute"
1042
+ };
1043
+ }
1044
+ return {
1045
+ success: false,
1046
+ error: error.message
1047
+ };
1048
+ }
1049
+ return {
1050
+ success: false,
1051
+ error: "Unknown error occurred during refund request"
1052
+ };
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Post topup request to provider API
1057
+ */
1058
+ async _postTopUp(baseUrl, storedToken, cashuToken) {
1059
+ if (!baseUrl) {
1060
+ return {
1061
+ success: false,
1062
+ error: "No base URL configured"
1063
+ };
1064
+ }
1065
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1066
+ const url = `${normalizedBaseUrl}v1/wallet/topup?cashu_token=${encodeURIComponent(
1067
+ cashuToken
1068
+ )}`;
1069
+ const controller = new AbortController();
1070
+ const timeoutId = setTimeout(() => {
1071
+ controller.abort();
1072
+ }, 6e4);
1073
+ try {
1074
+ const response = await fetch(url, {
1075
+ method: "POST",
1076
+ headers: {
1077
+ Authorization: `Bearer ${storedToken}`,
1078
+ "Content-Type": "application/json"
1079
+ },
1080
+ signal: controller.signal
1081
+ });
1082
+ clearTimeout(timeoutId);
1083
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
1084
+ if (!response.ok) {
1085
+ const errorData = await response.json().catch(() => ({}));
1086
+ return {
1087
+ success: false,
1088
+ requestId,
1089
+ error: errorData?.detail || `Top up failed with status ${response.status}`
1090
+ };
1091
+ }
1092
+ return { success: true, requestId };
1093
+ } catch (error) {
1094
+ clearTimeout(timeoutId);
1095
+ console.error("[BalanceManager._postTopUp] Fetch error", error);
1096
+ if (error instanceof Error) {
1097
+ if (error.name === "AbortError") {
1098
+ return {
1099
+ success: false,
1100
+ error: "Request timed out after 1 minute"
1101
+ };
1102
+ }
1103
+ return {
1104
+ success: false,
1105
+ error: error.message
1106
+ };
1107
+ }
1108
+ return {
1109
+ success: false,
1110
+ error: "Unknown error occurred during top up request"
1111
+ };
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Attempt to receive token back after failed top up
1116
+ */
1117
+ async _recoverFailedTopUp(cashuToken) {
1118
+ try {
1119
+ await this.cashuSpender.receiveToken(cashuToken);
1120
+ } catch (error) {
1121
+ console.error(
1122
+ "[BalanceManager._recoverFailedTopUp] Failed to recover token",
1123
+ error
1124
+ );
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Handle refund errors with specific error types
1129
+ */
1130
+ _handleRefundError(error, mintUrl, requestId) {
1131
+ if (error instanceof Error) {
1132
+ if (isNetworkErrorMessage(error.message)) {
1133
+ return {
1134
+ success: false,
1135
+ message: `Failed to connect to the mint: ${mintUrl}`,
1136
+ requestId
1137
+ };
1138
+ }
1139
+ if (error.message.includes("Wallet not found")) {
1140
+ return {
1141
+ success: false,
1142
+ message: `Wallet couldn't be loaded. Please save this refunded cashu token manually.`,
1143
+ requestId
1144
+ };
1145
+ }
1146
+ return {
1147
+ success: false,
1148
+ message: error.message,
1149
+ requestId
1150
+ };
1151
+ }
1152
+ return {
1153
+ success: false,
1154
+ message: "Refund failed",
1155
+ requestId
1156
+ };
1157
+ }
1158
+ /**
1159
+ * Get token balance from provider
1160
+ */
1161
+ async getTokenBalance(token, baseUrl) {
1162
+ try {
1163
+ const response = await fetch(`${baseUrl}v1/wallet/info`, {
1164
+ headers: {
1165
+ Authorization: `Bearer ${token}`
1166
+ }
1167
+ });
1168
+ if (response.ok) {
1169
+ const data = await response.json();
1170
+ console.log("TOKENA FASJDFAS", data);
1171
+ return {
1172
+ amount: data.balance,
1173
+ reserved: data.reserved ?? 0,
1174
+ unit: "msat",
1175
+ apiKey: data.api_key
1176
+ };
1177
+ } else {
1178
+ console.log(response.status);
1179
+ const data = await response.json();
1180
+ console.log("FAILED ", data);
1181
+ }
1182
+ } catch (error) {
1183
+ console.error("ERRORR IN RESTPONSE", error);
1184
+ }
1185
+ return { amount: -1, reserved: 0, unit: "sat", apiKey: "" };
1186
+ }
1187
+ /**
1188
+ * Handle topup errors with specific error types
1189
+ */
1190
+ _handleTopUpError(error, mintUrl, requestId) {
1191
+ if (error instanceof Error) {
1192
+ if (isNetworkErrorMessage(error.message)) {
1193
+ return {
1194
+ success: false,
1195
+ message: `Failed to connect to the mint: ${mintUrl}`,
1196
+ requestId
1197
+ };
1198
+ }
1199
+ if (error.message.includes("Wallet not found")) {
1200
+ return {
1201
+ success: false,
1202
+ message: "Wallet couldn't be loaded. The cashu token was recovered locally.",
1203
+ requestId
1204
+ };
1205
+ }
1206
+ return {
1207
+ success: false,
1208
+ message: error.message,
1209
+ requestId
1210
+ };
1211
+ }
1212
+ return {
1213
+ success: false,
1214
+ message: "Top up failed",
1215
+ requestId
1216
+ };
1217
+ }
1218
+ };
1219
+
1220
+ // client/StreamProcessor.ts
1221
+ var StreamProcessor = class {
1222
+ accumulatedContent = "";
1223
+ accumulatedThinking = "";
1224
+ accumulatedImages = [];
1225
+ isInThinking = false;
1226
+ isInContent = false;
1227
+ /**
1228
+ * Process a streaming response
1229
+ */
1230
+ async process(response, callbacks, modelId) {
1231
+ if (!response.body) {
1232
+ throw new Error("Response body is not available");
1233
+ }
1234
+ const reader = response.body.getReader();
1235
+ const decoder = new TextDecoder("utf-8");
1236
+ let buffer = "";
1237
+ this.accumulatedContent = "";
1238
+ this.accumulatedThinking = "";
1239
+ this.accumulatedImages = [];
1240
+ this.isInThinking = false;
1241
+ this.isInContent = false;
1242
+ let usage;
1243
+ let model;
1244
+ let finish_reason;
1245
+ let citations;
1246
+ let annotations;
1247
+ try {
1248
+ while (true) {
1249
+ const { done, value } = await reader.read();
1250
+ if (done) {
1251
+ break;
1252
+ }
1253
+ const chunk = decoder.decode(value, { stream: true });
1254
+ buffer += chunk;
1255
+ const lines = buffer.split("\n");
1256
+ buffer = lines.pop() || "";
1257
+ for (const line of lines) {
1258
+ const parsed = this._parseLine(line);
1259
+ if (!parsed) continue;
1260
+ if (parsed.content) {
1261
+ this._handleContent(parsed.content, callbacks, modelId);
1262
+ }
1263
+ if (parsed.reasoning) {
1264
+ this._handleThinking(parsed.reasoning, callbacks);
1265
+ }
1266
+ if (parsed.usage) {
1267
+ usage = parsed.usage;
1268
+ }
1269
+ if (parsed.model) {
1270
+ model = parsed.model;
1271
+ }
1272
+ if (parsed.finish_reason) {
1273
+ finish_reason = parsed.finish_reason;
1274
+ }
1275
+ if (parsed.citations) {
1276
+ citations = parsed.citations;
1277
+ }
1278
+ if (parsed.annotations) {
1279
+ annotations = parsed.annotations;
1280
+ }
1281
+ if (parsed.images) {
1282
+ this._mergeImages(parsed.images);
1283
+ }
1284
+ }
1285
+ }
1286
+ } finally {
1287
+ reader.releaseLock();
1288
+ }
1289
+ return {
1290
+ content: this.accumulatedContent,
1291
+ thinking: this.accumulatedThinking || void 0,
1292
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1293
+ usage,
1294
+ model,
1295
+ finish_reason,
1296
+ citations,
1297
+ annotations
1298
+ };
1299
+ }
1300
+ /**
1301
+ * Parse a single SSE line
1302
+ */
1303
+ _parseLine(line) {
1304
+ if (!line.trim()) return null;
1305
+ if (!line.startsWith("data: ")) {
1306
+ return null;
1307
+ }
1308
+ const jsonData = line.slice(6);
1309
+ if (jsonData === "[DONE]") {
1310
+ return null;
1311
+ }
1312
+ try {
1313
+ const parsed = JSON.parse(jsonData);
1314
+ const result = {};
1315
+ if (parsed.choices?.[0]?.delta?.content) {
1316
+ result.content = parsed.choices[0].delta.content;
1317
+ }
1318
+ if (parsed.choices?.[0]?.delta?.reasoning) {
1319
+ result.reasoning = parsed.choices[0].delta.reasoning;
1320
+ }
1321
+ if (parsed.usage) {
1322
+ result.usage = {
1323
+ total_tokens: parsed.usage.total_tokens,
1324
+ prompt_tokens: parsed.usage.prompt_tokens,
1325
+ completion_tokens: parsed.usage.completion_tokens
1326
+ };
1327
+ }
1328
+ if (parsed.model) {
1329
+ result.model = parsed.model;
1330
+ }
1331
+ if (parsed.citations) {
1332
+ result.citations = parsed.citations;
1333
+ }
1334
+ if (parsed.annotations) {
1335
+ result.annotations = parsed.annotations;
1336
+ }
1337
+ if (parsed.choices?.[0]?.finish_reason) {
1338
+ result.finish_reason = parsed.choices[0].finish_reason;
1339
+ }
1340
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
1341
+ if (images && Array.isArray(images)) {
1342
+ result.images = images;
1343
+ }
1344
+ return result;
1345
+ } catch {
1346
+ return null;
1347
+ }
1348
+ }
1349
+ /**
1350
+ * Handle content delta with thinking support
1351
+ */
1352
+ _handleContent(content, callbacks, modelId) {
1353
+ if (this.isInThinking && !this.isInContent) {
1354
+ this.accumulatedThinking += "</thinking>";
1355
+ callbacks.onThinking(this.accumulatedThinking);
1356
+ this.isInThinking = false;
1357
+ this.isInContent = true;
1358
+ }
1359
+ if (modelId) {
1360
+ this._extractThinkingFromContent(content, callbacks);
1361
+ } else {
1362
+ this.accumulatedContent += content;
1363
+ }
1364
+ callbacks.onContent(this.accumulatedContent);
1365
+ }
1366
+ /**
1367
+ * Handle thinking/reasoning content
1368
+ */
1369
+ _handleThinking(reasoning, callbacks) {
1370
+ if (!this.isInThinking) {
1371
+ this.accumulatedThinking += "<thinking> ";
1372
+ this.isInThinking = true;
1373
+ }
1374
+ this.accumulatedThinking += reasoning;
1375
+ callbacks.onThinking(this.accumulatedThinking);
1376
+ }
1377
+ /**
1378
+ * Extract thinking blocks from content (for models with inline thinking)
1379
+ */
1380
+ _extractThinkingFromContent(content, callbacks) {
1381
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
1382
+ for (const part of parts) {
1383
+ if (part === "<thinking>") {
1384
+ this.isInThinking = true;
1385
+ if (!this.accumulatedThinking.includes("<thinking>")) {
1386
+ this.accumulatedThinking += "<thinking> ";
1387
+ }
1388
+ } else if (part === "</thinking>") {
1389
+ this.isInThinking = false;
1390
+ this.accumulatedThinking += "</thinking>";
1391
+ } else if (this.isInThinking) {
1392
+ this.accumulatedThinking += part;
1393
+ } else {
1394
+ this.accumulatedContent += part;
1395
+ }
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Merge images into accumulated array, avoiding duplicates
1400
+ */
1401
+ _mergeImages(newImages) {
1402
+ for (const img of newImages) {
1403
+ const newUrl = img.image_url?.url;
1404
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
1405
+ const existingUrl = existing.image_url?.url;
1406
+ if (newUrl && existingUrl) {
1407
+ return existingUrl === newUrl;
1408
+ }
1409
+ if (img.index !== void 0 && existing.index !== void 0) {
1410
+ return existing.index === img.index;
1411
+ }
1412
+ return false;
1413
+ });
1414
+ if (existingIndex === -1) {
1415
+ this.accumulatedImages.push(img);
1416
+ } else {
1417
+ this.accumulatedImages[existingIndex] = img;
1418
+ }
1419
+ }
1420
+ }
1421
+ };
1422
+
1423
+ // utils/torUtils.ts
1424
+ var TOR_ONION_SUFFIX = ".onion";
1425
+ var isTorContext = () => {
1426
+ if (typeof window === "undefined") return false;
1427
+ const hostname = window.location.hostname.toLowerCase();
1428
+ return hostname.endsWith(TOR_ONION_SUFFIX);
1429
+ };
1430
+ var isOnionUrl = (url) => {
1431
+ if (!url) return false;
1432
+ const trimmed = url.trim().toLowerCase();
1433
+ if (!trimmed) return false;
1434
+ try {
1435
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1436
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1437
+ } catch {
1438
+ return trimmed.includes(TOR_ONION_SUFFIX);
1439
+ }
1440
+ };
1441
+
1442
+ // client/ProviderManager.ts
1443
+ function getImageResolutionFromDataUrl(dataUrl) {
1444
+ try {
1445
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1446
+ return null;
1447
+ const commaIdx = dataUrl.indexOf(",");
1448
+ if (commaIdx === -1) return null;
1449
+ const meta = dataUrl.slice(5, commaIdx);
1450
+ const base64 = dataUrl.slice(commaIdx + 1);
1451
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1452
+ const len = binary.length;
1453
+ const bytes = new Uint8Array(len);
1454
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1455
+ const isPNG = meta.includes("image/png");
1456
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1457
+ if (isPNG) {
1458
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1459
+ for (let i = 0; i < sig.length; i++) {
1460
+ if (bytes[i] !== sig[i]) return null;
1461
+ }
1462
+ const view = new DataView(
1463
+ bytes.buffer,
1464
+ bytes.byteOffset,
1465
+ bytes.byteLength
1466
+ );
1467
+ const width = view.getUint32(16, false);
1468
+ const height = view.getUint32(20, false);
1469
+ if (width > 0 && height > 0) return { width, height };
1470
+ return null;
1471
+ }
1472
+ if (isJPEG) {
1473
+ let offset = 0;
1474
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1475
+ while (offset < bytes.length) {
1476
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
1477
+ if (offset + 1 >= bytes.length) break;
1478
+ while (bytes[offset] === 255) offset++;
1479
+ const marker = bytes[offset++];
1480
+ if (marker === 216 || marker === 217) continue;
1481
+ if (offset + 1 >= bytes.length) break;
1482
+ const length = bytes[offset] << 8 | bytes[offset + 1];
1483
+ offset += 2;
1484
+ if (marker === 192 || marker === 194) {
1485
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
1486
+ const precision = bytes[offset];
1487
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1488
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1489
+ if (precision > 0 && width > 0 && height > 0)
1490
+ return { width, height };
1491
+ return null;
1492
+ } else {
1493
+ offset += length - 2;
1494
+ }
1495
+ }
1496
+ return null;
1497
+ }
1498
+ return null;
1499
+ } catch {
1500
+ return null;
1501
+ }
1502
+ }
1503
+ function calculateImageTokens(width, height, detail = "auto") {
1504
+ if (detail === "low") return 85;
1505
+ let w = width;
1506
+ let h = height;
1507
+ if (w > 2048 || h > 2048) {
1508
+ const aspectRatio = w / h;
1509
+ if (w > h) {
1510
+ w = 2048;
1511
+ h = Math.floor(w / aspectRatio);
1512
+ } else {
1513
+ h = 2048;
1514
+ w = Math.floor(h * aspectRatio);
1515
+ }
1516
+ }
1517
+ if (w > 768 || h > 768) {
1518
+ const aspectRatio = w / h;
1519
+ if (w > h) {
1520
+ w = 768;
1521
+ h = Math.floor(w / aspectRatio);
1522
+ } else {
1523
+ h = 768;
1524
+ w = Math.floor(h * aspectRatio);
1525
+ }
1526
+ }
1527
+ const tilesWidth = Math.floor((w + 511) / 512);
1528
+ const tilesHeight = Math.floor((h + 511) / 512);
1529
+ const numTiles = tilesWidth * tilesHeight;
1530
+ return 85 + 170 * numTiles;
1531
+ }
1532
+ function isInsecureHttpUrl(url) {
1533
+ return url.startsWith("http://");
1534
+ }
1535
+ var ProviderManager = class {
1536
+ constructor(providerRegistry) {
1537
+ this.providerRegistry = providerRegistry;
1538
+ }
1539
+ failedProviders = /* @__PURE__ */ new Set();
1540
+ /**
1541
+ * Reset the failed providers list
1542
+ */
1543
+ resetFailedProviders() {
1544
+ this.failedProviders.clear();
1545
+ }
1546
+ /**
1547
+ * Mark a provider as failed
1548
+ */
1549
+ markFailed(baseUrl) {
1550
+ this.failedProviders.add(baseUrl);
1551
+ }
1552
+ /**
1553
+ * Check if a provider has failed
1554
+ */
1555
+ hasFailed(baseUrl) {
1556
+ return this.failedProviders.has(baseUrl);
1557
+ }
1558
+ /**
1559
+ * Find the next best provider for a model
1560
+ * @param modelId The model ID to find a provider for
1561
+ * @param currentBaseUrl The current provider to exclude
1562
+ * @returns The best provider URL or null if none available
1563
+ */
1564
+ findNextBestProvider(modelId, currentBaseUrl) {
1565
+ try {
1566
+ const torMode = isTorContext();
1567
+ const disabledProviders = new Set(
1568
+ this.providerRegistry.getDisabledProviders()
1569
+ );
1570
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1571
+ const candidates = [];
1572
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1573
+ if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl)) {
1574
+ continue;
1575
+ }
1576
+ if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
1577
+ continue;
1578
+ }
1579
+ const model = models.find((m) => m.id === modelId);
1580
+ if (!model) continue;
1581
+ const cost = model.sats_pricing?.completion ?? 0;
1582
+ candidates.push({ baseUrl, model, cost });
1583
+ }
1584
+ candidates.sort((a, b) => a.cost - b.cost);
1585
+ return candidates.length > 0 ? candidates[0].baseUrl : null;
1586
+ } catch (error) {
1587
+ console.error("Error finding next best provider:", error);
1588
+ return null;
1589
+ }
1590
+ }
1591
+ /**
1592
+ * Find the best model for a provider
1593
+ * Useful when switching providers and need to find equivalent model
1594
+ */
1595
+ async getModelForProvider(baseUrl, modelId) {
1596
+ const models = this.providerRegistry.getModelsForProvider(baseUrl);
1597
+ const exactMatch = models.find((m) => m.id === modelId);
1598
+ if (exactMatch) return exactMatch;
1599
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
1600
+ if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
1601
+ const suffix = modelId.split("/").pop();
1602
+ const suffixMatch = models.find((m) => m.id === suffix);
1603
+ if (suffixMatch) return suffixMatch;
1604
+ }
1605
+ return null;
1606
+ }
1607
+ /**
1608
+ * Get all available providers for a model
1609
+ * Returns sorted list by price
1610
+ */
1611
+ getAllProvidersForModel(modelId) {
1612
+ const candidates = [];
1613
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1614
+ const disabledProviders = new Set(
1615
+ this.providerRegistry.getDisabledProviders()
1616
+ );
1617
+ const torMode = isTorContext();
1618
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1619
+ if (disabledProviders.has(baseUrl)) continue;
1620
+ if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
1621
+ continue;
1622
+ const model = models.find((m) => m.id === modelId);
1623
+ if (!model) continue;
1624
+ const cost = model.sats_pricing?.completion ?? 0;
1625
+ candidates.push({ baseUrl, model, cost });
1626
+ }
1627
+ return candidates.sort((a, b) => a.cost - b.cost);
1628
+ }
1629
+ /**
1630
+ * Get providers for a model sorted by prompt+completion pricing
1631
+ */
1632
+ getProviderPriceRankingForModel(modelId, options = {}) {
1633
+ const normalizedId = this.normalizeModelId(modelId);
1634
+ const includeDisabled = options.includeDisabled ?? false;
1635
+ const torMode = options.torMode ?? false;
1636
+ const disabledProviders = new Set(
1637
+ this.providerRegistry.getDisabledProviders()
1638
+ );
1639
+ const allModels = this.providerRegistry.getAllProvidersModels();
1640
+ const results = [];
1641
+ for (const [baseUrl, models] of Object.entries(allModels)) {
1642
+ if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
1643
+ if (torMode && !baseUrl.includes(".onion")) continue;
1644
+ if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1645
+ continue;
1646
+ const match = models.find(
1647
+ (model) => this.normalizeModelId(model.id) === normalizedId
1648
+ );
1649
+ if (!match?.sats_pricing) continue;
1650
+ const prompt = match.sats_pricing.prompt;
1651
+ const completion = match.sats_pricing.completion;
1652
+ if (typeof prompt !== "number" || typeof completion !== "number") {
1653
+ continue;
1654
+ }
1655
+ const promptPerMillion = prompt * 1e6;
1656
+ const completionPerMillion = completion * 1e6;
1657
+ const totalPerMillion = promptPerMillion + completionPerMillion;
1658
+ results.push({
1659
+ baseUrl,
1660
+ model: match,
1661
+ promptPerMillion,
1662
+ completionPerMillion,
1663
+ totalPerMillion
1664
+ });
1665
+ }
1666
+ return results.sort((a, b) => {
1667
+ if (a.totalPerMillion !== b.totalPerMillion) {
1668
+ return a.totalPerMillion - b.totalPerMillion;
1669
+ }
1670
+ return a.baseUrl.localeCompare(b.baseUrl);
1671
+ });
1672
+ }
1673
+ /**
1674
+ * Get best-priced provider for a specific model
1675
+ */
1676
+ getBestProviderForModel(modelId, options = {}) {
1677
+ const ranking = this.getProviderPriceRankingForModel(modelId, options);
1678
+ return ranking[0]?.baseUrl ?? null;
1679
+ }
1680
+ normalizeModelId(modelId) {
1681
+ return modelId.includes("/") ? modelId.split("/").pop() || modelId : modelId;
1682
+ }
1683
+ /**
1684
+ * Check if a provider accepts a specific mint
1685
+ */
1686
+ providerAcceptsMint(baseUrl, mintUrl) {
1687
+ const providerMints = this.providerRegistry.getProviderMints(baseUrl);
1688
+ if (providerMints.length === 0) {
1689
+ return true;
1690
+ }
1691
+ return providerMints.includes(mintUrl);
1692
+ }
1693
+ /**
1694
+ * Get required sats for a model based on message history
1695
+ * Simple estimation based on typical usage
1696
+ */
1697
+ getRequiredSatsForModel(model, apiMessages, maxTokens) {
1698
+ try {
1699
+ let imageTokens = 0;
1700
+ if (apiMessages) {
1701
+ for (const msg of apiMessages) {
1702
+ const content = msg?.content;
1703
+ if (Array.isArray(content)) {
1704
+ for (const part of content) {
1705
+ const isImage = part && typeof part === "object" && part.type === "image_url";
1706
+ const url = isImage ? typeof part.image_url === "string" ? part.image_url : part.image_url?.url : void 0;
1707
+ if (url && typeof url === "string" && url.startsWith("data:")) {
1708
+ const res = getImageResolutionFromDataUrl(url);
1709
+ if (res) {
1710
+ const tokensFromImage = calculateImageTokens(
1711
+ res.width,
1712
+ res.height
1713
+ );
1714
+ imageTokens += tokensFromImage;
1715
+ console.log("IMAGE INPUT RESOLUTION", {
1716
+ width: res.width,
1717
+ height: res.height,
1718
+ tokensFromImage
1719
+ });
1720
+ } else {
1721
+ console.log(
1722
+ "IMAGE INPUT RESOLUTION",
1723
+ "unknown (unsupported format or parse failure)"
1724
+ );
1725
+ }
1726
+ }
1727
+ }
1728
+ }
1729
+ }
1730
+ }
1731
+ const apiMessagesNoImages = apiMessages ? apiMessages.map((m) => {
1732
+ if (Array.isArray(m?.content)) {
1733
+ const filtered = m.content.filter(
1734
+ (p) => !(p && typeof p === "object" && p.type === "image_url")
1735
+ );
1736
+ return { ...m, content: filtered };
1737
+ }
1738
+ return m;
1739
+ }) : void 0;
1740
+ const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
1741
+ const totalInputTokens = approximateTokens + imageTokens;
1742
+ const sp = model?.sats_pricing;
1743
+ if (!sp) return 0;
1744
+ if (!sp.max_completion_cost) {
1745
+ return sp.max_cost ?? 50;
1746
+ }
1747
+ const promptCosts = (sp.prompt || 0) * totalInputTokens;
1748
+ let completionCost = sp.max_completion_cost;
1749
+ if (maxTokens !== void 0 && sp.completion) {
1750
+ completionCost = sp.completion * maxTokens;
1751
+ }
1752
+ const totalEstimatedCosts = (promptCosts + completionCost) * 1.05;
1753
+ return totalEstimatedCosts;
1754
+ } catch (e) {
1755
+ console.error(e);
1756
+ return 0;
1757
+ }
1758
+ }
1759
+ };
1760
+
1761
+ // client/RoutstrClient.ts
1762
+ var TOPUP_MARGIN = 0.7;
1763
+ var RoutstrClient = class {
1764
+ constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
1765
+ this.walletAdapter = walletAdapter;
1766
+ this.storageAdapter = storageAdapter;
1767
+ this.providerRegistry = providerRegistry;
1768
+ this.balanceManager = new BalanceManager(
1769
+ walletAdapter,
1770
+ storageAdapter,
1771
+ providerRegistry
1772
+ );
1773
+ this.cashuSpender = new CashuSpender(
1774
+ walletAdapter,
1775
+ storageAdapter,
1776
+ providerRegistry,
1777
+ this.balanceManager
1778
+ );
1779
+ this.streamProcessor = new StreamProcessor();
1780
+ this.providerManager = new ProviderManager(providerRegistry);
1781
+ this.alertLevel = alertLevel;
1782
+ this.mode = mode;
1783
+ }
1784
+ cashuSpender;
1785
+ balanceManager;
1786
+ streamProcessor;
1787
+ providerManager;
1788
+ alertLevel;
1789
+ mode;
1790
+ debugLevel = "WARN";
1791
+ /**
1792
+ * Get the current client mode
1793
+ */
1794
+ getMode() {
1795
+ return this.mode;
1796
+ }
1797
+ getDebugLevel() {
1798
+ return this.debugLevel;
1799
+ }
1800
+ setDebugLevel(level) {
1801
+ this.debugLevel = level;
1802
+ }
1803
+ _log(level, ...args) {
1804
+ const levelPriority = {
1805
+ DEBUG: 0,
1806
+ WARN: 1,
1807
+ ERROR: 2
1808
+ };
1809
+ if (levelPriority[level] >= levelPriority[this.debugLevel]) {
1810
+ switch (level) {
1811
+ case "DEBUG":
1812
+ console.log(...args);
1813
+ break;
1814
+ case "WARN":
1815
+ console.warn(...args);
1816
+ break;
1817
+ case "ERROR":
1818
+ console.error(...args);
1819
+ break;
1820
+ }
1821
+ }
1822
+ }
1823
+ /**
1824
+ * Get the CashuSpender instance
1825
+ */
1826
+ getCashuSpender() {
1827
+ return this.cashuSpender;
1828
+ }
1829
+ /**
1830
+ * Get the BalanceManager instance
1831
+ */
1832
+ getBalanceManager() {
1833
+ return this.balanceManager;
1834
+ }
1835
+ /**
1836
+ * Get the ProviderManager instance
1837
+ */
1838
+ getProviderManager() {
1839
+ return this.providerManager;
1840
+ }
1841
+ /**
1842
+ * Check if the client is currently busy (in critical section)
1843
+ */
1844
+ get isBusy() {
1845
+ return this.cashuSpender.isBusy;
1846
+ }
1847
+ /**
1848
+ * Route an API request to the upstream provider
1849
+ *
1850
+ * This is a simpler alternative to fetchAIResponse that just proxies
1851
+ * the request upstream without the streaming callback machinery.
1852
+ * Useful for daemon-style routing where you just need to forward
1853
+ * requests and get responses back.
1854
+ */
1855
+ async routeRequest(params) {
1856
+ const {
1857
+ path,
1858
+ method,
1859
+ body,
1860
+ headers = {},
1861
+ baseUrl,
1862
+ mintUrl,
1863
+ modelId
1864
+ } = params;
1865
+ await this._checkBalance();
1866
+ let requiredSats = 1;
1867
+ let selectedModel;
1868
+ if (modelId) {
1869
+ const providerModel = await this.providerManager.getModelForProvider(
1870
+ baseUrl,
1871
+ modelId
1872
+ );
1873
+ selectedModel = providerModel ?? void 0;
1874
+ if (selectedModel) {
1875
+ requiredSats = this.providerManager.getRequiredSatsForModel(
1876
+ selectedModel,
1877
+ []
1878
+ );
1879
+ }
1880
+ }
1881
+ const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
1882
+ mintUrl,
1883
+ amount: requiredSats,
1884
+ baseUrl
1885
+ });
1886
+ this._log("DEBUG", token, baseUrl);
1887
+ let requestBody = body;
1888
+ if (body && typeof body === "object") {
1889
+ const bodyObj = body;
1890
+ if (!bodyObj.stream) {
1891
+ requestBody = { ...bodyObj, stream: false };
1892
+ }
1893
+ }
1894
+ const baseHeaders = this._buildBaseHeaders(headers);
1895
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
1896
+ const response = await this._makeRequest({
1897
+ path,
1898
+ method,
1899
+ body: method === "GET" ? void 0 : requestBody,
1900
+ baseUrl,
1901
+ mintUrl,
1902
+ token,
1903
+ requiredSats,
1904
+ headers: requestHeaders,
1905
+ baseHeaders,
1906
+ selectedModel
1907
+ });
1908
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
1909
+ const baseUrlUsed = response.baseUrl || baseUrl;
1910
+ const tokenUsed = response.token || token;
1911
+ await this._handlePostResponseBalanceUpdate({
1912
+ token: tokenUsed,
1913
+ baseUrl: baseUrlUsed,
1914
+ initialTokenBalance: tokenBalanceInSats,
1915
+ response
1916
+ });
1917
+ return response;
1918
+ }
1919
+ /**
1920
+ * Fetch AI response with streaming
1921
+ */
1922
+ async fetchAIResponse(options, callbacks) {
1923
+ const {
1924
+ messageHistory,
1925
+ selectedModel,
1926
+ baseUrl,
1927
+ mintUrl,
1928
+ balance,
1929
+ transactionHistory,
1930
+ maxTokens,
1931
+ headers
1932
+ } = options;
1933
+ const apiMessages = await this._convertMessages(messageHistory);
1934
+ const requiredSats = this.providerManager.getRequiredSatsForModel(
1935
+ selectedModel,
1936
+ apiMessages,
1937
+ maxTokens
1938
+ );
1939
+ try {
1940
+ await this._checkBalance();
1941
+ callbacks.onPaymentProcessing?.(true);
1942
+ const spendResult = await this._spendToken({
1943
+ mintUrl,
1944
+ amount: requiredSats,
1945
+ baseUrl
1946
+ });
1947
+ let token = spendResult.token;
1948
+ let tokenBalance = spendResult.tokenBalance;
1949
+ let tokenBalanceUnit = spendResult.tokenBalanceUnit;
1950
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
1951
+ callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
1952
+ const baseHeaders = this._buildBaseHeaders(headers);
1953
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
1954
+ this.providerManager.resetFailedProviders();
1955
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
1956
+ const providerVersion = providerInfo?.version ?? "";
1957
+ let modelIdForRequest = selectedModel.id;
1958
+ if (/^0\.1\./.test(providerVersion)) {
1959
+ const newModel = await this.providerManager.getModelForProvider(
1960
+ baseUrl,
1961
+ selectedModel.id
1962
+ );
1963
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
1964
+ }
1965
+ const body = {
1966
+ model: modelIdForRequest,
1967
+ messages: apiMessages,
1968
+ stream: true
1969
+ };
1970
+ if (maxTokens !== void 0) {
1971
+ body.max_tokens = maxTokens;
1972
+ }
1973
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
1974
+ body.tools = [{ type: "web_search" }];
1975
+ }
1976
+ const response = await this._makeRequest({
1977
+ path: "/v1/chat/completions",
1978
+ method: "POST",
1979
+ body,
1980
+ selectedModel,
1981
+ baseUrl,
1982
+ mintUrl,
1983
+ token,
1984
+ requiredSats,
1985
+ maxTokens,
1986
+ headers: requestHeaders,
1987
+ baseHeaders
1988
+ });
1989
+ if (!response.body) {
1990
+ throw new Error("Response body is not available");
1991
+ }
1992
+ if (response.status === 200) {
1993
+ const baseUrlUsed = response.baseUrl || baseUrl;
1994
+ const streamingResult = await this.streamProcessor.process(
1995
+ response,
1996
+ {
1997
+ onContent: callbacks.onStreamingUpdate,
1998
+ onThinking: callbacks.onThinkingUpdate
1999
+ },
2000
+ selectedModel.id
2001
+ );
2002
+ if (streamingResult.finish_reason === "content_filter") {
2003
+ callbacks.onMessageAppend({
2004
+ role: "assistant",
2005
+ content: "Your request was denied due to content filtering."
2006
+ });
2007
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
2008
+ const message = await this._createAssistantMessage(streamingResult);
2009
+ callbacks.onMessageAppend(message);
2010
+ } else {
2011
+ callbacks.onMessageAppend({
2012
+ role: "system",
2013
+ content: "The provider did not respond to this request."
2014
+ });
2015
+ }
2016
+ callbacks.onStreamingUpdate("");
2017
+ callbacks.onThinkingUpdate("");
2018
+ const isApikeysEstimate = this.mode === "apikeys";
2019
+ let satsSpent = await this._handlePostResponseBalanceUpdate({
2020
+ token,
2021
+ baseUrl: baseUrlUsed,
2022
+ initialTokenBalance: tokenBalanceInSats,
2023
+ fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
2024
+ response
2025
+ });
2026
+ const estimatedCosts = this._getEstimatedCosts(
2027
+ selectedModel,
2028
+ streamingResult
2029
+ );
2030
+ const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
2031
+ onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
2032
+ } else {
2033
+ throw new Error(`${response.status} ${response.statusText}`);
2034
+ }
2035
+ } catch (error) {
2036
+ this._handleError(error, callbacks);
2037
+ } finally {
2038
+ callbacks.onPaymentProcessing?.(false);
2039
+ }
2040
+ }
2041
+ /**
2042
+ * Make the API request with failover support
2043
+ */
2044
+ async _makeRequest(params) {
2045
+ const { path, method, body, baseUrl, token, headers } = params;
2046
+ try {
2047
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
2048
+ if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
2049
+ const response = await fetch(url, {
2050
+ method,
2051
+ headers,
2052
+ body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
2053
+ });
2054
+ if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
2055
+ response.baseUrl = baseUrl;
2056
+ response.token = token;
2057
+ if (!response.ok) {
2058
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
2059
+ return await this._handleErrorResponse(
2060
+ params,
2061
+ token,
2062
+ response.status,
2063
+ requestId,
2064
+ this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0
2065
+ );
2066
+ }
2067
+ return response;
2068
+ } catch (error) {
2069
+ if (this._isNetworkError(error?.message || "")) {
2070
+ return await this._handleErrorResponse(
2071
+ params,
2072
+ token,
2073
+ -1
2074
+ // just for Network Error to skip all statuses
2075
+ );
2076
+ }
2077
+ throw error;
2078
+ }
2079
+ }
2080
+ /**
2081
+ * Handle error responses with failover
2082
+ */
2083
+ async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken) {
2084
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
2085
+ let tryNextProvider = false;
2086
+ this._log(
2087
+ "DEBUG",
2088
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
2089
+ );
2090
+ this._log(
2091
+ "DEBUG",
2092
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
2093
+ );
2094
+ if (params.token.startsWith("cashu")) {
2095
+ const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
2096
+ params.token
2097
+ );
2098
+ if (tryReceiveTokenResult.success) {
2099
+ this._log(
2100
+ "DEBUG",
2101
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
2102
+ );
2103
+ tryNextProvider = true;
2104
+ if (this.mode === "lazyrefund")
2105
+ this.storageAdapter.removeToken(baseUrl);
2106
+ } else {
2107
+ this._log(
2108
+ "DEBUG",
2109
+ `[RoutstrClient] _handleErrorResponse: Failed to receive token. `
2110
+ );
2111
+ }
2112
+ }
2113
+ if (this.mode === "xcashu") {
2114
+ if (xCashuRefundToken) {
2115
+ this._log(
2116
+ "DEBUG",
2117
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
2118
+ );
2119
+ try {
2120
+ const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
2121
+ if (receiveResult.success) {
2122
+ this._log(
2123
+ "DEBUG",
2124
+ `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
2125
+ );
2126
+ tryNextProvider = true;
2127
+ } else
2128
+ throw new ProviderError(
2129
+ baseUrl,
2130
+ status,
2131
+ "xcashu refund failed",
2132
+ requestId
2133
+ );
2134
+ } catch (error) {
2135
+ this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
2136
+ throw new ProviderError(
2137
+ baseUrl,
2138
+ status,
2139
+ "[xcashu] Failed to receive refund token",
2140
+ requestId
2141
+ );
2142
+ }
2143
+ } else {
2144
+ if (!tryNextProvider)
2145
+ throw new ProviderError(
2146
+ baseUrl,
2147
+ status,
2148
+ "[xcashu] Failed to receive refund token",
2149
+ requestId
2150
+ );
2151
+ }
2152
+ }
2153
+ if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
2154
+ const topupResult = await this.balanceManager.topUp({
2155
+ mintUrl,
2156
+ baseUrl,
2157
+ amount: params.requiredSats * TOPUP_MARGIN,
2158
+ token: params.token
2159
+ });
2160
+ this._log(
2161
+ "DEBUG",
2162
+ `[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
2163
+ );
2164
+ if (!topupResult.success) {
2165
+ const message = topupResult.message || "";
2166
+ if (message.includes("Insufficient balance")) {
2167
+ const needMatch = message.match(/need (\d+)/);
2168
+ const haveMatch = message.match(/have (\d+)/);
2169
+ const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
2170
+ const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
2171
+ this._log(
2172
+ "DEBUG",
2173
+ `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
2174
+ );
2175
+ throw new InsufficientBalanceError(required, available);
2176
+ } else {
2177
+ this._log(
2178
+ "DEBUG",
2179
+ `[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
2180
+ );
2181
+ tryNextProvider = true;
2182
+ }
2183
+ } else {
2184
+ this._log(
2185
+ "DEBUG",
2186
+ `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
2187
+ );
2188
+ }
2189
+ if (!tryNextProvider)
2190
+ return this._makeRequest({
2191
+ ...params,
2192
+ token: params.token,
2193
+ headers: this._withAuthHeader(params.baseHeaders, params.token)
2194
+ });
2195
+ }
2196
+ if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
2197
+ this._log(
2198
+ "DEBUG",
2199
+ `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
2200
+ );
2201
+ if (this.mode === "lazyrefund") {
2202
+ try {
2203
+ const refundResult = await this.balanceManager.refund({
2204
+ mintUrl,
2205
+ baseUrl,
2206
+ token: params.token
2207
+ });
2208
+ this._log(
2209
+ "DEBUG",
2210
+ `[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
2211
+ );
2212
+ if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
2213
+ else
2214
+ throw new ProviderError(
2215
+ baseUrl,
2216
+ status,
2217
+ "refund failed",
2218
+ requestId
2219
+ );
2220
+ } catch (error) {
2221
+ throw new ProviderError(
2222
+ baseUrl,
2223
+ status,
2224
+ "Failed to refund token",
2225
+ requestId
2226
+ );
2227
+ }
2228
+ } else if (this.mode === "apikeys") {
2229
+ this._log(
2230
+ "DEBUG",
2231
+ `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
2232
+ );
2233
+ const initialBalance = await this.balanceManager.getTokenBalance(
2234
+ token,
2235
+ baseUrl
2236
+ );
2237
+ this._log(
2238
+ "DEBUG",
2239
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
2240
+ );
2241
+ const refundResult = await this.balanceManager.refundApiKey({
2242
+ mintUrl,
2243
+ baseUrl,
2244
+ apiKey: token
2245
+ });
2246
+ this._log(
2247
+ "DEBUG",
2248
+ `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
2249
+ );
2250
+ if (!refundResult.success && initialBalance.amount > 0) {
2251
+ throw new ProviderError(
2252
+ baseUrl,
2253
+ status,
2254
+ refundResult.message ?? "Unknown error"
2255
+ );
2256
+ } else {
2257
+ this.storageAdapter.removeApiKey(baseUrl);
2258
+ }
2259
+ }
2260
+ }
2261
+ this.providerManager.markFailed(baseUrl);
2262
+ this._log(
2263
+ "DEBUG",
2264
+ `[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
2265
+ );
2266
+ if (!selectedModel) {
2267
+ throw new ProviderError(
2268
+ baseUrl,
2269
+ status,
2270
+ "Funny, no selected model. HMM. "
2271
+ );
2272
+ }
2273
+ const nextProvider = this.providerManager.findNextBestProvider(
2274
+ selectedModel.id,
2275
+ baseUrl
2276
+ );
2277
+ if (nextProvider) {
2278
+ this._log(
2279
+ "DEBUG",
2280
+ `[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
2281
+ );
2282
+ const newModel = await this.providerManager.getModelForProvider(
2283
+ nextProvider,
2284
+ selectedModel.id
2285
+ ) ?? selectedModel;
2286
+ const messagesForPricing = Array.isArray(
2287
+ body?.messages
2288
+ ) ? body.messages : [];
2289
+ const newRequiredSats = this.providerManager.getRequiredSatsForModel(
2290
+ newModel,
2291
+ messagesForPricing,
2292
+ params.maxTokens
2293
+ );
2294
+ this._log(
2295
+ "DEBUG",
2296
+ `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
2297
+ );
2298
+ const spendResult = await this._spendToken({
2299
+ mintUrl,
2300
+ amount: newRequiredSats,
2301
+ baseUrl: nextProvider
2302
+ });
2303
+ return this._makeRequest({
2304
+ ...params,
2305
+ path,
2306
+ method,
2307
+ body,
2308
+ baseUrl: nextProvider,
2309
+ selectedModel: newModel,
2310
+ token: spendResult.token,
2311
+ requiredSats: newRequiredSats,
2312
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
2313
+ });
2314
+ }
2315
+ throw new FailoverError(baseUrl, Array.from(this.providerManager));
2316
+ }
2317
+ /**
2318
+ * Handle post-response balance update for all modes
2319
+ */
2320
+ async _handlePostResponseBalanceUpdate(params) {
2321
+ const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
2322
+ let satsSpent = initialTokenBalance;
2323
+ if (this.mode === "xcashu" && response) {
2324
+ const refundToken = response.headers.get("x-cashu") ?? void 0;
2325
+ if (refundToken) {
2326
+ try {
2327
+ const receiveResult = await this.cashuSpender.receiveToken(refundToken);
2328
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
2329
+ } catch (error) {
2330
+ this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
2331
+ }
2332
+ }
2333
+ } else if (this.mode === "lazyrefund") {
2334
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
2335
+ token,
2336
+ baseUrl
2337
+ );
2338
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2339
+ this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
2340
+ satsSpent = initialTokenBalance - latestTokenBalance;
2341
+ } else if (this.mode === "apikeys") {
2342
+ try {
2343
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
2344
+ token,
2345
+ baseUrl
2346
+ );
2347
+ this._log(
2348
+ "DEBUG",
2349
+ "LATEST Balance",
2350
+ latestBalanceInfo.amount,
2351
+ latestBalanceInfo.reserved,
2352
+ latestBalanceInfo.apiKey,
2353
+ baseUrl
2354
+ );
2355
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2356
+ this.storageAdapter.updateChildKeyBalance(baseUrl, latestTokenBalance);
2357
+ this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
2358
+ satsSpent = initialTokenBalance - latestTokenBalance;
2359
+ } catch (e) {
2360
+ this._log("WARN", "Could not get updated API key balance:", e);
2361
+ satsSpent = fallbackSatsSpent ?? initialTokenBalance;
2362
+ }
2363
+ }
2364
+ return satsSpent;
2365
+ }
2366
+ /**
2367
+ * Convert messages for API format
2368
+ */
2369
+ async _convertMessages(messages) {
2370
+ return Promise.all(
2371
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
2372
+ role: m.role,
2373
+ content: typeof m.content === "string" ? m.content : m.content
2374
+ }))
2375
+ );
2376
+ }
2377
+ /**
2378
+ * Create assistant message from streaming result
2379
+ */
2380
+ async _createAssistantMessage(result) {
2381
+ if (result.images && result.images.length > 0) {
2382
+ const content = [];
2383
+ if (result.content) {
2384
+ content.push({
2385
+ type: "text",
2386
+ text: result.content,
2387
+ thinking: result.thinking,
2388
+ citations: result.citations,
2389
+ annotations: result.annotations
2390
+ });
2391
+ }
2392
+ for (const img of result.images) {
2393
+ content.push({
2394
+ type: "image_url",
2395
+ image_url: {
2396
+ url: img.image_url.url
2397
+ }
2398
+ });
2399
+ }
2400
+ return {
2401
+ role: "assistant",
2402
+ content
2403
+ };
2404
+ }
2405
+ return {
2406
+ role: "assistant",
2407
+ content: result.content || ""
2408
+ };
2409
+ }
2410
+ /**
2411
+ * Create a child key for a parent API key via the provider's API
2412
+ * POST /v1/balance/child-key
2413
+ */
2414
+ async _createChildKey(baseUrl, parentApiKey, options) {
2415
+ const response = await fetch(`${baseUrl}v1/balance/child-key`, {
2416
+ method: "POST",
2417
+ headers: {
2418
+ "Content-Type": "application/json",
2419
+ Authorization: `Bearer ${parentApiKey}`
2420
+ },
2421
+ body: JSON.stringify({
2422
+ count: options?.count ?? 1,
2423
+ balance_limit: options?.balanceLimit,
2424
+ balance_limit_reset: options?.balanceLimitReset,
2425
+ validity_date: options?.validityDate
2426
+ })
2427
+ });
2428
+ if (!response.ok) {
2429
+ throw new Error(
2430
+ `Failed to create child key: ${response.status} ${await response.text()}`
2431
+ );
2432
+ }
2433
+ const data = await response.json();
2434
+ return {
2435
+ childKey: data.api_keys?.[0],
2436
+ balance: data.balance ?? 0,
2437
+ balanceLimit: data.balance_limit,
2438
+ validityDate: data.validity_date
2439
+ };
2440
+ }
2441
+ /**
2442
+ * Calculate estimated costs from usage
2443
+ */
2444
+ _getEstimatedCosts(selectedModel, streamingResult) {
2445
+ let estimatedCosts = 0;
2446
+ if (streamingResult.usage) {
2447
+ const { completion_tokens, prompt_tokens } = streamingResult.usage;
2448
+ if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
2449
+ estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
2450
+ }
2451
+ }
2452
+ return estimatedCosts;
2453
+ }
2454
+ /**
2455
+ * Get pending cashu token amount
2456
+ */
2457
+ _getPendingCashuTokenAmount() {
2458
+ const distribution = this.storageAdapter.getCachedTokenDistribution();
2459
+ return distribution.reduce((total, item) => total + item.amount, 0);
2460
+ }
2461
+ /**
2462
+ * Check if error message indicates a network error
2463
+ */
2464
+ _isNetworkError(message) {
2465
+ return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
2466
+ }
2467
+ /**
2468
+ * Handle errors and notify callbacks
2469
+ */
2470
+ _handleError(error, callbacks) {
2471
+ this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
2472
+ if (error instanceof Error) {
2473
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
2474
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
2475
+ this._log(
2476
+ "ERROR",
2477
+ `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
2478
+ );
2479
+ callbacks.onMessageAppend({
2480
+ role: "system",
2481
+ content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
2482
+ });
2483
+ } else {
2484
+ callbacks.onMessageAppend({
2485
+ role: "system",
2486
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
2487
+ });
2488
+ }
2489
+ }
2490
+ /**
2491
+ * Check wallet balance and throw if insufficient
2492
+ */
2493
+ async _checkBalance() {
2494
+ const balances = await this.walletAdapter.getBalances();
2495
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
2496
+ if (totalBalance <= 0) {
2497
+ throw new InsufficientBalanceError(1, 0);
2498
+ }
2499
+ }
2500
+ /**
2501
+ * Spend a token using CashuSpender with standardized error handling
2502
+ */
2503
+ async _spendToken(params) {
2504
+ const { mintUrl, amount, baseUrl } = params;
2505
+ this._log(
2506
+ "DEBUG",
2507
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
2508
+ );
2509
+ if (this.mode === "apikeys") {
2510
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
2511
+ if (!parentApiKey) {
2512
+ this._log(
2513
+ "DEBUG",
2514
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
2515
+ );
2516
+ const spendResult2 = await this.cashuSpender.spend({
2517
+ mintUrl,
2518
+ amount: amount * TOPUP_MARGIN,
2519
+ baseUrl: "",
2520
+ reuseToken: false
2521
+ });
2522
+ if (!spendResult2.token) {
2523
+ this._log(
2524
+ "ERROR",
2525
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
2526
+ spendResult2.error
2527
+ );
2528
+ throw new Error(
2529
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
2530
+ );
2531
+ } else {
2532
+ this._log(
2533
+ "DEBUG",
2534
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
2535
+ );
2536
+ }
2537
+ this._log(
2538
+ "DEBUG",
2539
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
2540
+ );
2541
+ try {
2542
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
2543
+ } catch (error) {
2544
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
2545
+ const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
2546
+ spendResult2.token
2547
+ );
2548
+ if (tryReceiveTokenResult.success) {
2549
+ this._log(
2550
+ "DEBUG",
2551
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
2552
+ );
2553
+ } else {
2554
+ this._log(
2555
+ "DEBUG",
2556
+ `[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
2557
+ );
2558
+ }
2559
+ this._log(
2560
+ "DEBUG",
2561
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
2562
+ );
2563
+ } else {
2564
+ throw error;
2565
+ }
2566
+ }
2567
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
2568
+ } else {
2569
+ this._log(
2570
+ "DEBUG",
2571
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
2572
+ );
2573
+ }
2574
+ let tokenBalance = 0;
2575
+ let tokenBalanceUnit = "sat";
2576
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
2577
+ const distributionForBaseUrl = apiKeyDistribution.find(
2578
+ (d) => d.baseUrl === baseUrl
2579
+ );
2580
+ if (distributionForBaseUrl) {
2581
+ tokenBalance = distributionForBaseUrl.amount;
2582
+ }
2583
+ if (tokenBalance === 0 && parentApiKey) {
2584
+ try {
2585
+ const balanceInfo = await this.balanceManager.getTokenBalance(
2586
+ parentApiKey.key,
2587
+ baseUrl
2588
+ );
2589
+ tokenBalance = balanceInfo.amount;
2590
+ tokenBalanceUnit = balanceInfo.unit;
2591
+ } catch (e) {
2592
+ this._log("WARN", "Could not get initial API key balance:", e);
2593
+ }
2594
+ }
2595
+ this._log(
2596
+ "DEBUG",
2597
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
2598
+ );
2599
+ return {
2600
+ token: parentApiKey?.key ?? "",
2601
+ tokenBalance,
2602
+ tokenBalanceUnit
2603
+ };
2604
+ }
2605
+ this._log(
2606
+ "DEBUG",
2607
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
2608
+ );
2609
+ const spendResult = await this.cashuSpender.spend({
2610
+ mintUrl,
2611
+ amount,
2612
+ baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
2613
+ reuseToken: this.mode === "lazyrefund"
2614
+ });
2615
+ if (!spendResult.token) {
2616
+ this._log(
2617
+ "ERROR",
2618
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
2619
+ spendResult.error
2620
+ );
2621
+ } else {
2622
+ this._log(
2623
+ "DEBUG",
2624
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
2625
+ );
2626
+ }
2627
+ return {
2628
+ token: spendResult.token,
2629
+ tokenBalance: spendResult.balance,
2630
+ tokenBalanceUnit: spendResult.unit ?? "sat"
2631
+ };
2632
+ }
2633
+ /**
2634
+ * Build request headers with common defaults and dev mock controls
2635
+ */
2636
+ _buildBaseHeaders(additionalHeaders = {}, token) {
2637
+ const headers = {
2638
+ ...additionalHeaders,
2639
+ "Content-Type": "application/json"
2640
+ };
2641
+ return headers;
2642
+ }
2643
+ /**
2644
+ * Attach auth headers using the active client mode
2645
+ */
2646
+ _withAuthHeader(headers, token) {
2647
+ const nextHeaders = { ...headers };
2648
+ if (this.mode === "xcashu") {
2649
+ nextHeaders["X-Cashu"] = token;
2650
+ } else {
2651
+ nextHeaders["Authorization"] = `Bearer ${token}`;
2652
+ }
2653
+ return nextHeaders;
2654
+ }
2655
+ };
2656
+
2657
+ export { ProviderManager, RoutstrClient, StreamProcessor };
2658
+ //# sourceMappingURL=index.mjs.map
2659
+ //# sourceMappingURL=index.mjs.map