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