@routstr/sdk 0.1.0 → 0.1.1

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