@routstr/sdk 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/dist/client/index.d.mts +0 -411
- package/dist/client/index.d.ts +0 -411
- package/dist/client/index.js +0 -4819
- package/dist/client/index.js.map +0 -1
- package/dist/client/index.mjs +0 -4813
- package/dist/client/index.mjs.map +0 -1
- package/dist/discovery/index.d.mts +0 -196
- package/dist/discovery/index.d.ts +0 -196
- package/dist/discovery/index.js +0 -616
- package/dist/discovery/index.js.map +0 -1
- package/dist/discovery/index.mjs +0 -613
- package/dist/discovery/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -190
- package/dist/index.d.ts +0 -190
- package/dist/index.js +0 -6114
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -6065
- package/dist/index.mjs.map +0 -1
- package/dist/interfaces-Bp0Ngmqv.d.mts +0 -176
- package/dist/interfaces-CIfd_phZ.d.ts +0 -112
- package/dist/interfaces-Cxi8R4TT.d.mts +0 -112
- package/dist/interfaces-D2FDCLyP.d.ts +0 -176
- package/dist/storage/index.d.mts +0 -87
- package/dist/storage/index.d.ts +0 -87
- package/dist/storage/index.js +0 -1734
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/index.mjs +0 -1712
- package/dist/storage/index.mjs.map +0 -1
- package/dist/store-BD5zF9Hp.d.ts +0 -172
- package/dist/store-CBSyK2qg.d.mts +0 -172
- package/dist/types-DPQM6tIG.d.mts +0 -234
- package/dist/types-DPQM6tIG.d.ts +0 -234
- package/dist/wallet/index.d.mts +0 -245
- package/dist/wallet/index.d.ts +0 -245
- package/dist/wallet/index.js +0 -1329
- package/dist/wallet/index.js.map +0 -1
- package/dist/wallet/index.mjs +0 -1326
- package/dist/wallet/index.mjs.map +0 -1
package/dist/client/index.js
DELETED
|
@@ -1,4819 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var cashuTs = require('@cashu/cashu-ts');
|
|
4
|
-
var vanilla = require('zustand/vanilla');
|
|
5
|
-
var stream = require('stream');
|
|
6
|
-
var string_decoder = require('string_decoder');
|
|
7
|
-
|
|
8
|
-
// core/types.ts
|
|
9
|
-
function makeConsoleLogger(prefix) {
|
|
10
|
-
const fmt = (args) => prefix ? [prefix, ...args] : args;
|
|
11
|
-
return {
|
|
12
|
-
log: (...args) => console.log(...fmt(args)),
|
|
13
|
-
warn: (...args) => console.warn(...fmt(args)),
|
|
14
|
-
error: (...args) => console.error(...fmt(args)),
|
|
15
|
-
debug: (...args) => console.log(...fmt(args)),
|
|
16
|
-
child: (p) => makeConsoleLogger(prefix ? `${prefix}:${p}` : p)
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
var consoleLogger = makeConsoleLogger();
|
|
20
|
-
|
|
21
|
-
// core/errors.ts
|
|
22
|
-
var InsufficientBalanceError = class extends Error {
|
|
23
|
-
constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
|
|
24
|
-
super(
|
|
25
|
-
customMessage ?? `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
|
|
26
|
-
);
|
|
27
|
-
this.required = required;
|
|
28
|
-
this.available = available;
|
|
29
|
-
this.maxMintBalance = maxMintBalance;
|
|
30
|
-
this.maxMintUrl = maxMintUrl;
|
|
31
|
-
this.name = "InsufficientBalanceError";
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
var ProviderError = class extends Error {
|
|
35
|
-
constructor(baseUrl, statusCode, message, requestId) {
|
|
36
|
-
super(
|
|
37
|
-
`Provider ${baseUrl} returned ${statusCode}: ${message}` + (requestId ? ` (Request ID: ${requestId})` : "")
|
|
38
|
-
);
|
|
39
|
-
this.baseUrl = baseUrl;
|
|
40
|
-
this.statusCode = statusCode;
|
|
41
|
-
this.requestId = requestId;
|
|
42
|
-
this.name = "ProviderError";
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
var FailoverError = class extends Error {
|
|
46
|
-
constructor(originalProvider, failedProviders, message) {
|
|
47
|
-
super(
|
|
48
|
-
message || `All providers failed. Original: ${originalProvider}, Failed: ${failedProviders.join(", ")}`
|
|
49
|
-
);
|
|
50
|
-
this.originalProvider = originalProvider;
|
|
51
|
-
this.failedProviders = failedProviders;
|
|
52
|
-
this.name = "FailoverError";
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// wallet/AuditLogger.ts
|
|
57
|
-
var AuditLogger = class _AuditLogger {
|
|
58
|
-
static instance = null;
|
|
59
|
-
static getInstance() {
|
|
60
|
-
if (!_AuditLogger.instance) {
|
|
61
|
-
_AuditLogger.instance = new _AuditLogger();
|
|
62
|
-
}
|
|
63
|
-
return _AuditLogger.instance;
|
|
64
|
-
}
|
|
65
|
-
async log(entry) {
|
|
66
|
-
const fullEntry = {
|
|
67
|
-
...entry,
|
|
68
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
|
-
};
|
|
70
|
-
const logLine = JSON.stringify(fullEntry) + "\n";
|
|
71
|
-
if (typeof window === "undefined") {
|
|
72
|
-
try {
|
|
73
|
-
const fs = await import('fs');
|
|
74
|
-
const path = await import('path');
|
|
75
|
-
const logPath = path.join(process.cwd(), "audit.log");
|
|
76
|
-
fs.appendFileSync(logPath, logLine);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error("[AuditLogger] Failed to write to file:", error);
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
console.log("[AUDIT]", logLine.trim());
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async logBalanceSnapshot(action, amounts, options) {
|
|
85
|
-
await this.log({
|
|
86
|
-
action,
|
|
87
|
-
totalBalance: amounts.totalBalance,
|
|
88
|
-
providerBalances: amounts.providerBalances,
|
|
89
|
-
mintBalances: amounts.mintBalances,
|
|
90
|
-
amount: options?.amount,
|
|
91
|
-
mintUrl: options?.mintUrl,
|
|
92
|
-
baseUrl: options?.baseUrl,
|
|
93
|
-
status: options?.status ?? "success",
|
|
94
|
-
details: options?.details
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
var auditLogger = AuditLogger.getInstance();
|
|
99
|
-
|
|
100
|
-
// wallet/tokenUtils.ts
|
|
101
|
-
function isNetworkErrorMessage(message) {
|
|
102
|
-
return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed") || message.includes("ERR_TLS_CERT_ALTNAME_INVALID") || message.includes("ERR_TLS_CERT_NOT_YET_VALID") || message.includes("ERR_TLS_CERT_EXPIRED") || message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE") || message.includes("SELF_SIGNED_CERT_IN_CHAIN");
|
|
103
|
-
}
|
|
104
|
-
function getBalanceInSats(balance, unit) {
|
|
105
|
-
return unit === "msat" ? balance / 1e3 : balance;
|
|
106
|
-
}
|
|
107
|
-
function selectMintWithBalance(balances, units, amount, excludeMints = []) {
|
|
108
|
-
for (const mintUrl in balances) {
|
|
109
|
-
if (excludeMints.includes(mintUrl)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const balanceInSats = getBalanceInSats(balances[mintUrl], units[mintUrl]);
|
|
113
|
-
if (balanceInSats >= amount) {
|
|
114
|
-
return { selectedMintUrl: mintUrl, selectedMintBalance: balanceInSats };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { selectedMintUrl: null, selectedMintBalance: 0 };
|
|
118
|
-
}
|
|
119
|
-
var CashuSpender = class {
|
|
120
|
-
constructor(walletAdapter, storageAdapter, _providerRegistry, balanceManager, logger) {
|
|
121
|
-
this.walletAdapter = walletAdapter;
|
|
122
|
-
this.storageAdapter = storageAdapter;
|
|
123
|
-
this._providerRegistry = _providerRegistry;
|
|
124
|
-
this.balanceManager = balanceManager;
|
|
125
|
-
this.logger = (logger ?? consoleLogger).child("CashuSpender");
|
|
126
|
-
}
|
|
127
|
-
_isBusy = false;
|
|
128
|
-
debugLevel = "WARN";
|
|
129
|
-
logger;
|
|
130
|
-
async receiveToken(token) {
|
|
131
|
-
try {
|
|
132
|
-
const result = await this.walletAdapter.receiveToken(token);
|
|
133
|
-
return result;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
136
|
-
if (errorMessage.includes("Failed to fetch mint")) {
|
|
137
|
-
const cachedTokens = this.storageAdapter.getCachedReceiveTokens();
|
|
138
|
-
const existingIndex = cachedTokens.findIndex((t) => t.token === token);
|
|
139
|
-
if (existingIndex === -1) {
|
|
140
|
-
const { amount: amount2, unit: unit2 } = this._decodeTokenAmount(token);
|
|
141
|
-
this.storageAdapter.setCachedReceiveTokens([
|
|
142
|
-
...cachedTokens,
|
|
143
|
-
{
|
|
144
|
-
token,
|
|
145
|
-
amount: amount2,
|
|
146
|
-
unit: unit2,
|
|
147
|
-
createdAt: Date.now()
|
|
148
|
-
}
|
|
149
|
-
]);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const { amount, unit } = this._decodeTokenAmount(token);
|
|
153
|
-
return { success: false, amount, unit, message: errorMessage };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
_decodeTokenAmount(token) {
|
|
157
|
-
try {
|
|
158
|
-
const decoded = cashuTs.getDecodedToken(token);
|
|
159
|
-
const amount = decoded.proofs.reduce(
|
|
160
|
-
(acc, proof) => acc + proof.amount,
|
|
161
|
-
0
|
|
162
|
-
);
|
|
163
|
-
const unit = decoded.unit || "sat";
|
|
164
|
-
return { amount, unit };
|
|
165
|
-
} catch {
|
|
166
|
-
return { amount: 0, unit: "sat" };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
async _getBalanceState() {
|
|
170
|
-
if (this.balanceManager) {
|
|
171
|
-
return this.balanceManager.getBalanceState();
|
|
172
|
-
}
|
|
173
|
-
const mintBalances = await this.walletAdapter.getBalances();
|
|
174
|
-
const units = this.walletAdapter.getMintUnits();
|
|
175
|
-
let totalMintBalance = 0;
|
|
176
|
-
const normalizedMintBalances = {};
|
|
177
|
-
for (const url in mintBalances) {
|
|
178
|
-
const balance = mintBalances[url];
|
|
179
|
-
const unit = units[url];
|
|
180
|
-
const balanceInSats = getBalanceInSats(balance, unit);
|
|
181
|
-
normalizedMintBalances[url] = balanceInSats;
|
|
182
|
-
totalMintBalance += balanceInSats;
|
|
183
|
-
}
|
|
184
|
-
const providerBalances = {};
|
|
185
|
-
let totalProviderBalance = 0;
|
|
186
|
-
const apiKeys = this.storageAdapter.getAllApiKeys();
|
|
187
|
-
for (const apiKey of apiKeys) {
|
|
188
|
-
if (!providerBalances[apiKey.baseUrl]) {
|
|
189
|
-
providerBalances[apiKey.baseUrl] = 0;
|
|
190
|
-
}
|
|
191
|
-
providerBalances[apiKey.baseUrl] += apiKey.balance;
|
|
192
|
-
totalProviderBalance += apiKey.balance;
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
totalBalance: totalMintBalance + totalProviderBalance,
|
|
196
|
-
providerBalances,
|
|
197
|
-
mintBalances: normalizedMintBalances
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
async _logTransaction(action, options) {
|
|
201
|
-
const balanceState = await this._getBalanceState();
|
|
202
|
-
await auditLogger.logBalanceSnapshot(action, balanceState, options);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Check if the spender is currently in a critical operation
|
|
206
|
-
*/
|
|
207
|
-
get isBusy() {
|
|
208
|
-
return this._isBusy;
|
|
209
|
-
}
|
|
210
|
-
getDebugLevel() {
|
|
211
|
-
return this.debugLevel;
|
|
212
|
-
}
|
|
213
|
-
setDebugLevel(level) {
|
|
214
|
-
this.debugLevel = level;
|
|
215
|
-
}
|
|
216
|
-
_log(level, ...args) {
|
|
217
|
-
const levelPriority = {
|
|
218
|
-
DEBUG: 0,
|
|
219
|
-
WARN: 1,
|
|
220
|
-
ERROR: 2
|
|
221
|
-
};
|
|
222
|
-
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
223
|
-
switch (level) {
|
|
224
|
-
case "DEBUG":
|
|
225
|
-
this.logger.log(...args);
|
|
226
|
-
break;
|
|
227
|
-
case "WARN":
|
|
228
|
-
this.logger.warn(...args);
|
|
229
|
-
break;
|
|
230
|
-
case "ERROR":
|
|
231
|
-
this.logger.error(...args);
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Spend Cashu tokens with automatic mint selection and retry logic
|
|
238
|
-
* Throws errors on failure instead of returning failed SpendResult
|
|
239
|
-
*/
|
|
240
|
-
async spend(options) {
|
|
241
|
-
const {
|
|
242
|
-
mintUrl,
|
|
243
|
-
amount,
|
|
244
|
-
baseUrl,
|
|
245
|
-
reuseToken = false,
|
|
246
|
-
p2pkPubkey,
|
|
247
|
-
excludeMints = [],
|
|
248
|
-
retryCount = 0
|
|
249
|
-
} = options;
|
|
250
|
-
this._isBusy = true;
|
|
251
|
-
try {
|
|
252
|
-
const result = await this._spendInternal({
|
|
253
|
-
mintUrl,
|
|
254
|
-
amount,
|
|
255
|
-
baseUrl,
|
|
256
|
-
reuseToken,
|
|
257
|
-
p2pkPubkey,
|
|
258
|
-
excludeMints,
|
|
259
|
-
retryCount
|
|
260
|
-
});
|
|
261
|
-
if (result.status === "failed" || !result.token) {
|
|
262
|
-
const errorMsg = result.error || `Insufficient balance. Need ${amount} sats.`;
|
|
263
|
-
if (this._isNetworkError(errorMsg)) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
`Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
if (result.errorDetails) {
|
|
269
|
-
throw new InsufficientBalanceError(
|
|
270
|
-
result.errorDetails.required,
|
|
271
|
-
result.errorDetails.available,
|
|
272
|
-
result.errorDetails.maxMintBalance,
|
|
273
|
-
result.errorDetails.maxMintUrl
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
throw new Error(errorMsg);
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
} finally {
|
|
280
|
-
this._isBusy = false;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Check if error message indicates a network error
|
|
285
|
-
*/
|
|
286
|
-
_isNetworkError(message) {
|
|
287
|
-
return isNetworkErrorMessage(message) || message.includes("Your mint") && message.includes("unreachable");
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Internal spending logic
|
|
291
|
-
*/
|
|
292
|
-
async _spendInternal(options) {
|
|
293
|
-
let {
|
|
294
|
-
mintUrl,
|
|
295
|
-
amount,
|
|
296
|
-
baseUrl,
|
|
297
|
-
reuseToken,
|
|
298
|
-
p2pkPubkey,
|
|
299
|
-
excludeMints,
|
|
300
|
-
retryCount
|
|
301
|
-
} = options;
|
|
302
|
-
this._log(
|
|
303
|
-
"DEBUG",
|
|
304
|
-
`[CashuSpender] _spendInternal: amount=${amount}, mintUrl=${mintUrl}, baseUrl=${baseUrl}, reuseToken=${reuseToken}`
|
|
305
|
-
);
|
|
306
|
-
let adjustedAmount = Math.ceil(amount);
|
|
307
|
-
if (!adjustedAmount || isNaN(adjustedAmount)) {
|
|
308
|
-
this._log(
|
|
309
|
-
"ERROR",
|
|
310
|
-
`[CashuSpender] _spendInternal: Invalid amount: ${amount}`
|
|
311
|
-
);
|
|
312
|
-
return {
|
|
313
|
-
token: null,
|
|
314
|
-
status: "failed",
|
|
315
|
-
balance: 0,
|
|
316
|
-
error: "Please enter a valid amount"
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
if (reuseToken && baseUrl) {
|
|
320
|
-
this._log(
|
|
321
|
-
"DEBUG",
|
|
322
|
-
`[CashuSpender] _spendInternal: Attempting to reuse token for ${baseUrl}`
|
|
323
|
-
);
|
|
324
|
-
const existingResult = await this._tryReuseToken(
|
|
325
|
-
baseUrl,
|
|
326
|
-
adjustedAmount,
|
|
327
|
-
mintUrl
|
|
328
|
-
);
|
|
329
|
-
if (existingResult) {
|
|
330
|
-
this._log(
|
|
331
|
-
"DEBUG",
|
|
332
|
-
`[CashuSpender] _spendInternal: Successfully reused token, balance: ${existingResult.balance}`
|
|
333
|
-
);
|
|
334
|
-
return existingResult;
|
|
335
|
-
}
|
|
336
|
-
this._log(
|
|
337
|
-
"DEBUG",
|
|
338
|
-
`[CashuSpender] _spendInternal: Could not reuse token, will create new token`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
const balanceState = await this._getBalanceState();
|
|
342
|
-
const totalAvailableBalance = balanceState.totalBalance;
|
|
343
|
-
this._log(
|
|
344
|
-
"DEBUG",
|
|
345
|
-
`[CashuSpender] _spendInternal: totalAvailableBalance=${totalAvailableBalance}, adjustedAmount=${adjustedAmount}`
|
|
346
|
-
);
|
|
347
|
-
if (totalAvailableBalance < adjustedAmount) {
|
|
348
|
-
this._log(
|
|
349
|
-
"ERROR",
|
|
350
|
-
`[CashuSpender] _spendInternal: Insufficient balance, have=${totalAvailableBalance}, need=${adjustedAmount}`
|
|
351
|
-
);
|
|
352
|
-
return this._createInsufficientBalanceError(
|
|
353
|
-
adjustedAmount,
|
|
354
|
-
balanceState.mintBalances,
|
|
355
|
-
totalAvailableBalance
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
let token = null;
|
|
359
|
-
let selectedMintUrl;
|
|
360
|
-
let spentAmount = adjustedAmount;
|
|
361
|
-
if (this.balanceManager) {
|
|
362
|
-
const tokenResult = await this.balanceManager.createProviderToken({
|
|
363
|
-
mintUrl,
|
|
364
|
-
baseUrl,
|
|
365
|
-
amount: adjustedAmount,
|
|
366
|
-
p2pkPubkey,
|
|
367
|
-
excludeMints,
|
|
368
|
-
retryCount
|
|
369
|
-
});
|
|
370
|
-
if (!tokenResult.success || !tokenResult.token) {
|
|
371
|
-
if ((tokenResult.error || "").includes("Insufficient balance")) {
|
|
372
|
-
return this._createInsufficientBalanceError(
|
|
373
|
-
adjustedAmount,
|
|
374
|
-
balanceState.mintBalances,
|
|
375
|
-
totalAvailableBalance
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
return {
|
|
379
|
-
token: null,
|
|
380
|
-
status: "failed",
|
|
381
|
-
balance: 0,
|
|
382
|
-
error: tokenResult.error || "Failed to create token"
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
token = tokenResult.token;
|
|
386
|
-
selectedMintUrl = tokenResult.selectedMintUrl;
|
|
387
|
-
spentAmount = tokenResult.amountSpent || adjustedAmount;
|
|
388
|
-
} else {
|
|
389
|
-
try {
|
|
390
|
-
token = await this.walletAdapter.sendToken(
|
|
391
|
-
mintUrl,
|
|
392
|
-
adjustedAmount,
|
|
393
|
-
p2pkPubkey
|
|
394
|
-
);
|
|
395
|
-
selectedMintUrl = mintUrl;
|
|
396
|
-
} catch (error) {
|
|
397
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
398
|
-
return {
|
|
399
|
-
token: null,
|
|
400
|
-
status: "failed",
|
|
401
|
-
balance: 0,
|
|
402
|
-
error: `Error generating token: ${errorMsg}`
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (token) {
|
|
407
|
-
this._log(
|
|
408
|
-
"DEBUG",
|
|
409
|
-
`[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
this._logTransaction("spend", {
|
|
413
|
-
amount: spentAmount,
|
|
414
|
-
mintUrl: selectedMintUrl || mintUrl,
|
|
415
|
-
baseUrl,
|
|
416
|
-
status: "success"
|
|
417
|
-
});
|
|
418
|
-
this._log(
|
|
419
|
-
"DEBUG",
|
|
420
|
-
`[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
|
|
421
|
-
);
|
|
422
|
-
const units = this.walletAdapter.getMintUnits();
|
|
423
|
-
return {
|
|
424
|
-
token,
|
|
425
|
-
status: "success",
|
|
426
|
-
balance: spentAmount,
|
|
427
|
-
unit: (selectedMintUrl ? units[selectedMintUrl] : units[mintUrl]) || "sat"
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Try to reuse an existing API key
|
|
432
|
-
*/
|
|
433
|
-
async _tryReuseToken(baseUrl, amount, mintUrl) {
|
|
434
|
-
const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
435
|
-
if (!apiKeyEntry) return null;
|
|
436
|
-
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
437
|
-
const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
|
|
438
|
-
this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
|
|
439
|
-
if (balanceForBaseUrl > amount) {
|
|
440
|
-
const units = this.walletAdapter.getMintUnits();
|
|
441
|
-
const unit = units[mintUrl] || "sat";
|
|
442
|
-
return {
|
|
443
|
-
token: apiKeyEntry.key,
|
|
444
|
-
status: "success",
|
|
445
|
-
balance: balanceForBaseUrl,
|
|
446
|
-
unit
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
if (this.balanceManager) {
|
|
450
|
-
const topUpAmount = Math.ceil(amount * 1.2 - balanceForBaseUrl);
|
|
451
|
-
const topUpResult = await this.balanceManager.topUp({
|
|
452
|
-
mintUrl,
|
|
453
|
-
baseUrl,
|
|
454
|
-
amount: topUpAmount,
|
|
455
|
-
token: apiKeyEntry.key
|
|
456
|
-
});
|
|
457
|
-
this._log("DEBUG", "TOPUP ", topUpResult);
|
|
458
|
-
if (topUpResult.success && topUpResult.toppedUpAmount) {
|
|
459
|
-
const newBalance = balanceForBaseUrl + topUpResult.toppedUpAmount;
|
|
460
|
-
const units = this.walletAdapter.getMintUnits();
|
|
461
|
-
const unit = units[mintUrl] || "sat";
|
|
462
|
-
this._logTransaction("topup", {
|
|
463
|
-
amount: topUpResult.toppedUpAmount,
|
|
464
|
-
mintUrl,
|
|
465
|
-
baseUrl,
|
|
466
|
-
status: "success"
|
|
467
|
-
});
|
|
468
|
-
return {
|
|
469
|
-
token: apiKeyEntry.key,
|
|
470
|
-
status: "success",
|
|
471
|
-
balance: newBalance,
|
|
472
|
-
unit
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
const providerBalance = await this._getProviderTokenBalance(
|
|
476
|
-
baseUrl,
|
|
477
|
-
apiKeyEntry.key
|
|
478
|
-
);
|
|
479
|
-
this._log("DEBUG", providerBalance);
|
|
480
|
-
if (providerBalance <= 0) {
|
|
481
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Refund all xcashu tokens from storage by calling the provider's refund endpoint.
|
|
488
|
-
* The xcashu token acts as an API key to claim the refund, and the response contains
|
|
489
|
-
* the actual refunded Cashu token which is then received into the wallet.
|
|
490
|
-
* @param mintUrl - The mint URL for receiving tokens
|
|
491
|
-
* @param excludeBaseUrls - Base URLs to exclude from refund (optional)
|
|
492
|
-
* @returns Results for each xcashu token refund attempt
|
|
493
|
-
*/
|
|
494
|
-
async refundXcashuTokens(mintUrl, excludeBaseUrls) {
|
|
495
|
-
const results = [];
|
|
496
|
-
const xcashuTokens = this.storageAdapter.getXcashuTokens();
|
|
497
|
-
const excludedUrls = new Set(excludeBaseUrls || []);
|
|
498
|
-
for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
|
|
499
|
-
if (excludedUrls.has(baseUrl)) continue;
|
|
500
|
-
for (const xcashuToken of tokens) {
|
|
501
|
-
try {
|
|
502
|
-
if (!this.balanceManager) {
|
|
503
|
-
throw new Error("BalanceManager not available for xcashu refund");
|
|
504
|
-
}
|
|
505
|
-
const fetchResult = await this.balanceManager.fetchRefundToken(
|
|
506
|
-
baseUrl,
|
|
507
|
-
xcashuToken.token,
|
|
508
|
-
true
|
|
509
|
-
);
|
|
510
|
-
if (!fetchResult.success || !fetchResult.token) {
|
|
511
|
-
throw new Error(
|
|
512
|
-
fetchResult.error || "Failed to fetch refund token from provider"
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
const receiveResult = await this.receiveToken(fetchResult.token);
|
|
516
|
-
if (receiveResult.success) {
|
|
517
|
-
this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
|
|
518
|
-
results.push({
|
|
519
|
-
baseUrl,
|
|
520
|
-
token: xcashuToken.token,
|
|
521
|
-
success: true
|
|
522
|
-
});
|
|
523
|
-
this._log(
|
|
524
|
-
"DEBUG",
|
|
525
|
-
`[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
|
|
526
|
-
);
|
|
527
|
-
} else {
|
|
528
|
-
const currentTryCount = xcashuToken.tryCount ?? 0;
|
|
529
|
-
const newTryCount = currentTryCount + 1;
|
|
530
|
-
this.storageAdapter.updateXcashuTokenTryCount(
|
|
531
|
-
xcashuToken.token,
|
|
532
|
-
newTryCount
|
|
533
|
-
);
|
|
534
|
-
results.push({
|
|
535
|
-
baseUrl,
|
|
536
|
-
token: xcashuToken.token,
|
|
537
|
-
success: false,
|
|
538
|
-
error: receiveResult.message ?? "Refund failed"
|
|
539
|
-
});
|
|
540
|
-
this._log(
|
|
541
|
-
"DEBUG",
|
|
542
|
-
`[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}: ${receiveResult.message}`
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
} catch (error) {
|
|
546
|
-
const currentTryCount = xcashuToken.tryCount ?? 0;
|
|
547
|
-
const newTryCount = currentTryCount + 1;
|
|
548
|
-
this.storageAdapter.updateXcashuTokenTryCount(
|
|
549
|
-
xcashuToken.token,
|
|
550
|
-
newTryCount
|
|
551
|
-
);
|
|
552
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
553
|
-
results.push({
|
|
554
|
-
baseUrl,
|
|
555
|
-
token: xcashuToken.token,
|
|
556
|
-
success: false,
|
|
557
|
-
error: errorMessage
|
|
558
|
-
});
|
|
559
|
-
this._log(
|
|
560
|
-
"ERROR",
|
|
561
|
-
`[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return results;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Refund specific providers without retrying spend
|
|
570
|
-
*/
|
|
571
|
-
async refundProviders(mintUrl, forceRefund) {
|
|
572
|
-
const results = [];
|
|
573
|
-
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
574
|
-
for (const apiKeyEntry of apiKeyDistribution) {
|
|
575
|
-
const apiKeyEntryFull = this.storageAdapter.getApiKey(
|
|
576
|
-
apiKeyEntry.baseUrl
|
|
577
|
-
);
|
|
578
|
-
if (apiKeyEntryFull && this.balanceManager) {
|
|
579
|
-
try {
|
|
580
|
-
const balanceResult = await this.balanceManager.getTokenBalance(
|
|
581
|
-
apiKeyEntryFull.key,
|
|
582
|
-
apiKeyEntry.baseUrl
|
|
583
|
-
);
|
|
584
|
-
if (balanceResult.isInvalidApiKey) {
|
|
585
|
-
this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
|
|
586
|
-
results.push({
|
|
587
|
-
baseUrl: apiKeyEntry.baseUrl,
|
|
588
|
-
success: true
|
|
589
|
-
});
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
if (balanceResult.amount >= 0) {
|
|
593
|
-
const balanceSat = balanceResult.unit === "msat" ? Math.floor(balanceResult.amount / 1e3) : balanceResult.amount;
|
|
594
|
-
this.storageAdapter.updateApiKeyBalance(
|
|
595
|
-
apiKeyEntry.baseUrl,
|
|
596
|
-
balanceSat
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
} catch {
|
|
600
|
-
}
|
|
601
|
-
const refreshedEntry = this.storageAdapter.getApiKey(
|
|
602
|
-
apiKeyEntry.baseUrl
|
|
603
|
-
);
|
|
604
|
-
if (!refreshedEntry) continue;
|
|
605
|
-
const refundResult = await this.balanceManager.refundApiKey({
|
|
606
|
-
mintUrl,
|
|
607
|
-
baseUrl: apiKeyEntry.baseUrl,
|
|
608
|
-
apiKey: refreshedEntry.key,
|
|
609
|
-
forceRefund
|
|
610
|
-
});
|
|
611
|
-
if (refundResult.success) {
|
|
612
|
-
this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
|
|
613
|
-
} else {
|
|
614
|
-
const currentEntry = this.storageAdapter.getApiKey(
|
|
615
|
-
apiKeyEntry.baseUrl
|
|
616
|
-
);
|
|
617
|
-
if (currentEntry) {
|
|
618
|
-
this.storageAdapter.updateApiKeyBalance(
|
|
619
|
-
apiKeyEntry.baseUrl,
|
|
620
|
-
currentEntry.balance
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
results.push({
|
|
625
|
-
baseUrl: apiKeyEntry.baseUrl,
|
|
626
|
-
success: refundResult.success
|
|
627
|
-
});
|
|
628
|
-
} else {
|
|
629
|
-
results.push({
|
|
630
|
-
baseUrl: apiKeyEntry.baseUrl,
|
|
631
|
-
success: false
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return results;
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Create an insufficient balance error result
|
|
639
|
-
*/
|
|
640
|
-
_createInsufficientBalanceError(required, normalizedBalances, availableBalance) {
|
|
641
|
-
let maxBalance = 0;
|
|
642
|
-
let maxMintUrl = "";
|
|
643
|
-
for (const mintUrl in normalizedBalances) {
|
|
644
|
-
const balanceInSats = normalizedBalances[mintUrl];
|
|
645
|
-
if (balanceInSats > maxBalance) {
|
|
646
|
-
maxBalance = balanceInSats;
|
|
647
|
-
maxMintUrl = mintUrl;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
const error = new InsufficientBalanceError(
|
|
651
|
-
required,
|
|
652
|
-
availableBalance ?? maxBalance,
|
|
653
|
-
maxBalance,
|
|
654
|
-
maxMintUrl
|
|
655
|
-
);
|
|
656
|
-
return {
|
|
657
|
-
token: null,
|
|
658
|
-
status: "failed",
|
|
659
|
-
balance: 0,
|
|
660
|
-
error: error.message,
|
|
661
|
-
errorDetails: {
|
|
662
|
-
required,
|
|
663
|
-
available: availableBalance ?? maxBalance,
|
|
664
|
-
maxMintBalance: maxBalance,
|
|
665
|
-
maxMintUrl
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
async _getProviderTokenBalance(baseUrl, token) {
|
|
670
|
-
try {
|
|
671
|
-
const response = await fetch(`${baseUrl}v1/wallet/info`, {
|
|
672
|
-
headers: {
|
|
673
|
-
Authorization: `Bearer ${token}`
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
if (response.ok) {
|
|
677
|
-
const data = await response.json();
|
|
678
|
-
return data.balance / 1e3;
|
|
679
|
-
}
|
|
680
|
-
} catch {
|
|
681
|
-
return 0;
|
|
682
|
-
}
|
|
683
|
-
return 0;
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// wallet/BalanceManager.ts
|
|
688
|
-
var BalanceManager = class _BalanceManager {
|
|
689
|
-
constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender, logger) {
|
|
690
|
-
this.walletAdapter = walletAdapter;
|
|
691
|
-
this.storageAdapter = storageAdapter;
|
|
692
|
-
this.providerRegistry = providerRegistry;
|
|
693
|
-
this.logger = (logger ?? consoleLogger).child("BalanceManager");
|
|
694
|
-
if (cashuSpender) {
|
|
695
|
-
this.cashuSpender = cashuSpender;
|
|
696
|
-
} else {
|
|
697
|
-
this.cashuSpender = new CashuSpender(
|
|
698
|
-
walletAdapter,
|
|
699
|
-
storageAdapter,
|
|
700
|
-
providerRegistry,
|
|
701
|
-
this
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
cashuSpender;
|
|
706
|
-
/** In-memory guard for per-provider wallet mutations (topup / refund) */
|
|
707
|
-
providerWalletOps = /* @__PURE__ */ new Map();
|
|
708
|
-
/** Cooldown (ms) between opposite operations on the same provider */
|
|
709
|
-
static PROVIDER_WALLET_COOLDOWN_MS = 1e4;
|
|
710
|
-
logger;
|
|
711
|
-
/**
|
|
712
|
-
* Check whether a wallet operation (topup/refund) may run for a provider.
|
|
713
|
-
* Returns the reason when blocked.
|
|
714
|
-
*/
|
|
715
|
-
_canRunProviderWalletOperation(baseUrl, type) {
|
|
716
|
-
const existing = this.providerWalletOps.get(baseUrl);
|
|
717
|
-
if (!existing) {
|
|
718
|
-
return { allowed: true };
|
|
719
|
-
}
|
|
720
|
-
if (existing.type === type) {
|
|
721
|
-
return { allowed: true };
|
|
722
|
-
}
|
|
723
|
-
if (!existing.endTime) {
|
|
724
|
-
return {
|
|
725
|
-
allowed: false,
|
|
726
|
-
reason: `Provider wallet operation locked; ${existing.type} in progress`
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
const elapsed = Date.now() - existing.endTime;
|
|
730
|
-
if (elapsed < _BalanceManager.PROVIDER_WALLET_COOLDOWN_MS) {
|
|
731
|
-
return {
|
|
732
|
-
allowed: false,
|
|
733
|
-
reason: `Provider wallet operation locked; recent ${existing.type} completed ${Math.round(elapsed / 1e3)}s ago`
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
this.providerWalletOps.delete(baseUrl);
|
|
737
|
-
return { allowed: true };
|
|
738
|
-
}
|
|
739
|
-
_beginProviderWalletOperation(baseUrl, type) {
|
|
740
|
-
this.providerWalletOps.set(baseUrl, { type, startTime: Date.now() });
|
|
741
|
-
}
|
|
742
|
-
_endProviderWalletOperation(baseUrl, type) {
|
|
743
|
-
const existing = this.providerWalletOps.get(baseUrl);
|
|
744
|
-
if (existing && existing.type === type) {
|
|
745
|
-
existing.endTime = Date.now();
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
async getBalanceState() {
|
|
749
|
-
const mintBalances = await this.walletAdapter.getBalances();
|
|
750
|
-
const units = this.walletAdapter.getMintUnits();
|
|
751
|
-
let totalMintBalance = 0;
|
|
752
|
-
const normalizedMintBalances = {};
|
|
753
|
-
for (const url in mintBalances) {
|
|
754
|
-
const balance = mintBalances[url];
|
|
755
|
-
const unit = units[url];
|
|
756
|
-
const balanceInSats = getBalanceInSats(balance, unit);
|
|
757
|
-
normalizedMintBalances[url] = balanceInSats;
|
|
758
|
-
totalMintBalance += balanceInSats;
|
|
759
|
-
}
|
|
760
|
-
const providerBalances = {};
|
|
761
|
-
let totalProviderBalance = 0;
|
|
762
|
-
const apiKeys = this.storageAdapter.getAllApiKeys();
|
|
763
|
-
for (const apiKey of apiKeys) {
|
|
764
|
-
if (!providerBalances[apiKey.baseUrl]) {
|
|
765
|
-
providerBalances[apiKey.baseUrl] = 0;
|
|
766
|
-
}
|
|
767
|
-
providerBalances[apiKey.baseUrl] += apiKey.balance;
|
|
768
|
-
totalProviderBalance += apiKey.balance;
|
|
769
|
-
}
|
|
770
|
-
return {
|
|
771
|
-
totalBalance: totalMintBalance + totalProviderBalance,
|
|
772
|
-
providerBalances,
|
|
773
|
-
mintBalances: normalizedMintBalances
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Refund API key balance - convert remaining API key balance to cashu token
|
|
778
|
-
* @param options - Refund options including forceRefund flag
|
|
779
|
-
* @returns Refund result
|
|
780
|
-
*/
|
|
781
|
-
async refundApiKey(options) {
|
|
782
|
-
const { mintUrl, baseUrl, apiKey, forceRefund } = options;
|
|
783
|
-
const guard = this._canRunProviderWalletOperation(baseUrl, "refund");
|
|
784
|
-
if (!guard.allowed) {
|
|
785
|
-
this.logger.log(`Skipping refund for ${baseUrl} - ${guard.reason}`);
|
|
786
|
-
return { success: false, message: guard.reason };
|
|
787
|
-
}
|
|
788
|
-
this._beginProviderWalletOperation(baseUrl, "refund");
|
|
789
|
-
try {
|
|
790
|
-
return await this._refundApiKeyImpl({ mintUrl, baseUrl, apiKey, forceRefund });
|
|
791
|
-
} finally {
|
|
792
|
-
this._endProviderWalletOperation(baseUrl, "refund");
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
async _refundApiKeyImpl(options) {
|
|
796
|
-
const { mintUrl, baseUrl, apiKey, forceRefund } = options;
|
|
797
|
-
if (!apiKey) {
|
|
798
|
-
return { success: false, message: "No API key to refund" };
|
|
799
|
-
}
|
|
800
|
-
if (!forceRefund) {
|
|
801
|
-
const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
802
|
-
if (apiKeyEntry?.lastUsed) {
|
|
803
|
-
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
|
|
804
|
-
if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
|
|
805
|
-
this.logger.log(`Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`);
|
|
806
|
-
return {
|
|
807
|
-
success: false,
|
|
808
|
-
message: "API key was used recently, skipping refund"
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
let fetchResult;
|
|
814
|
-
try {
|
|
815
|
-
fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
|
|
816
|
-
if (!fetchResult.success) {
|
|
817
|
-
return {
|
|
818
|
-
success: false,
|
|
819
|
-
message: fetchResult.error || "API key refund failed",
|
|
820
|
-
requestId: fetchResult.requestId
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
if (!fetchResult.token) {
|
|
824
|
-
return {
|
|
825
|
-
success: false,
|
|
826
|
-
message: "No token received from API key refund",
|
|
827
|
-
requestId: fetchResult.requestId
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
if (fetchResult.error === "No balance to refund") {
|
|
831
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
832
|
-
return { success: true, message: "No balance to refund, key cleaned up" };
|
|
833
|
-
}
|
|
834
|
-
const receiveResult = await this.cashuSpender.receiveToken(
|
|
835
|
-
fetchResult.token
|
|
836
|
-
);
|
|
837
|
-
const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
|
|
838
|
-
if (receiveResult.success) {
|
|
839
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
840
|
-
}
|
|
841
|
-
return {
|
|
842
|
-
success: receiveResult.success,
|
|
843
|
-
refundedAmount: totalAmountMsat,
|
|
844
|
-
message: receiveResult.message,
|
|
845
|
-
requestId: fetchResult.requestId
|
|
846
|
-
};
|
|
847
|
-
} catch (error) {
|
|
848
|
-
this.logger.error("API key refund error", error);
|
|
849
|
-
return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Fetch refund token from provider API using API key (or xcashu token) authentication
|
|
854
|
-
*/
|
|
855
|
-
async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
|
|
856
|
-
if (!baseUrl) {
|
|
857
|
-
return {
|
|
858
|
-
success: false,
|
|
859
|
-
error: "No base URL configured"
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
863
|
-
const url = `${normalizedBaseUrl}v1/wallet/refund`;
|
|
864
|
-
const controller = new AbortController();
|
|
865
|
-
const timeoutId = setTimeout(() => {
|
|
866
|
-
controller.abort();
|
|
867
|
-
}, 6e4);
|
|
868
|
-
try {
|
|
869
|
-
const headers = {
|
|
870
|
-
"Content-Type": "application/json"
|
|
871
|
-
};
|
|
872
|
-
if (xCashu) {
|
|
873
|
-
headers["X-Cashu"] = apiKeyOrToken;
|
|
874
|
-
} else {
|
|
875
|
-
headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
|
|
876
|
-
}
|
|
877
|
-
const response = await fetch(url, {
|
|
878
|
-
method: "POST",
|
|
879
|
-
headers,
|
|
880
|
-
signal: controller.signal
|
|
881
|
-
});
|
|
882
|
-
clearTimeout(timeoutId);
|
|
883
|
-
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
884
|
-
if (!response.ok) {
|
|
885
|
-
const errorData = await response.json().catch(() => ({}));
|
|
886
|
-
return {
|
|
887
|
-
success: false,
|
|
888
|
-
requestId,
|
|
889
|
-
error: `API key refund failed: ${errorData?.detail || response.statusText}`
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
const data = await response.json();
|
|
893
|
-
return {
|
|
894
|
-
success: true,
|
|
895
|
-
token: data.token,
|
|
896
|
-
requestId
|
|
897
|
-
};
|
|
898
|
-
} catch (error) {
|
|
899
|
-
clearTimeout(timeoutId);
|
|
900
|
-
this.logger.error("fetchRefundToken fetch error", error);
|
|
901
|
-
if (error instanceof Error) {
|
|
902
|
-
if (error.name === "AbortError") {
|
|
903
|
-
return {
|
|
904
|
-
success: false,
|
|
905
|
-
error: "Request timed out after 1 minute"
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
return {
|
|
909
|
-
success: false,
|
|
910
|
-
error: error.message
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
success: false,
|
|
915
|
-
error: "Unknown error occurred during API key refund request"
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Top up API key balance with a cashu token
|
|
921
|
-
*/
|
|
922
|
-
async topUp(options) {
|
|
923
|
-
const { mintUrl, baseUrl, amount, token: providedToken } = options;
|
|
924
|
-
const guard = this._canRunProviderWalletOperation(baseUrl, "topup");
|
|
925
|
-
if (!guard.allowed) {
|
|
926
|
-
this.logger.log(`Skipping topup for ${baseUrl} - ${guard.reason}`);
|
|
927
|
-
return { success: false, message: guard.reason };
|
|
928
|
-
}
|
|
929
|
-
this._beginProviderWalletOperation(baseUrl, "topup");
|
|
930
|
-
try {
|
|
931
|
-
return await this._topUpImpl({ mintUrl, baseUrl, amount, token: providedToken });
|
|
932
|
-
} finally {
|
|
933
|
-
this._endProviderWalletOperation(baseUrl, "topup");
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
async _topUpImpl(options) {
|
|
937
|
-
const { mintUrl, baseUrl, amount, token: providedToken } = options;
|
|
938
|
-
if (!amount || amount <= 0) {
|
|
939
|
-
return { success: false, message: "Invalid top up amount" };
|
|
940
|
-
}
|
|
941
|
-
const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
|
|
942
|
-
const apiKey = providedToken || apiKeyEntry?.key;
|
|
943
|
-
if (!apiKey) {
|
|
944
|
-
return { success: false, message: "No API key available for top up" };
|
|
945
|
-
}
|
|
946
|
-
let cashuToken = null;
|
|
947
|
-
let requestId;
|
|
948
|
-
try {
|
|
949
|
-
const tokenResult = await this.createProviderToken({
|
|
950
|
-
mintUrl,
|
|
951
|
-
baseUrl,
|
|
952
|
-
amount
|
|
953
|
-
});
|
|
954
|
-
if (!tokenResult.success || !tokenResult.token) {
|
|
955
|
-
return {
|
|
956
|
-
success: false,
|
|
957
|
-
message: tokenResult.error || "Unable to create top up token"
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
cashuToken = tokenResult.token;
|
|
961
|
-
const topUpResult = await this._postTopUp(baseUrl, apiKey, cashuToken);
|
|
962
|
-
requestId = topUpResult.requestId;
|
|
963
|
-
this.logger.log("topUpResult:", topUpResult);
|
|
964
|
-
if (!topUpResult.success) {
|
|
965
|
-
await this._recoverFailedTopUp(cashuToken);
|
|
966
|
-
return {
|
|
967
|
-
success: false,
|
|
968
|
-
message: topUpResult.error || "Top up failed",
|
|
969
|
-
requestId,
|
|
970
|
-
recoveredToken: true
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
return {
|
|
974
|
-
success: true,
|
|
975
|
-
toppedUpAmount: amount,
|
|
976
|
-
requestId
|
|
977
|
-
};
|
|
978
|
-
} catch (error) {
|
|
979
|
-
this.logger.log(`topup error for ${baseUrl}: ${error}`);
|
|
980
|
-
if (cashuToken) {
|
|
981
|
-
await this._recoverFailedTopUp(cashuToken);
|
|
982
|
-
}
|
|
983
|
-
return this._handleTopUpError(error, mintUrl, requestId);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
async createProviderToken(options) {
|
|
987
|
-
const {
|
|
988
|
-
mintUrl,
|
|
989
|
-
baseUrl,
|
|
990
|
-
amount,
|
|
991
|
-
retryCount = 0,
|
|
992
|
-
excludeMints = [],
|
|
993
|
-
p2pkPubkey
|
|
994
|
-
} = options;
|
|
995
|
-
const adjustedAmount = Math.ceil(amount);
|
|
996
|
-
this.logger.log(`createProviderToken: baseUrl=${baseUrl} mintUrl=${mintUrl} amount=${amount} adjustedAmount=${adjustedAmount} retryCount=${retryCount}`);
|
|
997
|
-
if (!adjustedAmount || isNaN(adjustedAmount)) {
|
|
998
|
-
this.logger.error(`createProviderToken: invalid amount=${amount}`);
|
|
999
|
-
return { success: false, error: "Invalid top up amount" };
|
|
1000
|
-
}
|
|
1001
|
-
const balanceState = await this.getBalanceState();
|
|
1002
|
-
const balances = await this.walletAdapter.getBalances();
|
|
1003
|
-
const units = this.walletAdapter.getMintUnits();
|
|
1004
|
-
const totalMintBalance = Object.values(balanceState.mintBalances).reduce(
|
|
1005
|
-
(sum, value) => sum + value,
|
|
1006
|
-
0
|
|
1007
|
-
);
|
|
1008
|
-
const targetProviderBalance = balanceState.providerBalances[baseUrl] || 0;
|
|
1009
|
-
const refundableProviderBalance = Object.entries(
|
|
1010
|
-
balanceState.providerBalances
|
|
1011
|
-
).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
|
|
1012
|
-
if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
|
|
1013
|
-
await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
|
|
1014
|
-
return this.createProviderToken({
|
|
1015
|
-
...options,
|
|
1016
|
-
retryCount: retryCount + 1
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
if (totalMintBalance + targetProviderBalance < adjustedAmount) {
|
|
1020
|
-
const error = new InsufficientBalanceError(
|
|
1021
|
-
adjustedAmount,
|
|
1022
|
-
totalMintBalance + targetProviderBalance,
|
|
1023
|
-
totalMintBalance,
|
|
1024
|
-
Object.entries(balanceState.mintBalances).reduce(
|
|
1025
|
-
(max, [url, balance]) => balance > max.balance ? { url, balance } : max,
|
|
1026
|
-
{ url: "", balance: 0 }
|
|
1027
|
-
).url
|
|
1028
|
-
);
|
|
1029
|
-
this.logger.error(`createProviderToken: insufficient balance required=${adjustedAmount} available=${totalMintBalance + targetProviderBalance} totalMint=${totalMintBalance} targetProvider=${targetProviderBalance}`);
|
|
1030
|
-
return { success: false, error: error.message };
|
|
1031
|
-
}
|
|
1032
|
-
const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
|
|
1033
|
-
let requiredAmount = adjustedAmount;
|
|
1034
|
-
const supportedMintsOnly = providerMints.length > 0;
|
|
1035
|
-
let candidates = this._selectCandidateMints({
|
|
1036
|
-
balances,
|
|
1037
|
-
units,
|
|
1038
|
-
amount: requiredAmount,
|
|
1039
|
-
preferredMintUrl: mintUrl,
|
|
1040
|
-
excludeMints,
|
|
1041
|
-
allowedMints: supportedMintsOnly ? providerMints : void 0
|
|
1042
|
-
});
|
|
1043
|
-
if (candidates.length === 0 && supportedMintsOnly) {
|
|
1044
|
-
requiredAmount += 2;
|
|
1045
|
-
candidates = this._selectCandidateMints({
|
|
1046
|
-
balances,
|
|
1047
|
-
units,
|
|
1048
|
-
amount: requiredAmount,
|
|
1049
|
-
preferredMintUrl: mintUrl,
|
|
1050
|
-
excludeMints
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
if (candidates.length === 0) {
|
|
1054
|
-
let maxBalance = 0;
|
|
1055
|
-
let maxMintUrl = "";
|
|
1056
|
-
for (const mintUrl2 in balances) {
|
|
1057
|
-
const balance = balances[mintUrl2];
|
|
1058
|
-
const unit = units[mintUrl2];
|
|
1059
|
-
const balanceInSats = getBalanceInSats(balance, unit);
|
|
1060
|
-
if (balanceInSats > maxBalance) {
|
|
1061
|
-
maxBalance = balanceInSats;
|
|
1062
|
-
maxMintUrl = mintUrl2;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
this.logger.error(`createProviderToken: no candidate mints required=${requiredAmount} totalMint=${totalMintBalance} maxBalance=${maxBalance} maxMint=${maxMintUrl}`);
|
|
1066
|
-
const error = new InsufficientBalanceError(
|
|
1067
|
-
adjustedAmount,
|
|
1068
|
-
totalMintBalance,
|
|
1069
|
-
maxBalance,
|
|
1070
|
-
maxMintUrl
|
|
1071
|
-
);
|
|
1072
|
-
return { success: false, error: error.message };
|
|
1073
|
-
}
|
|
1074
|
-
let lastError;
|
|
1075
|
-
for (const candidateMint of candidates) {
|
|
1076
|
-
try {
|
|
1077
|
-
this.logger.log(`createProviderToken: attempting mint=${candidateMint} amount=${requiredAmount}`);
|
|
1078
|
-
const token = await this.walletAdapter.sendToken(
|
|
1079
|
-
candidateMint,
|
|
1080
|
-
requiredAmount,
|
|
1081
|
-
p2pkPubkey
|
|
1082
|
-
);
|
|
1083
|
-
this.logger.log(`createProviderToken: success from mint=${candidateMint}`);
|
|
1084
|
-
return {
|
|
1085
|
-
success: true,
|
|
1086
|
-
token,
|
|
1087
|
-
selectedMintUrl: candidateMint,
|
|
1088
|
-
amountSpent: requiredAmount
|
|
1089
|
-
};
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1092
|
-
this.logger.error(`createProviderToken: mint=${candidateMint} failed: ${errorMsg}`);
|
|
1093
|
-
if (error instanceof Error) {
|
|
1094
|
-
lastError = errorMsg;
|
|
1095
|
-
if (isNetworkErrorMessage(error.message)) {
|
|
1096
|
-
this.logger.warn(`createProviderToken: network error from ${candidateMint}, trying next mint...`);
|
|
1097
|
-
continue;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
return {
|
|
1101
|
-
success: false,
|
|
1102
|
-
error: lastError || "Failed to create top up token"
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
this.logger.error(`createProviderToken: all candidate mints exhausted lastError=${lastError}`);
|
|
1107
|
-
return {
|
|
1108
|
-
success: false,
|
|
1109
|
-
error: lastError || "All candidate mints failed while creating top up token"
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
_selectCandidateMints(options) {
|
|
1113
|
-
const {
|
|
1114
|
-
balances,
|
|
1115
|
-
units,
|
|
1116
|
-
amount,
|
|
1117
|
-
preferredMintUrl,
|
|
1118
|
-
excludeMints,
|
|
1119
|
-
allowedMints
|
|
1120
|
-
} = options;
|
|
1121
|
-
const candidates = [];
|
|
1122
|
-
const { selectedMintUrl: firstMint } = selectMintWithBalance(
|
|
1123
|
-
balances,
|
|
1124
|
-
units,
|
|
1125
|
-
amount,
|
|
1126
|
-
excludeMints
|
|
1127
|
-
);
|
|
1128
|
-
if (firstMint && (!allowedMints || allowedMints.length === 0 || allowedMints.includes(firstMint))) {
|
|
1129
|
-
candidates.push(firstMint);
|
|
1130
|
-
}
|
|
1131
|
-
const canUseMint = (mint) => {
|
|
1132
|
-
if (excludeMints.includes(mint)) return false;
|
|
1133
|
-
if (allowedMints && allowedMints.length > 0 && !allowedMints.includes(mint)) {
|
|
1134
|
-
return false;
|
|
1135
|
-
}
|
|
1136
|
-
const rawBalance = balances[mint] || 0;
|
|
1137
|
-
const unit = units[mint];
|
|
1138
|
-
const balanceInSats = getBalanceInSats(rawBalance, unit);
|
|
1139
|
-
return balanceInSats >= amount;
|
|
1140
|
-
};
|
|
1141
|
-
if (preferredMintUrl && canUseMint(preferredMintUrl) && !candidates.includes(preferredMintUrl)) {
|
|
1142
|
-
candidates.push(preferredMintUrl);
|
|
1143
|
-
}
|
|
1144
|
-
for (const mint in balances) {
|
|
1145
|
-
if (mint === preferredMintUrl || candidates.includes(mint)) continue;
|
|
1146
|
-
if (canUseMint(mint)) {
|
|
1147
|
-
candidates.push(mint);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return candidates;
|
|
1151
|
-
}
|
|
1152
|
-
async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
|
|
1153
|
-
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
1154
|
-
const forceRefund = retryCount >= 2;
|
|
1155
|
-
const apiKeysToRefund = apiKeyDistribution.filter(
|
|
1156
|
-
(apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
|
|
1157
|
-
);
|
|
1158
|
-
const apiKeyRefundResults = await Promise.allSettled(
|
|
1159
|
-
apiKeysToRefund.map(async (apiKeyEntry) => {
|
|
1160
|
-
const fullApiKeyEntry = this.storageAdapter.getApiKey(
|
|
1161
|
-
apiKeyEntry.baseUrl
|
|
1162
|
-
);
|
|
1163
|
-
if (!fullApiKeyEntry) {
|
|
1164
|
-
return { baseUrl: apiKeyEntry.baseUrl, success: false };
|
|
1165
|
-
}
|
|
1166
|
-
const result = await this.refundApiKey({
|
|
1167
|
-
mintUrl,
|
|
1168
|
-
baseUrl: apiKeyEntry.baseUrl,
|
|
1169
|
-
apiKey: fullApiKeyEntry.key,
|
|
1170
|
-
forceRefund
|
|
1171
|
-
});
|
|
1172
|
-
return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
|
|
1173
|
-
})
|
|
1174
|
-
);
|
|
1175
|
-
for (const result of apiKeyRefundResults) {
|
|
1176
|
-
if (result.status === "fulfilled" && result.value.success) {
|
|
1177
|
-
this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Post topup request to provider API
|
|
1183
|
-
*/
|
|
1184
|
-
async _postTopUp(baseUrl, storedToken, cashuToken) {
|
|
1185
|
-
if (!baseUrl) {
|
|
1186
|
-
return {
|
|
1187
|
-
success: false,
|
|
1188
|
-
error: "No base URL configured"
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1192
|
-
const url = `${normalizedBaseUrl}v1/wallet/topup?cashu_token=${encodeURIComponent(
|
|
1193
|
-
cashuToken
|
|
1194
|
-
)}`;
|
|
1195
|
-
const controller = new AbortController();
|
|
1196
|
-
const timeoutId = setTimeout(() => {
|
|
1197
|
-
controller.abort();
|
|
1198
|
-
}, 6e4);
|
|
1199
|
-
try {
|
|
1200
|
-
const response = await fetch(url, {
|
|
1201
|
-
method: "POST",
|
|
1202
|
-
headers: {
|
|
1203
|
-
Authorization: `Bearer ${storedToken}`,
|
|
1204
|
-
"Content-Type": "application/json"
|
|
1205
|
-
},
|
|
1206
|
-
signal: controller.signal
|
|
1207
|
-
});
|
|
1208
|
-
clearTimeout(timeoutId);
|
|
1209
|
-
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
1210
|
-
if (!response.ok) {
|
|
1211
|
-
const errorData = await response.json().catch(() => ({}));
|
|
1212
|
-
return {
|
|
1213
|
-
success: false,
|
|
1214
|
-
requestId,
|
|
1215
|
-
error: errorData?.detail || `Top up failed with status ${response.status}`
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
return { success: true, requestId };
|
|
1219
|
-
} catch (error) {
|
|
1220
|
-
clearTimeout(timeoutId);
|
|
1221
|
-
this.logger.error("_postTopUp fetch error", error);
|
|
1222
|
-
if (error instanceof Error) {
|
|
1223
|
-
if (error.name === "AbortError") {
|
|
1224
|
-
return {
|
|
1225
|
-
success: false,
|
|
1226
|
-
error: "Request timed out after 1 minute"
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
return {
|
|
1230
|
-
success: false,
|
|
1231
|
-
error: error.message
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
return {
|
|
1235
|
-
success: false,
|
|
1236
|
-
error: "Unknown error occurred during top up request"
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
/**
|
|
1241
|
-
* Attempt to receive token back after failed top up
|
|
1242
|
-
*/
|
|
1243
|
-
async _recoverFailedTopUp(cashuToken) {
|
|
1244
|
-
try {
|
|
1245
|
-
await this.cashuSpender.receiveToken(cashuToken);
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
this.logger.error("_recoverFailedTopUp: failed to recover token", error);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Handle refund errors with specific error types
|
|
1252
|
-
*/
|
|
1253
|
-
_handleRefundError(error, mintUrl, requestId) {
|
|
1254
|
-
if (error instanceof Error) {
|
|
1255
|
-
if (isNetworkErrorMessage(error.message)) {
|
|
1256
|
-
return {
|
|
1257
|
-
success: false,
|
|
1258
|
-
message: `Failed to connect to the mint: ${mintUrl}`,
|
|
1259
|
-
requestId
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
if (error.message.includes("Wallet not found")) {
|
|
1263
|
-
return {
|
|
1264
|
-
success: false,
|
|
1265
|
-
message: `Wallet couldn't be loaded. Please save this refunded cashu token manually.`,
|
|
1266
|
-
requestId
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
return {
|
|
1270
|
-
success: false,
|
|
1271
|
-
message: error.message,
|
|
1272
|
-
requestId
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
return {
|
|
1276
|
-
success: false,
|
|
1277
|
-
message: "Refund failed",
|
|
1278
|
-
requestId
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Get token balance from provider
|
|
1283
|
-
*/
|
|
1284
|
-
async getTokenBalance(token, baseUrl) {
|
|
1285
|
-
try {
|
|
1286
|
-
const response = await fetch(`${baseUrl}v1/wallet/info`, {
|
|
1287
|
-
headers: {
|
|
1288
|
-
Authorization: `Bearer ${token}`
|
|
1289
|
-
}
|
|
1290
|
-
});
|
|
1291
|
-
if (response.ok) {
|
|
1292
|
-
const data = await response.json();
|
|
1293
|
-
return {
|
|
1294
|
-
amount: data.balance,
|
|
1295
|
-
reserved: data.reserved ?? 0,
|
|
1296
|
-
unit: "msat",
|
|
1297
|
-
apiKey: data.api_key
|
|
1298
|
-
};
|
|
1299
|
-
} else {
|
|
1300
|
-
this.logger.warn(`getTokenBalance: status=${response.status}`);
|
|
1301
|
-
const data = await response.json();
|
|
1302
|
-
this.logger.warn("getTokenBalance: FAILED", data);
|
|
1303
|
-
const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
|
|
1304
|
-
return {
|
|
1305
|
-
amount: -1,
|
|
1306
|
-
reserved: data.reserved ?? 0,
|
|
1307
|
-
unit: "msat",
|
|
1308
|
-
apiKey: data.api_key,
|
|
1309
|
-
isInvalidApiKey
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
this.logger.error("getTokenBalance error", error);
|
|
1314
|
-
}
|
|
1315
|
-
return { amount: -1, reserved: 0, unit: "sat", apiKey: "" };
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Handle topup errors with specific error types
|
|
1319
|
-
*/
|
|
1320
|
-
_handleTopUpError(error, mintUrl, requestId) {
|
|
1321
|
-
if (error instanceof Error) {
|
|
1322
|
-
if (isNetworkErrorMessage(error.message)) {
|
|
1323
|
-
return {
|
|
1324
|
-
success: false,
|
|
1325
|
-
message: `Failed to connect to the mint: ${mintUrl}`,
|
|
1326
|
-
requestId
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
if (error.message.includes("Wallet not found")) {
|
|
1330
|
-
return {
|
|
1331
|
-
success: false,
|
|
1332
|
-
message: "Wallet couldn't be loaded. The cashu token was recovered locally.",
|
|
1333
|
-
requestId
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
return {
|
|
1337
|
-
success: false,
|
|
1338
|
-
message: error.message,
|
|
1339
|
-
requestId
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
return {
|
|
1343
|
-
success: false,
|
|
1344
|
-
message: "Top up failed",
|
|
1345
|
-
requestId
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
|
-
// client/usage.ts
|
|
1351
|
-
function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
|
|
1352
|
-
if (!body || typeof body !== "object") return null;
|
|
1353
|
-
const usage = body.usage;
|
|
1354
|
-
if (!usage || typeof usage !== "object") return null;
|
|
1355
|
-
const promptTokens = Number(usage.prompt_tokens ?? 0);
|
|
1356
|
-
const completionTokens = Number(usage.completion_tokens ?? 0);
|
|
1357
|
-
const totalTokens = Number(usage.total_tokens ?? 0);
|
|
1358
|
-
const costValue = usage.cost;
|
|
1359
|
-
let cost = 0;
|
|
1360
|
-
let satsCost = fallbackSatsCost;
|
|
1361
|
-
if (typeof costValue === "number") {
|
|
1362
|
-
cost = costValue;
|
|
1363
|
-
} else if (costValue && typeof costValue === "object") {
|
|
1364
|
-
const costObj = costValue;
|
|
1365
|
-
const totalUsd = costObj.total_usd;
|
|
1366
|
-
const totalMsats = costObj.total_msats;
|
|
1367
|
-
cost = typeof totalUsd === "number" ? totalUsd : 0;
|
|
1368
|
-
if (typeof totalMsats === "number") {
|
|
1369
|
-
satsCost = totalMsats / 1e3;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
|
|
1373
|
-
return null;
|
|
1374
|
-
}
|
|
1375
|
-
return {
|
|
1376
|
-
promptTokens,
|
|
1377
|
-
completionTokens,
|
|
1378
|
-
totalTokens,
|
|
1379
|
-
cost,
|
|
1380
|
-
satsCost
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
function extractResponseId(body) {
|
|
1384
|
-
if (!body || typeof body !== "object") return void 0;
|
|
1385
|
-
const id = body.id;
|
|
1386
|
-
if (typeof id !== "string") return void 0;
|
|
1387
|
-
const trimmed = id.trim();
|
|
1388
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
1389
|
-
}
|
|
1390
|
-
function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
|
|
1391
|
-
if (!parsed || typeof parsed !== "object") {
|
|
1392
|
-
return null;
|
|
1393
|
-
}
|
|
1394
|
-
if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
|
|
1395
|
-
const costObj = parsed.cost;
|
|
1396
|
-
const msats2 = costObj.total_msats ?? 0;
|
|
1397
|
-
const cost2 = costObj.total_usd ?? 0;
|
|
1398
|
-
if (msats2 === 0 && cost2 === 0) return null;
|
|
1399
|
-
return {
|
|
1400
|
-
promptTokens: Number(costObj.input_tokens ?? 0),
|
|
1401
|
-
completionTokens: Number(costObj.output_tokens ?? 0),
|
|
1402
|
-
totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
|
|
1403
|
-
cost: Number(cost2),
|
|
1404
|
-
satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
if (!parsed.usage) {
|
|
1408
|
-
return null;
|
|
1409
|
-
}
|
|
1410
|
-
const usage = parsed.usage;
|
|
1411
|
-
const usageCost = usage.cost;
|
|
1412
|
-
let cost = 0;
|
|
1413
|
-
let msats = 0;
|
|
1414
|
-
if (typeof usageCost === "number") {
|
|
1415
|
-
cost = usageCost;
|
|
1416
|
-
} else if (usageCost && typeof usageCost === "object") {
|
|
1417
|
-
cost = usageCost.total_usd ?? 0;
|
|
1418
|
-
msats = usageCost.total_msats ?? 0;
|
|
1419
|
-
}
|
|
1420
|
-
if (cost === 0) {
|
|
1421
|
-
cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
|
|
1422
|
-
}
|
|
1423
|
-
if (msats === 0) {
|
|
1424
|
-
msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
|
|
1425
|
-
}
|
|
1426
|
-
const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
|
|
1427
|
-
const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
|
|
1428
|
-
const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
|
|
1429
|
-
const result = {
|
|
1430
|
-
promptTokens,
|
|
1431
|
-
completionTokens,
|
|
1432
|
-
totalTokens,
|
|
1433
|
-
cost: Number(cost ?? 0),
|
|
1434
|
-
satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
|
|
1435
|
-
};
|
|
1436
|
-
if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
|
|
1437
|
-
return null;
|
|
1438
|
-
}
|
|
1439
|
-
return result;
|
|
1440
|
-
}
|
|
1441
|
-
function toUsageStats(usage) {
|
|
1442
|
-
if (!usage) return void 0;
|
|
1443
|
-
return {
|
|
1444
|
-
total_tokens: usage.totalTokens,
|
|
1445
|
-
prompt_tokens: usage.promptTokens,
|
|
1446
|
-
completion_tokens: usage.completionTokens,
|
|
1447
|
-
cost: usage.cost,
|
|
1448
|
-
sats_cost: usage.satsCost
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// client/StreamProcessor.ts
|
|
1453
|
-
var StreamProcessor = class {
|
|
1454
|
-
accumulatedContent = "";
|
|
1455
|
-
accumulatedThinking = "";
|
|
1456
|
-
accumulatedImages = [];
|
|
1457
|
-
isInThinking = false;
|
|
1458
|
-
isInContent = false;
|
|
1459
|
-
/**
|
|
1460
|
-
* Process a streaming response
|
|
1461
|
-
*/
|
|
1462
|
-
async process(response, callbacks, modelId) {
|
|
1463
|
-
if (!response.body) {
|
|
1464
|
-
throw new Error("Response body is not available");
|
|
1465
|
-
}
|
|
1466
|
-
const reader = response.body.getReader();
|
|
1467
|
-
const decoder = new TextDecoder("utf-8");
|
|
1468
|
-
let buffer = "";
|
|
1469
|
-
this.accumulatedContent = "";
|
|
1470
|
-
this.accumulatedThinking = "";
|
|
1471
|
-
this.accumulatedImages = [];
|
|
1472
|
-
this.isInThinking = false;
|
|
1473
|
-
this.isInContent = false;
|
|
1474
|
-
let usage;
|
|
1475
|
-
let model;
|
|
1476
|
-
let finish_reason;
|
|
1477
|
-
let citations;
|
|
1478
|
-
let annotations;
|
|
1479
|
-
let responseId;
|
|
1480
|
-
try {
|
|
1481
|
-
while (true) {
|
|
1482
|
-
const { done, value } = await reader.read();
|
|
1483
|
-
if (done) {
|
|
1484
|
-
break;
|
|
1485
|
-
}
|
|
1486
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
1487
|
-
buffer += chunk;
|
|
1488
|
-
const lines = buffer.split("\n");
|
|
1489
|
-
buffer = lines.pop() || "";
|
|
1490
|
-
for (const line of lines) {
|
|
1491
|
-
const parsed = this._parseLine(line);
|
|
1492
|
-
if (!parsed) continue;
|
|
1493
|
-
if (parsed.content) {
|
|
1494
|
-
this._handleContent(parsed.content, callbacks, modelId);
|
|
1495
|
-
}
|
|
1496
|
-
if (parsed.reasoning) {
|
|
1497
|
-
this._handleThinking(parsed.reasoning, callbacks);
|
|
1498
|
-
}
|
|
1499
|
-
if (parsed.usage) {
|
|
1500
|
-
usage = parsed.usage;
|
|
1501
|
-
}
|
|
1502
|
-
if (parsed.model) {
|
|
1503
|
-
model = parsed.model;
|
|
1504
|
-
}
|
|
1505
|
-
if (parsed.finish_reason) {
|
|
1506
|
-
finish_reason = parsed.finish_reason;
|
|
1507
|
-
}
|
|
1508
|
-
if (parsed.responseId) {
|
|
1509
|
-
responseId = parsed.responseId;
|
|
1510
|
-
}
|
|
1511
|
-
if (parsed.citations) {
|
|
1512
|
-
citations = parsed.citations;
|
|
1513
|
-
}
|
|
1514
|
-
if (parsed.annotations) {
|
|
1515
|
-
annotations = parsed.annotations;
|
|
1516
|
-
}
|
|
1517
|
-
if (parsed.images) {
|
|
1518
|
-
this._mergeImages(parsed.images);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
} finally {
|
|
1523
|
-
reader.releaseLock();
|
|
1524
|
-
}
|
|
1525
|
-
return {
|
|
1526
|
-
content: this.accumulatedContent,
|
|
1527
|
-
thinking: this.accumulatedThinking || void 0,
|
|
1528
|
-
images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
|
|
1529
|
-
usage,
|
|
1530
|
-
model,
|
|
1531
|
-
responseId,
|
|
1532
|
-
finish_reason,
|
|
1533
|
-
citations,
|
|
1534
|
-
annotations
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* Parse a single SSE line
|
|
1539
|
-
*/
|
|
1540
|
-
_parseLine(line) {
|
|
1541
|
-
if (!line.trim()) return null;
|
|
1542
|
-
if (!line.startsWith("data: ")) {
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
const jsonData = line.slice(6);
|
|
1546
|
-
if (jsonData === "[DONE]") {
|
|
1547
|
-
return null;
|
|
1548
|
-
}
|
|
1549
|
-
try {
|
|
1550
|
-
const parsed = JSON.parse(jsonData);
|
|
1551
|
-
const result = {};
|
|
1552
|
-
if (parsed.choices?.[0]?.delta?.content) {
|
|
1553
|
-
result.content = parsed.choices[0].delta.content;
|
|
1554
|
-
}
|
|
1555
|
-
if (parsed.choices?.[0]?.delta?.reasoning) {
|
|
1556
|
-
result.reasoning = parsed.choices[0].delta.reasoning;
|
|
1557
|
-
}
|
|
1558
|
-
const extractedUsage = extractUsageFromSSEJson(parsed);
|
|
1559
|
-
if (extractedUsage) {
|
|
1560
|
-
result.usage = toUsageStats(extractedUsage);
|
|
1561
|
-
} else if (parsed.usage) {
|
|
1562
|
-
result.usage = {
|
|
1563
|
-
total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
|
|
1564
|
-
prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
|
|
1565
|
-
completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
if (parsed.id) {
|
|
1569
|
-
result.responseId = parsed.id;
|
|
1570
|
-
}
|
|
1571
|
-
if (parsed.model) {
|
|
1572
|
-
result.model = parsed.model;
|
|
1573
|
-
}
|
|
1574
|
-
if (parsed.citations) {
|
|
1575
|
-
result.citations = parsed.citations;
|
|
1576
|
-
}
|
|
1577
|
-
if (parsed.annotations) {
|
|
1578
|
-
result.annotations = parsed.annotations;
|
|
1579
|
-
}
|
|
1580
|
-
if (parsed.choices?.[0]?.finish_reason) {
|
|
1581
|
-
result.finish_reason = parsed.choices[0].finish_reason;
|
|
1582
|
-
}
|
|
1583
|
-
const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
|
|
1584
|
-
if (images && Array.isArray(images)) {
|
|
1585
|
-
result.images = images;
|
|
1586
|
-
}
|
|
1587
|
-
return result;
|
|
1588
|
-
} catch {
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
/**
|
|
1593
|
-
* Handle content delta with thinking support
|
|
1594
|
-
*/
|
|
1595
|
-
_handleContent(content, callbacks, modelId) {
|
|
1596
|
-
if (this.isInThinking && !this.isInContent) {
|
|
1597
|
-
this.accumulatedThinking += "</thinking>";
|
|
1598
|
-
callbacks.onThinking(this.accumulatedThinking);
|
|
1599
|
-
this.isInThinking = false;
|
|
1600
|
-
this.isInContent = true;
|
|
1601
|
-
}
|
|
1602
|
-
if (modelId) {
|
|
1603
|
-
this._extractThinkingFromContent(content, callbacks);
|
|
1604
|
-
} else {
|
|
1605
|
-
this.accumulatedContent += content;
|
|
1606
|
-
}
|
|
1607
|
-
callbacks.onContent(this.accumulatedContent);
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* Handle thinking/reasoning content
|
|
1611
|
-
*/
|
|
1612
|
-
_handleThinking(reasoning, callbacks) {
|
|
1613
|
-
if (!this.isInThinking) {
|
|
1614
|
-
this.accumulatedThinking += "<thinking> ";
|
|
1615
|
-
this.isInThinking = true;
|
|
1616
|
-
}
|
|
1617
|
-
this.accumulatedThinking += reasoning;
|
|
1618
|
-
callbacks.onThinking(this.accumulatedThinking);
|
|
1619
|
-
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Extract thinking blocks from content (for models with inline thinking)
|
|
1622
|
-
*/
|
|
1623
|
-
_extractThinkingFromContent(content, callbacks) {
|
|
1624
|
-
const parts = content.split(/(<thinking>|<\/thinking>)/);
|
|
1625
|
-
for (const part of parts) {
|
|
1626
|
-
if (part === "<thinking>") {
|
|
1627
|
-
this.isInThinking = true;
|
|
1628
|
-
if (!this.accumulatedThinking.includes("<thinking>")) {
|
|
1629
|
-
this.accumulatedThinking += "<thinking> ";
|
|
1630
|
-
}
|
|
1631
|
-
} else if (part === "</thinking>") {
|
|
1632
|
-
this.isInThinking = false;
|
|
1633
|
-
this.accumulatedThinking += "</thinking>";
|
|
1634
|
-
} else if (this.isInThinking) {
|
|
1635
|
-
this.accumulatedThinking += part;
|
|
1636
|
-
} else {
|
|
1637
|
-
this.accumulatedContent += part;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Merge images into accumulated array, avoiding duplicates
|
|
1643
|
-
*/
|
|
1644
|
-
_mergeImages(newImages) {
|
|
1645
|
-
for (const img of newImages) {
|
|
1646
|
-
const newUrl = img.image_url?.url;
|
|
1647
|
-
const existingIndex = this.accumulatedImages.findIndex((existing) => {
|
|
1648
|
-
const existingUrl = existing.image_url?.url;
|
|
1649
|
-
if (newUrl && existingUrl) {
|
|
1650
|
-
return existingUrl === newUrl;
|
|
1651
|
-
}
|
|
1652
|
-
if (img.index !== void 0 && existing.index !== void 0) {
|
|
1653
|
-
return existing.index === img.index;
|
|
1654
|
-
}
|
|
1655
|
-
return false;
|
|
1656
|
-
});
|
|
1657
|
-
if (existingIndex === -1) {
|
|
1658
|
-
this.accumulatedImages.push(img);
|
|
1659
|
-
} else {
|
|
1660
|
-
this.accumulatedImages[existingIndex] = img;
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
|
|
1666
|
-
// utils/torUtils.ts
|
|
1667
|
-
var TOR_ONION_SUFFIX = ".onion";
|
|
1668
|
-
var isTorContext = () => {
|
|
1669
|
-
if (typeof window === "undefined") return false;
|
|
1670
|
-
const hostname = window.location.hostname.toLowerCase();
|
|
1671
|
-
return hostname.endsWith(TOR_ONION_SUFFIX);
|
|
1672
|
-
};
|
|
1673
|
-
var isOnionUrl = (url) => {
|
|
1674
|
-
if (!url) return false;
|
|
1675
|
-
const trimmed = url.trim().toLowerCase();
|
|
1676
|
-
if (!trimmed) return false;
|
|
1677
|
-
try {
|
|
1678
|
-
const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
|
|
1679
|
-
return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
|
|
1680
|
-
} catch {
|
|
1681
|
-
return trimmed.includes(TOR_ONION_SUFFIX);
|
|
1682
|
-
}
|
|
1683
|
-
};
|
|
1684
|
-
|
|
1685
|
-
// client/ProviderManager.ts
|
|
1686
|
-
function getImageResolutionFromDataUrl(dataUrl) {
|
|
1687
|
-
try {
|
|
1688
|
-
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
|
|
1689
|
-
return null;
|
|
1690
|
-
const commaIdx = dataUrl.indexOf(",");
|
|
1691
|
-
if (commaIdx === -1) return null;
|
|
1692
|
-
const meta = dataUrl.slice(5, commaIdx);
|
|
1693
|
-
const base64 = dataUrl.slice(commaIdx + 1);
|
|
1694
|
-
const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
|
|
1695
|
-
const len = binary.length;
|
|
1696
|
-
const bytes = new Uint8Array(len);
|
|
1697
|
-
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
|
1698
|
-
const isPNG = meta.includes("image/png");
|
|
1699
|
-
const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
|
|
1700
|
-
if (isPNG) {
|
|
1701
|
-
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1702
|
-
for (let i = 0; i < sig.length; i++) {
|
|
1703
|
-
if (bytes[i] !== sig[i]) return null;
|
|
1704
|
-
}
|
|
1705
|
-
const view = new DataView(
|
|
1706
|
-
bytes.buffer,
|
|
1707
|
-
bytes.byteOffset,
|
|
1708
|
-
bytes.byteLength
|
|
1709
|
-
);
|
|
1710
|
-
const width = view.getUint32(16, false);
|
|
1711
|
-
const height = view.getUint32(20, false);
|
|
1712
|
-
if (width > 0 && height > 0) return { width, height };
|
|
1713
|
-
return null;
|
|
1714
|
-
}
|
|
1715
|
-
if (isJPEG) {
|
|
1716
|
-
let offset = 0;
|
|
1717
|
-
if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
|
|
1718
|
-
while (offset < bytes.length) {
|
|
1719
|
-
while (offset < bytes.length && bytes[offset] !== 255) offset++;
|
|
1720
|
-
if (offset + 1 >= bytes.length) break;
|
|
1721
|
-
while (bytes[offset] === 255) offset++;
|
|
1722
|
-
const marker = bytes[offset++];
|
|
1723
|
-
if (marker === 216 || marker === 217) continue;
|
|
1724
|
-
if (offset + 1 >= bytes.length) break;
|
|
1725
|
-
const length = bytes[offset] << 8 | bytes[offset + 1];
|
|
1726
|
-
offset += 2;
|
|
1727
|
-
if (marker === 192 || marker === 194) {
|
|
1728
|
-
if (length < 7 || offset + length - 2 > bytes.length) return null;
|
|
1729
|
-
const precision = bytes[offset];
|
|
1730
|
-
const height = bytes[offset + 1] << 8 | bytes[offset + 2];
|
|
1731
|
-
const width = bytes[offset + 3] << 8 | bytes[offset + 4];
|
|
1732
|
-
if (precision > 0 && width > 0 && height > 0)
|
|
1733
|
-
return { width, height };
|
|
1734
|
-
return null;
|
|
1735
|
-
} else {
|
|
1736
|
-
offset += length - 2;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
return null;
|
|
1740
|
-
}
|
|
1741
|
-
return null;
|
|
1742
|
-
} catch {
|
|
1743
|
-
return null;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
function calculateImageTokens(width, height, detail = "auto") {
|
|
1747
|
-
if (detail === "low") return 85;
|
|
1748
|
-
let w = width;
|
|
1749
|
-
let h = height;
|
|
1750
|
-
if (w > 2048 || h > 2048) {
|
|
1751
|
-
const aspectRatio = w / h;
|
|
1752
|
-
if (w > h) {
|
|
1753
|
-
w = 2048;
|
|
1754
|
-
h = Math.floor(w / aspectRatio);
|
|
1755
|
-
} else {
|
|
1756
|
-
h = 2048;
|
|
1757
|
-
w = Math.floor(h * aspectRatio);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
if (w > 768 || h > 768) {
|
|
1761
|
-
const aspectRatio = w / h;
|
|
1762
|
-
if (w > h) {
|
|
1763
|
-
w = 768;
|
|
1764
|
-
h = Math.floor(w / aspectRatio);
|
|
1765
|
-
} else {
|
|
1766
|
-
h = 768;
|
|
1767
|
-
w = Math.floor(h * aspectRatio);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
const tilesWidth = Math.floor((w + 511) / 512);
|
|
1771
|
-
const tilesHeight = Math.floor((h + 511) / 512);
|
|
1772
|
-
const numTiles = tilesWidth * tilesHeight;
|
|
1773
|
-
return 85 + 170 * numTiles;
|
|
1774
|
-
}
|
|
1775
|
-
function isInsecureHttpUrl(url) {
|
|
1776
|
-
return url.startsWith("http://");
|
|
1777
|
-
}
|
|
1778
|
-
var ProviderManager = class _ProviderManager {
|
|
1779
|
-
constructor(providerRegistry, store, logger) {
|
|
1780
|
-
this.providerRegistry = providerRegistry;
|
|
1781
|
-
this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1782
|
-
this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
|
|
1783
|
-
if (store) {
|
|
1784
|
-
this.store = store;
|
|
1785
|
-
this.hydrateFromStore();
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
failedProviders = /* @__PURE__ */ new Set();
|
|
1789
|
-
/** Track when each provider last failed (provider URL -> timestamp) */
|
|
1790
|
-
lastFailed = /* @__PURE__ */ new Map();
|
|
1791
|
-
/** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
|
|
1792
|
-
providersOnCoolDown = [];
|
|
1793
|
-
/** Cooldown duration in milliseconds (42 seconds) */
|
|
1794
|
-
static COOLDOWN_DURATION_MS = 42 * 1e3;
|
|
1795
|
-
/** Optional persistent store for failure tracking */
|
|
1796
|
-
store = null;
|
|
1797
|
-
/** Instance ID for debugging */
|
|
1798
|
-
instanceId;
|
|
1799
|
-
logger;
|
|
1800
|
-
/**
|
|
1801
|
-
* Hydrate in-memory state from persistent store
|
|
1802
|
-
*/
|
|
1803
|
-
hydrateFromStore() {
|
|
1804
|
-
if (!this.store) return;
|
|
1805
|
-
const state = this.store.getState();
|
|
1806
|
-
this.failedProviders = new Set(state.failedProviders);
|
|
1807
|
-
this.lastFailed = new Map(Object.entries(state.lastFailed));
|
|
1808
|
-
const now = Date.now();
|
|
1809
|
-
this.providersOnCoolDown = state.providersOnCooldown.filter(
|
|
1810
|
-
(entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
|
|
1811
|
-
).map((entry) => [entry.baseUrl, entry.timestamp]);
|
|
1812
|
-
this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Get instance ID for debugging
|
|
1816
|
-
*/
|
|
1817
|
-
getInstanceId() {
|
|
1818
|
-
return this.instanceId;
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Clean up expired cooldown entries
|
|
1822
|
-
* Also removes the provider from failedProviders so it can be retried
|
|
1823
|
-
*/
|
|
1824
|
-
cleanupExpiredCooldowns() {
|
|
1825
|
-
const now = Date.now();
|
|
1826
|
-
const before = this.providersOnCoolDown.length;
|
|
1827
|
-
this.providersOnCoolDown = this.providersOnCoolDown.filter(
|
|
1828
|
-
([url, timestamp]) => {
|
|
1829
|
-
const age = now - timestamp;
|
|
1830
|
-
const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
|
|
1831
|
-
if (isExpired) {
|
|
1832
|
-
this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
|
|
1833
|
-
this.failedProviders.delete(url);
|
|
1834
|
-
if (this.store) {
|
|
1835
|
-
this.store.getState().removeFailedProvider(url);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
return !isExpired;
|
|
1839
|
-
}
|
|
1840
|
-
);
|
|
1841
|
-
const after = this.providersOnCoolDown.length;
|
|
1842
|
-
if (before !== after) {
|
|
1843
|
-
this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* Get the cooldown duration in milliseconds
|
|
1848
|
-
*/
|
|
1849
|
-
getCooldownDurationMs() {
|
|
1850
|
-
return _ProviderManager.COOLDOWN_DURATION_MS;
|
|
1851
|
-
}
|
|
1852
|
-
/**
|
|
1853
|
-
* Check if a provider is currently on cooldown
|
|
1854
|
-
*/
|
|
1855
|
-
isOnCooldown(baseUrl) {
|
|
1856
|
-
this.cleanupExpiredCooldowns();
|
|
1857
|
-
const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
|
|
1858
|
-
return result;
|
|
1859
|
-
}
|
|
1860
|
-
/**
|
|
1861
|
-
* Get all providers currently on cooldown
|
|
1862
|
-
*/
|
|
1863
|
-
getProvidersOnCooldown() {
|
|
1864
|
-
this.cleanupExpiredCooldowns();
|
|
1865
|
-
return [...this.providersOnCoolDown];
|
|
1866
|
-
}
|
|
1867
|
-
/**
|
|
1868
|
-
* Reset the failed providers list
|
|
1869
|
-
*/
|
|
1870
|
-
resetFailedProviders() {
|
|
1871
|
-
this.failedProviders.clear();
|
|
1872
|
-
if (this.store) {
|
|
1873
|
-
this.store.getState().setFailedProviders([]);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
/**
|
|
1877
|
-
* Get the last failed timestamp for a provider
|
|
1878
|
-
*/
|
|
1879
|
-
getLastFailed(baseUrl) {
|
|
1880
|
-
return this.lastFailed.get(baseUrl);
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Get all providers with their last failed timestamps
|
|
1884
|
-
*/
|
|
1885
|
-
getAllLastFailed() {
|
|
1886
|
-
return new Map(this.lastFailed);
|
|
1887
|
-
}
|
|
1888
|
-
/**
|
|
1889
|
-
* Mark a provider as failed
|
|
1890
|
-
* If a provider fails twice within 5 minutes, it's added to cooldown
|
|
1891
|
-
*/
|
|
1892
|
-
markFailed(baseUrl) {
|
|
1893
|
-
const now = Date.now();
|
|
1894
|
-
const lastFailure = this.lastFailed.get(baseUrl);
|
|
1895
|
-
this.logger.log(`markFailed: ${baseUrl} lastFailure=${lastFailure} now=${now}`);
|
|
1896
|
-
if (lastFailure !== void 0) {
|
|
1897
|
-
const timeSinceLastFailure = now - lastFailure;
|
|
1898
|
-
this.logger.log(`markFailed: timeSinceLastFailure=${timeSinceLastFailure}ms withinCooldown=${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
|
|
1899
|
-
}
|
|
1900
|
-
this.lastFailed.set(baseUrl, now);
|
|
1901
|
-
this.failedProviders.add(baseUrl);
|
|
1902
|
-
if (this.store) {
|
|
1903
|
-
this.store.getState().setLastFailedTimestamp(baseUrl, now);
|
|
1904
|
-
this.store.getState().addFailedProvider(baseUrl);
|
|
1905
|
-
}
|
|
1906
|
-
this.logger.log(`markFailed: updated ${baseUrl} to ${now}, failedProviders=${this.failedProviders.size}`);
|
|
1907
|
-
if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
|
|
1908
|
-
this.logger.log(`markFailed: second failure within cooldown window for ${baseUrl}`);
|
|
1909
|
-
if (!this.isOnCooldown(baseUrl)) {
|
|
1910
|
-
this.providersOnCoolDown.push([baseUrl, now]);
|
|
1911
|
-
if (this.store) {
|
|
1912
|
-
this.store.getState().addProviderOnCooldown(baseUrl, now);
|
|
1913
|
-
}
|
|
1914
|
-
this.logger.log(`markFailed: ${baseUrl} added to cooldown`);
|
|
1915
|
-
} else {
|
|
1916
|
-
this.logger.log(`markFailed: ${baseUrl} already on cooldown`);
|
|
1917
|
-
}
|
|
1918
|
-
} else {
|
|
1919
|
-
if (lastFailure === void 0) {
|
|
1920
|
-
this.logger.log(`markFailed: first failure for ${baseUrl}`);
|
|
1921
|
-
} else {
|
|
1922
|
-
this.logger.log(`markFailed: failure outside cooldown window for ${baseUrl} (${now - lastFailure}ms ago)`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* Remove a provider from cooldown (e.g., after successful request)
|
|
1928
|
-
*/
|
|
1929
|
-
removeFromCooldown(baseUrl) {
|
|
1930
|
-
this.providersOnCoolDown = this.providersOnCoolDown.filter(
|
|
1931
|
-
([url]) => url !== baseUrl
|
|
1932
|
-
);
|
|
1933
|
-
if (this.store) {
|
|
1934
|
-
this.store.getState().removeProviderFromCooldown(baseUrl);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
/**
|
|
1938
|
-
* Clear all cooldown tracking
|
|
1939
|
-
*/
|
|
1940
|
-
clearCooldowns() {
|
|
1941
|
-
this.providersOnCoolDown = [];
|
|
1942
|
-
if (this.store) {
|
|
1943
|
-
this.store.getState().clearProvidersOnCooldown();
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Clear all failure tracking (lastFailed timestamps)
|
|
1948
|
-
*/
|
|
1949
|
-
clearFailureHistory() {
|
|
1950
|
-
this.lastFailed.clear();
|
|
1951
|
-
if (this.store) {
|
|
1952
|
-
this.store.getState().setLastFailed({});
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
/**
|
|
1956
|
-
* Check if a provider has failed
|
|
1957
|
-
*/
|
|
1958
|
-
hasFailed(baseUrl) {
|
|
1959
|
-
return this.failedProviders.has(baseUrl);
|
|
1960
|
-
}
|
|
1961
|
-
/**
|
|
1962
|
-
* Get a copy of the failed providers set
|
|
1963
|
-
*/
|
|
1964
|
-
getFailedProviders() {
|
|
1965
|
-
return new Set(this.failedProviders);
|
|
1966
|
-
}
|
|
1967
|
-
/**
|
|
1968
|
-
* Find the next best provider for a model
|
|
1969
|
-
* @param modelId The model ID to find a provider for
|
|
1970
|
-
* @param currentBaseUrl The current provider to exclude
|
|
1971
|
-
* @returns The best provider URL or null if none available
|
|
1972
|
-
*/
|
|
1973
|
-
findNextBestProvider(modelId, currentBaseUrl) {
|
|
1974
|
-
try {
|
|
1975
|
-
const torMode = isTorContext();
|
|
1976
|
-
const disabledProviders = new Set(
|
|
1977
|
-
this.providerRegistry.getDisabledProviders()
|
|
1978
|
-
);
|
|
1979
|
-
this.logger.log(`findNextBestProvider: model=${modelId} disabled=${[...disabledProviders].length} onCooldown=${this.providersOnCoolDown.length}`);
|
|
1980
|
-
const allProviders = this.providerRegistry.getAllProvidersModels();
|
|
1981
|
-
this.logger.log(`findNextBestProvider: total providers=${Object.keys(allProviders).length}`);
|
|
1982
|
-
const candidates = [];
|
|
1983
|
-
for (const [baseUrl, models] of Object.entries(allProviders)) {
|
|
1984
|
-
if (baseUrl === currentBaseUrl) {
|
|
1985
|
-
continue;
|
|
1986
|
-
}
|
|
1987
|
-
if (disabledProviders.has(baseUrl)) {
|
|
1988
|
-
continue;
|
|
1989
|
-
}
|
|
1990
|
-
if (this.isOnCooldown(baseUrl)) {
|
|
1991
|
-
continue;
|
|
1992
|
-
}
|
|
1993
|
-
if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
|
|
1994
|
-
continue;
|
|
1995
|
-
}
|
|
1996
|
-
const model = models.find((m) => m.id === modelId);
|
|
1997
|
-
if (!model) {
|
|
1998
|
-
continue;
|
|
1999
|
-
}
|
|
2000
|
-
const cost = model.sats_pricing?.completion ?? 0;
|
|
2001
|
-
candidates.push({ baseUrl, model, cost });
|
|
2002
|
-
}
|
|
2003
|
-
candidates.sort((a, b) => a.cost - b.cost);
|
|
2004
|
-
if (candidates.length > 0) {
|
|
2005
|
-
return candidates[0].baseUrl;
|
|
2006
|
-
} else {
|
|
2007
|
-
return null;
|
|
2008
|
-
}
|
|
2009
|
-
} catch (error) {
|
|
2010
|
-
this.logger.error("findNextBestProvider error:", error);
|
|
2011
|
-
return null;
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
/**
|
|
2015
|
-
* Find the best model for a provider
|
|
2016
|
-
* Useful when switching providers and need to find equivalent model
|
|
2017
|
-
*/
|
|
2018
|
-
async getModelForProvider(baseUrl, modelId) {
|
|
2019
|
-
const models = this.providerRegistry.getModelsForProvider(baseUrl);
|
|
2020
|
-
const exactMatch = models.find((m) => m.id === modelId);
|
|
2021
|
-
if (exactMatch) return exactMatch;
|
|
2022
|
-
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
2023
|
-
if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
|
|
2024
|
-
const suffix = modelId.split("/").pop();
|
|
2025
|
-
const suffixMatch = models.find((m) => m.id === suffix);
|
|
2026
|
-
if (suffixMatch) return suffixMatch;
|
|
2027
|
-
}
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2030
|
-
/**
|
|
2031
|
-
* Get all available providers for a model
|
|
2032
|
-
* Returns sorted list by price
|
|
2033
|
-
*/
|
|
2034
|
-
getAllProvidersForModel(modelId) {
|
|
2035
|
-
const candidates = [];
|
|
2036
|
-
const allProviders = this.providerRegistry.getAllProvidersModels();
|
|
2037
|
-
const disabledProviders = new Set(
|
|
2038
|
-
this.providerRegistry.getDisabledProviders()
|
|
2039
|
-
);
|
|
2040
|
-
const torMode = isTorContext();
|
|
2041
|
-
for (const [baseUrl, models] of Object.entries(allProviders)) {
|
|
2042
|
-
if (disabledProviders.has(baseUrl)) continue;
|
|
2043
|
-
if (this.isOnCooldown(baseUrl)) continue;
|
|
2044
|
-
if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
|
|
2045
|
-
continue;
|
|
2046
|
-
const model = models.find((m) => m.id === modelId);
|
|
2047
|
-
if (!model) continue;
|
|
2048
|
-
const cost = model.sats_pricing?.completion ?? 0;
|
|
2049
|
-
candidates.push({ baseUrl, model, cost });
|
|
2050
|
-
}
|
|
2051
|
-
return candidates.sort((a, b) => a.cost - b.cost);
|
|
2052
|
-
}
|
|
2053
|
-
/**
|
|
2054
|
-
* Get providers for a model sorted by prompt+completion pricing
|
|
2055
|
-
*/
|
|
2056
|
-
getProviderPriceRankingForModel(modelId, options = {}) {
|
|
2057
|
-
const includeDisabled = options.includeDisabled ?? false;
|
|
2058
|
-
const torMode = options.torMode ?? false;
|
|
2059
|
-
const disabledProviders = new Set(
|
|
2060
|
-
this.providerRegistry.getDisabledProviders()
|
|
2061
|
-
);
|
|
2062
|
-
const allModels = this.providerRegistry.getAllProvidersModels();
|
|
2063
|
-
const results = [];
|
|
2064
|
-
for (const [baseUrl, models] of Object.entries(allModels)) {
|
|
2065
|
-
if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
|
|
2066
|
-
if (this.isOnCooldown(baseUrl)) continue;
|
|
2067
|
-
if (torMode && !baseUrl.includes(".onion")) continue;
|
|
2068
|
-
if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
|
|
2069
|
-
continue;
|
|
2070
|
-
const match = models.find((model) => model.id === modelId);
|
|
2071
|
-
if (!match?.sats_pricing) continue;
|
|
2072
|
-
const prompt = match.sats_pricing.prompt;
|
|
2073
|
-
const completion = match.sats_pricing.completion;
|
|
2074
|
-
if (typeof prompt !== "number" || typeof completion !== "number") {
|
|
2075
|
-
continue;
|
|
2076
|
-
}
|
|
2077
|
-
const promptPerMillion = prompt * 1e6;
|
|
2078
|
-
const completionPerMillion = completion * 1e6;
|
|
2079
|
-
const totalPerMillion = promptPerMillion + completionPerMillion;
|
|
2080
|
-
results.push({
|
|
2081
|
-
baseUrl,
|
|
2082
|
-
model: match,
|
|
2083
|
-
promptPerMillion,
|
|
2084
|
-
completionPerMillion,
|
|
2085
|
-
totalPerMillion
|
|
2086
|
-
});
|
|
2087
|
-
}
|
|
2088
|
-
return results.sort((a, b) => {
|
|
2089
|
-
if (a.totalPerMillion !== b.totalPerMillion) {
|
|
2090
|
-
return a.totalPerMillion - b.totalPerMillion;
|
|
2091
|
-
}
|
|
2092
|
-
return a.baseUrl.localeCompare(b.baseUrl);
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
/**
|
|
2096
|
-
* Get best-priced provider for a specific model
|
|
2097
|
-
*/
|
|
2098
|
-
getBestProviderForModel(modelId, options = {}) {
|
|
2099
|
-
const ranking = this.getProviderPriceRankingForModel(modelId, options);
|
|
2100
|
-
return ranking[0]?.baseUrl ?? null;
|
|
2101
|
-
}
|
|
2102
|
-
normalizeModelId(modelId) {
|
|
2103
|
-
return modelId.includes("/") ? modelId.split("/").pop() || modelId : modelId;
|
|
2104
|
-
}
|
|
2105
|
-
/**
|
|
2106
|
-
* Check if a provider accepts a specific mint
|
|
2107
|
-
*/
|
|
2108
|
-
providerAcceptsMint(baseUrl, mintUrl) {
|
|
2109
|
-
const providerMints = this.providerRegistry.getProviderMints(baseUrl);
|
|
2110
|
-
if (providerMints.length === 0) {
|
|
2111
|
-
return true;
|
|
2112
|
-
}
|
|
2113
|
-
return providerMints.includes(mintUrl);
|
|
2114
|
-
}
|
|
2115
|
-
/**
|
|
2116
|
-
* Get required sats for a model based on message history
|
|
2117
|
-
* Simple estimation based on typical usage
|
|
2118
|
-
*/
|
|
2119
|
-
getRequiredSatsForModel(model, apiMessages, maxTokens) {
|
|
2120
|
-
try {
|
|
2121
|
-
let imageTokens = 0;
|
|
2122
|
-
if (apiMessages) {
|
|
2123
|
-
for (const msg of apiMessages) {
|
|
2124
|
-
const content = msg?.content;
|
|
2125
|
-
if (Array.isArray(content)) {
|
|
2126
|
-
for (const part of content) {
|
|
2127
|
-
const isImage = part && typeof part === "object" && part.type === "image_url";
|
|
2128
|
-
const url = isImage ? typeof part.image_url === "string" ? part.image_url : part.image_url?.url : void 0;
|
|
2129
|
-
if (url && typeof url === "string" && url.startsWith("data:")) {
|
|
2130
|
-
const res = getImageResolutionFromDataUrl(url);
|
|
2131
|
-
if (res) {
|
|
2132
|
-
const tokensFromImage = calculateImageTokens(
|
|
2133
|
-
res.width,
|
|
2134
|
-
res.height
|
|
2135
|
-
);
|
|
2136
|
-
imageTokens += tokensFromImage;
|
|
2137
|
-
this.logger.log(`IMAGE INPUT RESOLUTION width=${res.width} height=${res.height} tokens=${tokensFromImage}`);
|
|
2138
|
-
} else {
|
|
2139
|
-
this.logger.log("IMAGE INPUT RESOLUTION: unknown format");
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
const apiMessagesNoImages = apiMessages ? apiMessages.map((m) => {
|
|
2147
|
-
if (Array.isArray(m?.content)) {
|
|
2148
|
-
const filtered = m.content.filter(
|
|
2149
|
-
(p) => !(p && typeof p === "object" && p.type === "image_url")
|
|
2150
|
-
);
|
|
2151
|
-
return { ...m, content: filtered };
|
|
2152
|
-
}
|
|
2153
|
-
return m;
|
|
2154
|
-
}) : void 0;
|
|
2155
|
-
const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
|
|
2156
|
-
const totalInputTokens = approximateTokens + imageTokens;
|
|
2157
|
-
const sp = model?.sats_pricing;
|
|
2158
|
-
if (!sp) {
|
|
2159
|
-
return 0;
|
|
2160
|
-
}
|
|
2161
|
-
if (!sp.max_completion_cost) {
|
|
2162
|
-
return sp.max_cost ?? 50;
|
|
2163
|
-
}
|
|
2164
|
-
const promptCosts = (sp.prompt || 0) * totalInputTokens;
|
|
2165
|
-
let completionCost = sp.max_completion_cost;
|
|
2166
|
-
if (maxTokens !== void 0 && sp.completion) {
|
|
2167
|
-
completionCost = sp.completion * maxTokens;
|
|
2168
|
-
}
|
|
2169
|
-
const totalEstimatedCosts = (promptCosts + completionCost) * 1.05;
|
|
2170
|
-
return totalEstimatedCosts;
|
|
2171
|
-
} catch (e) {
|
|
2172
|
-
this.logger.error("getRequiredSatsForModel error:", e);
|
|
2173
|
-
return 0;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
};
|
|
2177
|
-
|
|
2178
|
-
// storage/drivers/localStorage.ts
|
|
2179
|
-
var canUseLocalStorage = () => {
|
|
2180
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
2181
|
-
};
|
|
2182
|
-
var isQuotaExceeded = (error) => {
|
|
2183
|
-
const e = error;
|
|
2184
|
-
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
2185
|
-
};
|
|
2186
|
-
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
2187
|
-
var localStorageDriver = {
|
|
2188
|
-
async getItem(key, defaultValue) {
|
|
2189
|
-
if (!canUseLocalStorage()) return defaultValue;
|
|
2190
|
-
try {
|
|
2191
|
-
const item = window.localStorage.getItem(key);
|
|
2192
|
-
if (item === null) return defaultValue;
|
|
2193
|
-
try {
|
|
2194
|
-
return JSON.parse(item);
|
|
2195
|
-
} catch (parseError) {
|
|
2196
|
-
if (typeof defaultValue === "string") {
|
|
2197
|
-
return item;
|
|
2198
|
-
}
|
|
2199
|
-
throw parseError;
|
|
2200
|
-
}
|
|
2201
|
-
} catch (error) {
|
|
2202
|
-
console.error(`Error retrieving item with key "${key}":`, error);
|
|
2203
|
-
if (canUseLocalStorage()) {
|
|
2204
|
-
try {
|
|
2205
|
-
window.localStorage.removeItem(key);
|
|
2206
|
-
} catch (removeError) {
|
|
2207
|
-
console.error(
|
|
2208
|
-
`Error removing corrupted item with key "${key}":`,
|
|
2209
|
-
removeError
|
|
2210
|
-
);
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
return defaultValue;
|
|
2214
|
-
}
|
|
2215
|
-
},
|
|
2216
|
-
async setItem(key, value) {
|
|
2217
|
-
if (!canUseLocalStorage()) return;
|
|
2218
|
-
try {
|
|
2219
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2220
|
-
} catch (error) {
|
|
2221
|
-
if (isQuotaExceeded(error)) {
|
|
2222
|
-
if (NON_CRITICAL_KEYS.has(key)) {
|
|
2223
|
-
console.warn(
|
|
2224
|
-
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
2225
|
-
);
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
try {
|
|
2229
|
-
window.localStorage.removeItem("modelsFromAllProviders");
|
|
2230
|
-
} catch {
|
|
2231
|
-
}
|
|
2232
|
-
try {
|
|
2233
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2234
|
-
return;
|
|
2235
|
-
} catch (retryError) {
|
|
2236
|
-
console.warn(
|
|
2237
|
-
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
2238
|
-
retryError
|
|
2239
|
-
);
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
console.error(`Error storing item with key "${key}":`, error);
|
|
2244
|
-
}
|
|
2245
|
-
},
|
|
2246
|
-
async removeItem(key) {
|
|
2247
|
-
if (!canUseLocalStorage()) return;
|
|
2248
|
-
try {
|
|
2249
|
-
window.localStorage.removeItem(key);
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
console.error(`Error removing item with key "${key}":`, error);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
|
|
2256
|
-
// storage/drivers/memory.ts
|
|
2257
|
-
var createMemoryDriver = (seed) => {
|
|
2258
|
-
const store = /* @__PURE__ */ new Map();
|
|
2259
|
-
return {
|
|
2260
|
-
async getItem(key, defaultValue) {
|
|
2261
|
-
const item = store.get(key);
|
|
2262
|
-
if (item === void 0) return defaultValue;
|
|
2263
|
-
try {
|
|
2264
|
-
return JSON.parse(item);
|
|
2265
|
-
} catch (parseError) {
|
|
2266
|
-
if (typeof defaultValue === "string") {
|
|
2267
|
-
return item;
|
|
2268
|
-
}
|
|
2269
|
-
throw parseError;
|
|
2270
|
-
}
|
|
2271
|
-
},
|
|
2272
|
-
async setItem(key, value) {
|
|
2273
|
-
store.set(key, JSON.stringify(value));
|
|
2274
|
-
},
|
|
2275
|
-
async removeItem(key) {
|
|
2276
|
-
store.delete(key);
|
|
2277
|
-
}
|
|
2278
|
-
};
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
// storage/drivers/sqlite.ts
|
|
2282
|
-
var isBun = () => {
|
|
2283
|
-
return typeof process.versions.bun !== "undefined";
|
|
2284
|
-
};
|
|
2285
|
-
var cachedDbModule = null;
|
|
2286
|
-
var loadDatabase = async (dbPath) => {
|
|
2287
|
-
if (isBun()) {
|
|
2288
|
-
throw new Error(
|
|
2289
|
-
"SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
try {
|
|
2293
|
-
if (!cachedDbModule) {
|
|
2294
|
-
cachedDbModule = (await import('better-sqlite3')).default;
|
|
2295
|
-
}
|
|
2296
|
-
return new cachedDbModule(dbPath);
|
|
2297
|
-
} catch (error) {
|
|
2298
|
-
throw new Error(
|
|
2299
|
-
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
2300
|
-
);
|
|
2301
|
-
}
|
|
2302
|
-
};
|
|
2303
|
-
var createSqliteDriver = (options = {}) => {
|
|
2304
|
-
const dbPath = options.dbPath || "routstr.sqlite";
|
|
2305
|
-
const tableName = options.tableName || "sdk_storage";
|
|
2306
|
-
let db;
|
|
2307
|
-
let selectStmt;
|
|
2308
|
-
let upsertStmt;
|
|
2309
|
-
let deleteStmt;
|
|
2310
|
-
const initDb = async () => {
|
|
2311
|
-
if (!db) {
|
|
2312
|
-
db = await loadDatabase(dbPath);
|
|
2313
|
-
db.exec(
|
|
2314
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
2315
|
-
);
|
|
2316
|
-
selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
2317
|
-
upsertStmt = db.prepare(
|
|
2318
|
-
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
2319
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
2320
|
-
);
|
|
2321
|
-
deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
2322
|
-
}
|
|
2323
|
-
};
|
|
2324
|
-
const ensureInit = async () => {
|
|
2325
|
-
if (!db) {
|
|
2326
|
-
await initDb();
|
|
2327
|
-
}
|
|
2328
|
-
};
|
|
2329
|
-
return {
|
|
2330
|
-
async getItem(key, defaultValue) {
|
|
2331
|
-
try {
|
|
2332
|
-
await ensureInit();
|
|
2333
|
-
const row = selectStmt.get(key);
|
|
2334
|
-
if (!row || typeof row.value !== "string") return defaultValue;
|
|
2335
|
-
try {
|
|
2336
|
-
return JSON.parse(row.value);
|
|
2337
|
-
} catch (parseError) {
|
|
2338
|
-
if (typeof defaultValue === "string") {
|
|
2339
|
-
return row.value;
|
|
2340
|
-
}
|
|
2341
|
-
throw parseError;
|
|
2342
|
-
}
|
|
2343
|
-
} catch (error) {
|
|
2344
|
-
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
2345
|
-
return defaultValue;
|
|
2346
|
-
}
|
|
2347
|
-
},
|
|
2348
|
-
async setItem(key, value) {
|
|
2349
|
-
try {
|
|
2350
|
-
await ensureInit();
|
|
2351
|
-
upsertStmt.run(key, JSON.stringify(value));
|
|
2352
|
-
} catch (error) {
|
|
2353
|
-
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
2354
|
-
}
|
|
2355
|
-
},
|
|
2356
|
-
async removeItem(key) {
|
|
2357
|
-
try {
|
|
2358
|
-
await ensureInit();
|
|
2359
|
-
deleteStmt.run(key);
|
|
2360
|
-
} catch (error) {
|
|
2361
|
-
console.error(`SQLite removeItem failed for key "${key}":`, error);
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
};
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2367
|
-
// storage/keys.ts
|
|
2368
|
-
var SDK_STORAGE_KEYS = {
|
|
2369
|
-
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
2370
|
-
LAST_USED_MODEL: "lastUsedModel",
|
|
2371
|
-
BASE_URLS_LIST: "base_urls_list",
|
|
2372
|
-
DISABLED_PROVIDERS: "disabled_providers",
|
|
2373
|
-
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
2374
|
-
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
2375
|
-
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
2376
|
-
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
2377
|
-
API_KEYS: "api_keys",
|
|
2378
|
-
CHILD_KEYS: "child_keys",
|
|
2379
|
-
XCASHU_TOKENS: "xcashu_tokens",
|
|
2380
|
-
ROUTSTR21_MODELS: "routstr21Models",
|
|
2381
|
-
LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
|
|
2382
|
-
CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
|
|
2383
|
-
USAGE_TRACKING: "usage_tracking",
|
|
2384
|
-
CLIENT_IDS: "client_ids",
|
|
2385
|
-
FAILED_PROVIDERS: "failed_providers",
|
|
2386
|
-
LAST_FAILED: "last_failed",
|
|
2387
|
-
PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
|
|
2388
|
-
};
|
|
2389
|
-
|
|
2390
|
-
// storage/usageTracking/indexedDB.ts
|
|
2391
|
-
var DEFAULT_DB_NAME = "routstr-sdk";
|
|
2392
|
-
var DEFAULT_STORE_NAME = "usage_tracking";
|
|
2393
|
-
var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
|
|
2394
|
-
var isBrowser = typeof indexedDB !== "undefined";
|
|
2395
|
-
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2396
|
-
var openDatabase = (dbName, storeName) => {
|
|
2397
|
-
if (!isBrowser) {
|
|
2398
|
-
return Promise.reject(new Error("IndexedDB is not available"));
|
|
2399
|
-
}
|
|
2400
|
-
return new Promise((resolve, reject) => {
|
|
2401
|
-
const request = indexedDB.open(dbName, 1);
|
|
2402
|
-
request.onupgradeneeded = () => {
|
|
2403
|
-
const db = request.result;
|
|
2404
|
-
if (!db.objectStoreNames.contains(storeName)) {
|
|
2405
|
-
const store = db.createObjectStore(storeName, { keyPath: "id" });
|
|
2406
|
-
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
2407
|
-
store.createIndex("modelId", "modelId", { unique: false });
|
|
2408
|
-
store.createIndex("baseUrl", "baseUrl", { unique: false });
|
|
2409
|
-
store.createIndex("sessionId", "sessionId", { unique: false });
|
|
2410
|
-
store.createIndex("client", "client", { unique: false });
|
|
2411
|
-
}
|
|
2412
|
-
};
|
|
2413
|
-
request.onsuccess = () => resolve(request.result);
|
|
2414
|
-
request.onerror = () => reject(request.error);
|
|
2415
|
-
});
|
|
2416
|
-
};
|
|
2417
|
-
var matchesFilters = (entry, options = {}) => {
|
|
2418
|
-
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
2419
|
-
return false;
|
|
2420
|
-
}
|
|
2421
|
-
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
2422
|
-
return false;
|
|
2423
|
-
}
|
|
2424
|
-
if (options.modelId && entry.modelId !== options.modelId) {
|
|
2425
|
-
return false;
|
|
2426
|
-
}
|
|
2427
|
-
if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
|
|
2428
|
-
return false;
|
|
2429
|
-
}
|
|
2430
|
-
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
2431
|
-
return false;
|
|
2432
|
-
}
|
|
2433
|
-
if (options.client && entry.client !== options.client) {
|
|
2434
|
-
return false;
|
|
2435
|
-
}
|
|
2436
|
-
return true;
|
|
2437
|
-
};
|
|
2438
|
-
var createIndexedDBUsageTrackingDriver = (options = {}) => {
|
|
2439
|
-
const dbName = options.dbName || DEFAULT_DB_NAME;
|
|
2440
|
-
const storeName = options.storeName || DEFAULT_STORE_NAME;
|
|
2441
|
-
const legacyStorageDriver = options.legacyStorageDriver;
|
|
2442
|
-
let dbPromise = null;
|
|
2443
|
-
let migrationPromise = null;
|
|
2444
|
-
const getDb = () => {
|
|
2445
|
-
if (!dbPromise) {
|
|
2446
|
-
dbPromise = openDatabase(dbName, storeName);
|
|
2447
|
-
}
|
|
2448
|
-
return dbPromise;
|
|
2449
|
-
};
|
|
2450
|
-
const putMany = async (entries) => {
|
|
2451
|
-
if (entries.length === 0) return;
|
|
2452
|
-
const db = await getDb();
|
|
2453
|
-
await new Promise((resolve, reject) => {
|
|
2454
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
2455
|
-
const store = tx.objectStore(storeName);
|
|
2456
|
-
for (const entry of entries) {
|
|
2457
|
-
store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
|
|
2458
|
-
}
|
|
2459
|
-
tx.oncomplete = () => resolve();
|
|
2460
|
-
tx.onerror = () => reject(tx.error);
|
|
2461
|
-
});
|
|
2462
|
-
};
|
|
2463
|
-
const ensureMigrated = async () => {
|
|
2464
|
-
if (!legacyStorageDriver) return;
|
|
2465
|
-
if (!migrationPromise) {
|
|
2466
|
-
migrationPromise = (async () => {
|
|
2467
|
-
const migrated = await legacyStorageDriver.getItem(
|
|
2468
|
-
MIGRATION_MARKER_KEY,
|
|
2469
|
-
false
|
|
2470
|
-
);
|
|
2471
|
-
if (migrated) return;
|
|
2472
|
-
const legacyEntries = await legacyStorageDriver.getItem(
|
|
2473
|
-
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
2474
|
-
[]
|
|
2475
|
-
);
|
|
2476
|
-
if (legacyEntries.length > 0) {
|
|
2477
|
-
await putMany(legacyEntries);
|
|
2478
|
-
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
2479
|
-
}
|
|
2480
|
-
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
|
|
2481
|
-
})();
|
|
2482
|
-
}
|
|
2483
|
-
await migrationPromise;
|
|
2484
|
-
};
|
|
2485
|
-
return {
|
|
2486
|
-
async migrate() {
|
|
2487
|
-
await ensureMigrated();
|
|
2488
|
-
},
|
|
2489
|
-
async append(entry) {
|
|
2490
|
-
await ensureMigrated();
|
|
2491
|
-
await putMany([entry]);
|
|
2492
|
-
},
|
|
2493
|
-
async appendMany(entries) {
|
|
2494
|
-
await ensureMigrated();
|
|
2495
|
-
await putMany(entries);
|
|
2496
|
-
},
|
|
2497
|
-
async list(options2 = {}) {
|
|
2498
|
-
await ensureMigrated();
|
|
2499
|
-
const db = await getDb();
|
|
2500
|
-
return new Promise((resolve, reject) => {
|
|
2501
|
-
const tx = db.transaction(storeName, "readonly");
|
|
2502
|
-
const store = tx.objectStore(storeName);
|
|
2503
|
-
const index = store.index("timestamp");
|
|
2504
|
-
const direction = "prev";
|
|
2505
|
-
const request = index.openCursor(null, direction);
|
|
2506
|
-
const results = [];
|
|
2507
|
-
const limit = options2.limit;
|
|
2508
|
-
request.onsuccess = () => {
|
|
2509
|
-
const cursor = request.result;
|
|
2510
|
-
if (!cursor) {
|
|
2511
|
-
resolve(results);
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
const value = cursor.value;
|
|
2515
|
-
if (matchesFilters(value, options2)) {
|
|
2516
|
-
results.push(value);
|
|
2517
|
-
if (typeof limit === "number" && results.length >= limit) {
|
|
2518
|
-
resolve(results);
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
cursor.continue();
|
|
2523
|
-
};
|
|
2524
|
-
request.onerror = () => reject(request.error);
|
|
2525
|
-
});
|
|
2526
|
-
},
|
|
2527
|
-
async count(options2 = {}) {
|
|
2528
|
-
const results = await this.list(options2);
|
|
2529
|
-
return results.length;
|
|
2530
|
-
},
|
|
2531
|
-
async deleteOlderThan(timestamp) {
|
|
2532
|
-
await ensureMigrated();
|
|
2533
|
-
const db = await getDb();
|
|
2534
|
-
return new Promise((resolve, reject) => {
|
|
2535
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
2536
|
-
const store = tx.objectStore(storeName);
|
|
2537
|
-
const index = store.index("timestamp");
|
|
2538
|
-
const range = IDBKeyRange.upperBound(timestamp, true);
|
|
2539
|
-
const request = index.openCursor(range);
|
|
2540
|
-
let deleted = 0;
|
|
2541
|
-
request.onsuccess = () => {
|
|
2542
|
-
const cursor = request.result;
|
|
2543
|
-
if (!cursor) {
|
|
2544
|
-
resolve(deleted);
|
|
2545
|
-
return;
|
|
2546
|
-
}
|
|
2547
|
-
deleted += 1;
|
|
2548
|
-
cursor.delete();
|
|
2549
|
-
cursor.continue();
|
|
2550
|
-
};
|
|
2551
|
-
request.onerror = () => reject(request.error);
|
|
2552
|
-
});
|
|
2553
|
-
},
|
|
2554
|
-
async clear() {
|
|
2555
|
-
await ensureMigrated();
|
|
2556
|
-
const db = await getDb();
|
|
2557
|
-
await new Promise((resolve, reject) => {
|
|
2558
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
2559
|
-
tx.objectStore(storeName).clear();
|
|
2560
|
-
tx.oncomplete = () => resolve();
|
|
2561
|
-
tx.onerror = () => reject(tx.error);
|
|
2562
|
-
});
|
|
2563
|
-
}
|
|
2564
|
-
};
|
|
2565
|
-
};
|
|
2566
|
-
|
|
2567
|
-
// storage/usageTracking/sqlite.ts
|
|
2568
|
-
var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
|
|
2569
|
-
var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2570
|
-
var isBun2 = () => {
|
|
2571
|
-
return typeof process.versions.bun !== "undefined";
|
|
2572
|
-
};
|
|
2573
|
-
var cachedDbModule2 = null;
|
|
2574
|
-
var loadDatabase2 = async (dbPath) => {
|
|
2575
|
-
if (isBun2()) {
|
|
2576
|
-
throw new Error(
|
|
2577
|
-
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
2578
|
-
);
|
|
2579
|
-
}
|
|
2580
|
-
try {
|
|
2581
|
-
if (!cachedDbModule2) {
|
|
2582
|
-
cachedDbModule2 = (await import('better-sqlite3')).default;
|
|
2583
|
-
}
|
|
2584
|
-
return new cachedDbModule2(dbPath);
|
|
2585
|
-
} catch (error) {
|
|
2586
|
-
throw new Error(
|
|
2587
|
-
`better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
|
|
2588
|
-
);
|
|
2589
|
-
}
|
|
2590
|
-
};
|
|
2591
|
-
var buildWhereClause = (options = {}) => {
|
|
2592
|
-
const clauses = [];
|
|
2593
|
-
const params = [];
|
|
2594
|
-
if (typeof options.before === "number") {
|
|
2595
|
-
clauses.push("timestamp < ?");
|
|
2596
|
-
params.push(options.before);
|
|
2597
|
-
}
|
|
2598
|
-
if (typeof options.after === "number") {
|
|
2599
|
-
clauses.push("timestamp > ?");
|
|
2600
|
-
params.push(options.after);
|
|
2601
|
-
}
|
|
2602
|
-
if (options.modelId) {
|
|
2603
|
-
clauses.push("model_id = ?");
|
|
2604
|
-
params.push(options.modelId);
|
|
2605
|
-
}
|
|
2606
|
-
if (options.baseUrl) {
|
|
2607
|
-
clauses.push("base_url = ?");
|
|
2608
|
-
params.push(normalizeBaseUrl2(options.baseUrl));
|
|
2609
|
-
}
|
|
2610
|
-
if (options.sessionId) {
|
|
2611
|
-
clauses.push("session_id = ?");
|
|
2612
|
-
params.push(options.sessionId);
|
|
2613
|
-
}
|
|
2614
|
-
if (options.client) {
|
|
2615
|
-
clauses.push("client = ?");
|
|
2616
|
-
params.push(options.client);
|
|
2617
|
-
}
|
|
2618
|
-
return {
|
|
2619
|
-
sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
2620
|
-
params
|
|
2621
|
-
};
|
|
2622
|
-
};
|
|
2623
|
-
var createSqliteUsageTrackingDriver = (options = {}) => {
|
|
2624
|
-
const dbPath = options.dbPath || "routstr.sqlite";
|
|
2625
|
-
const tableName = options.tableName || "usage_tracking";
|
|
2626
|
-
const legacyStorageDriver = options.legacyStorageDriver;
|
|
2627
|
-
let db;
|
|
2628
|
-
let insertStmt;
|
|
2629
|
-
let migrationComplete = false;
|
|
2630
|
-
const initDb = async () => {
|
|
2631
|
-
if (!db) {
|
|
2632
|
-
db = await loadDatabase2(dbPath);
|
|
2633
|
-
db.exec(`
|
|
2634
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
2635
|
-
id TEXT PRIMARY KEY,
|
|
2636
|
-
timestamp INTEGER NOT NULL,
|
|
2637
|
-
model_id TEXT NOT NULL,
|
|
2638
|
-
base_url TEXT NOT NULL,
|
|
2639
|
-
request_id TEXT NOT NULL,
|
|
2640
|
-
cost REAL NOT NULL,
|
|
2641
|
-
sats_cost REAL NOT NULL,
|
|
2642
|
-
prompt_tokens INTEGER NOT NULL,
|
|
2643
|
-
completion_tokens INTEGER NOT NULL,
|
|
2644
|
-
total_tokens INTEGER NOT NULL,
|
|
2645
|
-
client TEXT,
|
|
2646
|
-
session_id TEXT,
|
|
2647
|
-
tags TEXT
|
|
2648
|
-
);
|
|
2649
|
-
CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
|
|
2650
|
-
CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
|
|
2651
|
-
CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
|
|
2652
|
-
CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
|
|
2653
|
-
CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
|
|
2654
|
-
`);
|
|
2655
|
-
insertStmt = db.prepare(`
|
|
2656
|
-
INSERT OR REPLACE INTO ${tableName} (
|
|
2657
|
-
id, timestamp, model_id, base_url, request_id,
|
|
2658
|
-
cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
|
|
2659
|
-
client, session_id, tags
|
|
2660
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2661
|
-
`);
|
|
2662
|
-
}
|
|
2663
|
-
};
|
|
2664
|
-
const ensureInit = async () => {
|
|
2665
|
-
if (!db) {
|
|
2666
|
-
await initDb();
|
|
2667
|
-
}
|
|
2668
|
-
};
|
|
2669
|
-
const appendOne = (entry) => {
|
|
2670
|
-
insertStmt.run(
|
|
2671
|
-
entry.id,
|
|
2672
|
-
entry.timestamp,
|
|
2673
|
-
entry.modelId,
|
|
2674
|
-
normalizeBaseUrl2(entry.baseUrl),
|
|
2675
|
-
entry.requestId,
|
|
2676
|
-
entry.cost,
|
|
2677
|
-
entry.satsCost,
|
|
2678
|
-
entry.promptTokens,
|
|
2679
|
-
entry.completionTokens,
|
|
2680
|
-
entry.totalTokens,
|
|
2681
|
-
entry.client ?? null,
|
|
2682
|
-
entry.sessionId ?? null,
|
|
2683
|
-
JSON.stringify(entry.tags ?? [])
|
|
2684
|
-
);
|
|
2685
|
-
};
|
|
2686
|
-
const ensureMigrated = async () => {
|
|
2687
|
-
if (!legacyStorageDriver || migrationComplete) return;
|
|
2688
|
-
const migrated = await legacyStorageDriver.getItem(
|
|
2689
|
-
MIGRATION_MARKER_KEY2,
|
|
2690
|
-
false
|
|
2691
|
-
);
|
|
2692
|
-
if (migrated) {
|
|
2693
|
-
migrationComplete = true;
|
|
2694
|
-
return;
|
|
2695
|
-
}
|
|
2696
|
-
const legacyEntries = await legacyStorageDriver.getItem(
|
|
2697
|
-
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
2698
|
-
[]
|
|
2699
|
-
);
|
|
2700
|
-
for (const entry of legacyEntries) {
|
|
2701
|
-
appendOne(entry);
|
|
2702
|
-
}
|
|
2703
|
-
if (legacyEntries.length > 0) {
|
|
2704
|
-
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
2705
|
-
}
|
|
2706
|
-
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
|
|
2707
|
-
migrationComplete = true;
|
|
2708
|
-
};
|
|
2709
|
-
const mapRow = (row) => ({
|
|
2710
|
-
id: row.id,
|
|
2711
|
-
timestamp: row.timestamp,
|
|
2712
|
-
modelId: row.model_id,
|
|
2713
|
-
baseUrl: row.base_url,
|
|
2714
|
-
requestId: row.request_id,
|
|
2715
|
-
cost: row.cost,
|
|
2716
|
-
satsCost: row.sats_cost,
|
|
2717
|
-
promptTokens: row.prompt_tokens,
|
|
2718
|
-
completionTokens: row.completion_tokens,
|
|
2719
|
-
totalTokens: row.total_tokens,
|
|
2720
|
-
client: row.client ?? void 0,
|
|
2721
|
-
sessionId: row.session_id ?? void 0,
|
|
2722
|
-
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
|
|
2723
|
-
});
|
|
2724
|
-
return {
|
|
2725
|
-
async migrate() {
|
|
2726
|
-
await ensureInit();
|
|
2727
|
-
await ensureMigrated();
|
|
2728
|
-
},
|
|
2729
|
-
async append(entry) {
|
|
2730
|
-
await ensureInit();
|
|
2731
|
-
await ensureMigrated();
|
|
2732
|
-
appendOne(entry);
|
|
2733
|
-
},
|
|
2734
|
-
async appendMany(entries) {
|
|
2735
|
-
await ensureInit();
|
|
2736
|
-
await ensureMigrated();
|
|
2737
|
-
for (const entry of entries) {
|
|
2738
|
-
appendOne(entry);
|
|
2739
|
-
}
|
|
2740
|
-
},
|
|
2741
|
-
async list(options2 = {}) {
|
|
2742
|
-
await ensureInit();
|
|
2743
|
-
await ensureMigrated();
|
|
2744
|
-
const { sql, params } = buildWhereClause(options2);
|
|
2745
|
-
const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
|
|
2746
|
-
const stmt = db.prepare(
|
|
2747
|
-
`SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
|
|
2748
|
-
);
|
|
2749
|
-
const rows = stmt.all(
|
|
2750
|
-
...typeof options2.limit === "number" ? [...params, options2.limit] : params
|
|
2751
|
-
);
|
|
2752
|
-
return rows.map(mapRow);
|
|
2753
|
-
},
|
|
2754
|
-
async count(options2 = {}) {
|
|
2755
|
-
await ensureInit();
|
|
2756
|
-
await ensureMigrated();
|
|
2757
|
-
const { sql, params } = buildWhereClause(options2);
|
|
2758
|
-
const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
|
|
2759
|
-
const row = stmt.get(...params);
|
|
2760
|
-
return Number(row?.count ?? 0);
|
|
2761
|
-
},
|
|
2762
|
-
async deleteOlderThan(timestamp) {
|
|
2763
|
-
await ensureInit();
|
|
2764
|
-
await ensureMigrated();
|
|
2765
|
-
const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
|
|
2766
|
-
const result = stmt.run(timestamp);
|
|
2767
|
-
return result.changes;
|
|
2768
|
-
},
|
|
2769
|
-
async clear() {
|
|
2770
|
-
await ensureInit();
|
|
2771
|
-
await ensureMigrated();
|
|
2772
|
-
db.prepare(`DELETE FROM ${tableName}`).run();
|
|
2773
|
-
}
|
|
2774
|
-
};
|
|
2775
|
-
};
|
|
2776
|
-
|
|
2777
|
-
// storage/usageTracking/bunSqlite.ts
|
|
2778
|
-
var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
|
|
2779
|
-
var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2780
|
-
var buildWhereClause2 = (options = {}) => {
|
|
2781
|
-
const clauses = [];
|
|
2782
|
-
const params = [];
|
|
2783
|
-
if (typeof options.before === "number") {
|
|
2784
|
-
clauses.push("timestamp < ?");
|
|
2785
|
-
params.push(options.before);
|
|
2786
|
-
}
|
|
2787
|
-
if (typeof options.after === "number") {
|
|
2788
|
-
clauses.push("timestamp > ?");
|
|
2789
|
-
params.push(options.after);
|
|
2790
|
-
}
|
|
2791
|
-
if (options.modelId) {
|
|
2792
|
-
clauses.push("model_id = ?");
|
|
2793
|
-
params.push(options.modelId);
|
|
2794
|
-
}
|
|
2795
|
-
if (options.baseUrl) {
|
|
2796
|
-
clauses.push("base_url = ?");
|
|
2797
|
-
params.push(normalizeBaseUrl3(options.baseUrl));
|
|
2798
|
-
}
|
|
2799
|
-
if (options.sessionId) {
|
|
2800
|
-
clauses.push("session_id = ?");
|
|
2801
|
-
params.push(options.sessionId);
|
|
2802
|
-
}
|
|
2803
|
-
if (options.client) {
|
|
2804
|
-
clauses.push("client = ?");
|
|
2805
|
-
params.push(options.client);
|
|
2806
|
-
}
|
|
2807
|
-
return {
|
|
2808
|
-
sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
2809
|
-
params
|
|
2810
|
-
};
|
|
2811
|
-
};
|
|
2812
|
-
var createBunSqliteUsageTrackingDriver = (options = {}) => {
|
|
2813
|
-
const dbPath = options.dbPath || "routstr.sqlite";
|
|
2814
|
-
const tableName = options.tableName || "usage_tracking";
|
|
2815
|
-
const legacyStorageDriver = options.legacyStorageDriver;
|
|
2816
|
-
const SQLiteDatabase = options.sqlite?.Database;
|
|
2817
|
-
let migrationPromise = null;
|
|
2818
|
-
if (!SQLiteDatabase) {
|
|
2819
|
-
throw new Error(
|
|
2820
|
-
"Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
|
|
2821
|
-
);
|
|
2822
|
-
}
|
|
2823
|
-
const db = new SQLiteDatabase(dbPath);
|
|
2824
|
-
db.run(`
|
|
2825
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
2826
|
-
id TEXT PRIMARY KEY,
|
|
2827
|
-
timestamp INTEGER NOT NULL,
|
|
2828
|
-
model_id TEXT NOT NULL,
|
|
2829
|
-
base_url TEXT NOT NULL,
|
|
2830
|
-
request_id TEXT NOT NULL,
|
|
2831
|
-
cost REAL NOT NULL,
|
|
2832
|
-
sats_cost REAL NOT NULL,
|
|
2833
|
-
prompt_tokens INTEGER NOT NULL,
|
|
2834
|
-
completion_tokens INTEGER NOT NULL,
|
|
2835
|
-
total_tokens INTEGER NOT NULL,
|
|
2836
|
-
client TEXT,
|
|
2837
|
-
session_id TEXT,
|
|
2838
|
-
tags TEXT
|
|
2839
|
-
)
|
|
2840
|
-
`);
|
|
2841
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
|
|
2842
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
|
|
2843
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
|
|
2844
|
-
const appendOne = (entry) => {
|
|
2845
|
-
db.query(`
|
|
2846
|
-
INSERT OR REPLACE INTO ${tableName} (
|
|
2847
|
-
id, timestamp, model_id, base_url, request_id,
|
|
2848
|
-
cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
|
|
2849
|
-
client, session_id, tags
|
|
2850
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2851
|
-
`).run(
|
|
2852
|
-
entry.id,
|
|
2853
|
-
entry.timestamp,
|
|
2854
|
-
entry.modelId,
|
|
2855
|
-
normalizeBaseUrl3(entry.baseUrl),
|
|
2856
|
-
entry.requestId,
|
|
2857
|
-
entry.cost,
|
|
2858
|
-
entry.satsCost,
|
|
2859
|
-
entry.promptTokens,
|
|
2860
|
-
entry.completionTokens,
|
|
2861
|
-
entry.totalTokens,
|
|
2862
|
-
entry.client ?? null,
|
|
2863
|
-
entry.sessionId ?? null,
|
|
2864
|
-
JSON.stringify(entry.tags ?? [])
|
|
2865
|
-
);
|
|
2866
|
-
};
|
|
2867
|
-
const mapRow = (row) => ({
|
|
2868
|
-
id: row.id,
|
|
2869
|
-
timestamp: row.timestamp,
|
|
2870
|
-
modelId: row.model_id,
|
|
2871
|
-
baseUrl: row.base_url,
|
|
2872
|
-
requestId: row.request_id,
|
|
2873
|
-
cost: row.cost,
|
|
2874
|
-
satsCost: row.sats_cost,
|
|
2875
|
-
promptTokens: row.prompt_tokens,
|
|
2876
|
-
completionTokens: row.completion_tokens,
|
|
2877
|
-
totalTokens: row.total_tokens,
|
|
2878
|
-
client: row.client ?? void 0,
|
|
2879
|
-
sessionId: row.session_id ?? void 0,
|
|
2880
|
-
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
|
|
2881
|
-
});
|
|
2882
|
-
const ensureMigrated = async () => {
|
|
2883
|
-
if (!legacyStorageDriver) return;
|
|
2884
|
-
if (!migrationPromise) {
|
|
2885
|
-
migrationPromise = (async () => {
|
|
2886
|
-
const migrated = await legacyStorageDriver.getItem(
|
|
2887
|
-
MIGRATION_MARKER_KEY3,
|
|
2888
|
-
false
|
|
2889
|
-
);
|
|
2890
|
-
if (migrated) return;
|
|
2891
|
-
const legacyEntries = await legacyStorageDriver.getItem(
|
|
2892
|
-
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
2893
|
-
[]
|
|
2894
|
-
);
|
|
2895
|
-
if (legacyEntries.length > 0) {
|
|
2896
|
-
for (const entry of legacyEntries) {
|
|
2897
|
-
appendOne(entry);
|
|
2898
|
-
}
|
|
2899
|
-
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
2900
|
-
}
|
|
2901
|
-
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
|
|
2902
|
-
})();
|
|
2903
|
-
}
|
|
2904
|
-
await migrationPromise;
|
|
2905
|
-
};
|
|
2906
|
-
return {
|
|
2907
|
-
async migrate() {
|
|
2908
|
-
await ensureMigrated();
|
|
2909
|
-
},
|
|
2910
|
-
async append(entry) {
|
|
2911
|
-
await ensureMigrated();
|
|
2912
|
-
appendOne(entry);
|
|
2913
|
-
},
|
|
2914
|
-
async appendMany(entries) {
|
|
2915
|
-
await ensureMigrated();
|
|
2916
|
-
for (const entry of entries) {
|
|
2917
|
-
appendOne(entry);
|
|
2918
|
-
}
|
|
2919
|
-
},
|
|
2920
|
-
async list(options2 = {}) {
|
|
2921
|
-
await ensureMigrated();
|
|
2922
|
-
const { sql, params } = buildWhereClause2(options2);
|
|
2923
|
-
const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
|
|
2924
|
-
const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
|
|
2925
|
-
let rows;
|
|
2926
|
-
if (typeof options2.limit === "number") {
|
|
2927
|
-
rows = db.query(query).all(...params, options2.limit);
|
|
2928
|
-
} else {
|
|
2929
|
-
rows = db.query(query).all(...params);
|
|
2930
|
-
}
|
|
2931
|
-
return rows.map(mapRow);
|
|
2932
|
-
},
|
|
2933
|
-
async count(options2 = {}) {
|
|
2934
|
-
const { sql, params } = buildWhereClause2(options2);
|
|
2935
|
-
const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
|
|
2936
|
-
const row = db.query(query).get(...params);
|
|
2937
|
-
return Number(row?.count ?? 0);
|
|
2938
|
-
},
|
|
2939
|
-
async deleteOlderThan(timestamp) {
|
|
2940
|
-
await ensureMigrated();
|
|
2941
|
-
const before = timestamp;
|
|
2942
|
-
const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
|
|
2943
|
-
return result.changes ?? 0;
|
|
2944
|
-
},
|
|
2945
|
-
async clear() {
|
|
2946
|
-
await ensureMigrated();
|
|
2947
|
-
db.query(`DELETE FROM ${tableName}`).run();
|
|
2948
|
-
}
|
|
2949
|
-
};
|
|
2950
|
-
};
|
|
2951
|
-
|
|
2952
|
-
// storage/usageTracking/memory.ts
|
|
2953
|
-
var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2954
|
-
var matchesFilters2 = (entry, options = {}) => {
|
|
2955
|
-
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
2956
|
-
return false;
|
|
2957
|
-
}
|
|
2958
|
-
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
2959
|
-
return false;
|
|
2960
|
-
}
|
|
2961
|
-
if (options.modelId && entry.modelId !== options.modelId) {
|
|
2962
|
-
return false;
|
|
2963
|
-
}
|
|
2964
|
-
if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
|
|
2965
|
-
return false;
|
|
2966
|
-
}
|
|
2967
|
-
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
2968
|
-
return false;
|
|
2969
|
-
}
|
|
2970
|
-
if (options.client && entry.client !== options.client) {
|
|
2971
|
-
return false;
|
|
2972
|
-
}
|
|
2973
|
-
return true;
|
|
2974
|
-
};
|
|
2975
|
-
var createMemoryUsageTrackingDriver = (seed = []) => {
|
|
2976
|
-
const store = /* @__PURE__ */ new Map();
|
|
2977
|
-
for (const entry of seed) {
|
|
2978
|
-
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
|
|
2979
|
-
}
|
|
2980
|
-
return {
|
|
2981
|
-
async migrate() {
|
|
2982
|
-
return;
|
|
2983
|
-
},
|
|
2984
|
-
async append(entry) {
|
|
2985
|
-
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
|
|
2986
|
-
},
|
|
2987
|
-
async appendMany(entries) {
|
|
2988
|
-
for (const entry of entries) {
|
|
2989
|
-
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
|
|
2990
|
-
}
|
|
2991
|
-
},
|
|
2992
|
-
async list(options = {}) {
|
|
2993
|
-
const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
|
|
2994
|
-
if (typeof options.limit === "number") {
|
|
2995
|
-
return entries.slice(0, options.limit);
|
|
2996
|
-
}
|
|
2997
|
-
return entries;
|
|
2998
|
-
},
|
|
2999
|
-
async count(options = {}) {
|
|
3000
|
-
return (await this.list(options)).length;
|
|
3001
|
-
},
|
|
3002
|
-
async deleteOlderThan(timestamp) {
|
|
3003
|
-
let deleted = 0;
|
|
3004
|
-
for (const [id, entry] of store.entries()) {
|
|
3005
|
-
if (entry.timestamp < timestamp) {
|
|
3006
|
-
store.delete(id);
|
|
3007
|
-
deleted += 1;
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
return deleted;
|
|
3011
|
-
},
|
|
3012
|
-
async clear() {
|
|
3013
|
-
store.clear();
|
|
3014
|
-
}
|
|
3015
|
-
};
|
|
3016
|
-
};
|
|
3017
|
-
var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3018
|
-
var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
|
|
3019
|
-
modelsFromAllProviders: {},
|
|
3020
|
-
lastUsedModel: null,
|
|
3021
|
-
baseUrlsList: [],
|
|
3022
|
-
lastBaseUrlsUpdate: null,
|
|
3023
|
-
disabledProviders: [],
|
|
3024
|
-
mintsFromAllProviders: {},
|
|
3025
|
-
infoFromAllProviders: {},
|
|
3026
|
-
lastModelsUpdate: {},
|
|
3027
|
-
apiKeys: [],
|
|
3028
|
-
childKeys: [],
|
|
3029
|
-
xcashuTokens: {},
|
|
3030
|
-
routstr21Models: [],
|
|
3031
|
-
lastRoutstr21ModelsUpdate: null,
|
|
3032
|
-
cachedReceiveTokens: [],
|
|
3033
|
-
clientIds: [],
|
|
3034
|
-
failedProviders: [],
|
|
3035
|
-
lastFailed: {},
|
|
3036
|
-
providersOnCooldown: [],
|
|
3037
|
-
setModelsFromAllProviders: (value) => {
|
|
3038
|
-
const normalized = {};
|
|
3039
|
-
for (const [baseUrl, models] of Object.entries(value)) {
|
|
3040
|
-
normalized[normalizeBaseUrl5(baseUrl)] = models;
|
|
3041
|
-
}
|
|
3042
|
-
void driver.setItem(
|
|
3043
|
-
SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
|
|
3044
|
-
normalized
|
|
3045
|
-
);
|
|
3046
|
-
set({ modelsFromAllProviders: normalized });
|
|
3047
|
-
},
|
|
3048
|
-
setLastUsedModel: (value) => {
|
|
3049
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, value);
|
|
3050
|
-
set({ lastUsedModel: value });
|
|
3051
|
-
},
|
|
3052
|
-
setBaseUrlsList: (value) => {
|
|
3053
|
-
const normalized = value.map((url) => normalizeBaseUrl5(url));
|
|
3054
|
-
void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
|
|
3055
|
-
set({ baseUrlsList: normalized });
|
|
3056
|
-
},
|
|
3057
|
-
setBaseUrlsLastUpdate: (value) => {
|
|
3058
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, value);
|
|
3059
|
-
set({ lastBaseUrlsUpdate: value });
|
|
3060
|
-
},
|
|
3061
|
-
setDisabledProviders: (value) => {
|
|
3062
|
-
const normalized = value.map((url) => normalizeBaseUrl5(url));
|
|
3063
|
-
void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
|
|
3064
|
-
set({ disabledProviders: normalized });
|
|
3065
|
-
},
|
|
3066
|
-
setMintsFromAllProviders: (value) => {
|
|
3067
|
-
const normalized = {};
|
|
3068
|
-
for (const [baseUrl, mints] of Object.entries(value)) {
|
|
3069
|
-
normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
|
|
3070
|
-
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
3071
|
-
);
|
|
3072
|
-
}
|
|
3073
|
-
void driver.setItem(
|
|
3074
|
-
SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
|
|
3075
|
-
normalized
|
|
3076
|
-
);
|
|
3077
|
-
set({ mintsFromAllProviders: normalized });
|
|
3078
|
-
},
|
|
3079
|
-
setInfoFromAllProviders: (value) => {
|
|
3080
|
-
const normalized = {};
|
|
3081
|
-
for (const [baseUrl, info] of Object.entries(value)) {
|
|
3082
|
-
normalized[normalizeBaseUrl5(baseUrl)] = info;
|
|
3083
|
-
}
|
|
3084
|
-
void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
|
|
3085
|
-
set({ infoFromAllProviders: normalized });
|
|
3086
|
-
},
|
|
3087
|
-
setLastModelsUpdate: (value) => {
|
|
3088
|
-
const normalized = {};
|
|
3089
|
-
for (const [baseUrl, timestamp] of Object.entries(value)) {
|
|
3090
|
-
normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
|
|
3091
|
-
}
|
|
3092
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
|
|
3093
|
-
set({ lastModelsUpdate: normalized });
|
|
3094
|
-
},
|
|
3095
|
-
setApiKeys: (value) => {
|
|
3096
|
-
set((state) => {
|
|
3097
|
-
const updates = typeof value === "function" ? value(state.apiKeys) : value;
|
|
3098
|
-
const normalized = updates.map((entry) => ({
|
|
3099
|
-
...entry,
|
|
3100
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3101
|
-
balance: entry.balance ?? 0,
|
|
3102
|
-
lastUsed: entry.lastUsed ?? null
|
|
3103
|
-
}));
|
|
3104
|
-
void driver.setItem(SDK_STORAGE_KEYS.API_KEYS, normalized);
|
|
3105
|
-
return { apiKeys: normalized };
|
|
3106
|
-
});
|
|
3107
|
-
},
|
|
3108
|
-
setChildKeys: (value) => {
|
|
3109
|
-
set((state) => {
|
|
3110
|
-
const updates = typeof value === "function" ? value(state.childKeys) : value;
|
|
3111
|
-
const normalized = updates.map((entry) => ({
|
|
3112
|
-
parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
|
|
3113
|
-
childKey: entry.childKey,
|
|
3114
|
-
balance: entry.balance ?? 0,
|
|
3115
|
-
balanceLimit: entry.balanceLimit,
|
|
3116
|
-
validityDate: entry.validityDate,
|
|
3117
|
-
createdAt: entry.createdAt ?? Date.now()
|
|
3118
|
-
}));
|
|
3119
|
-
void driver.setItem(SDK_STORAGE_KEYS.CHILD_KEYS, normalized);
|
|
3120
|
-
return { childKeys: normalized };
|
|
3121
|
-
});
|
|
3122
|
-
},
|
|
3123
|
-
setXcashuTokens: (value) => {
|
|
3124
|
-
const normalized = {};
|
|
3125
|
-
for (const [baseUrl, tokens] of Object.entries(value)) {
|
|
3126
|
-
normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
|
|
3127
|
-
...entry,
|
|
3128
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3129
|
-
createdAt: entry.createdAt ?? Date.now(),
|
|
3130
|
-
tryCount: entry.tryCount ?? 0
|
|
3131
|
-
}));
|
|
3132
|
-
}
|
|
3133
|
-
void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, normalized);
|
|
3134
|
-
set({ xcashuTokens: normalized });
|
|
3135
|
-
},
|
|
3136
|
-
updateXcashuTokenTryCount: (token, tryCount) => {
|
|
3137
|
-
const currentTokens = get().xcashuTokens;
|
|
3138
|
-
const updatedTokens = {};
|
|
3139
|
-
for (const [baseUrl, tokens] of Object.entries(currentTokens)) {
|
|
3140
|
-
updatedTokens[baseUrl] = tokens.map(
|
|
3141
|
-
(entry) => entry.token === token ? { ...entry, tryCount } : entry
|
|
3142
|
-
);
|
|
3143
|
-
}
|
|
3144
|
-
void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, updatedTokens);
|
|
3145
|
-
set({ xcashuTokens: updatedTokens });
|
|
3146
|
-
},
|
|
3147
|
-
setRoutstr21Models: (value) => {
|
|
3148
|
-
void driver.setItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, value);
|
|
3149
|
-
set({ routstr21Models: value });
|
|
3150
|
-
},
|
|
3151
|
-
setRoutstr21ModelsLastUpdate: (value) => {
|
|
3152
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE, value);
|
|
3153
|
-
set({ lastRoutstr21ModelsUpdate: value });
|
|
3154
|
-
},
|
|
3155
|
-
setCachedReceiveTokens: (value) => {
|
|
3156
|
-
const normalized = value.map((entry) => ({
|
|
3157
|
-
token: entry.token,
|
|
3158
|
-
amount: entry.amount,
|
|
3159
|
-
unit: entry.unit || "sat",
|
|
3160
|
-
createdAt: entry.createdAt ?? Date.now()
|
|
3161
|
-
}));
|
|
3162
|
-
void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
|
|
3163
|
-
set({ cachedReceiveTokens: normalized });
|
|
3164
|
-
},
|
|
3165
|
-
setClientIds: (value) => {
|
|
3166
|
-
set((state) => {
|
|
3167
|
-
const updates = typeof value === "function" ? value(state.clientIds) : value;
|
|
3168
|
-
const normalized = updates.map((entry) => ({
|
|
3169
|
-
...entry,
|
|
3170
|
-
createdAt: entry.createdAt ?? Date.now(),
|
|
3171
|
-
lastUsed: entry.lastUsed ?? null
|
|
3172
|
-
}));
|
|
3173
|
-
void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
|
|
3174
|
-
return { clientIds: normalized };
|
|
3175
|
-
});
|
|
3176
|
-
},
|
|
3177
|
-
// ========== Failure Tracking ==========
|
|
3178
|
-
setFailedProviders: (value) => {
|
|
3179
|
-
const normalized = value.map((url) => normalizeBaseUrl5(url));
|
|
3180
|
-
void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
|
|
3181
|
-
set({ failedProviders: normalized });
|
|
3182
|
-
},
|
|
3183
|
-
addFailedProvider: (baseUrl) => {
|
|
3184
|
-
const normalized = normalizeBaseUrl5(baseUrl);
|
|
3185
|
-
const current = get().failedProviders;
|
|
3186
|
-
if (!current.includes(normalized)) {
|
|
3187
|
-
const updated = [...current, normalized];
|
|
3188
|
-
void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
|
|
3189
|
-
set({ failedProviders: updated });
|
|
3190
|
-
}
|
|
3191
|
-
},
|
|
3192
|
-
removeFailedProvider: (baseUrl) => {
|
|
3193
|
-
const normalized = normalizeBaseUrl5(baseUrl);
|
|
3194
|
-
const current = get().failedProviders;
|
|
3195
|
-
const updated = current.filter((url) => url !== normalized);
|
|
3196
|
-
void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
|
|
3197
|
-
set({ failedProviders: updated });
|
|
3198
|
-
},
|
|
3199
|
-
setLastFailed: (value) => {
|
|
3200
|
-
const normalized = {};
|
|
3201
|
-
for (const [baseUrl, timestamp] of Object.entries(value)) {
|
|
3202
|
-
normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
|
|
3203
|
-
}
|
|
3204
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
|
|
3205
|
-
set({ lastFailed: normalized });
|
|
3206
|
-
},
|
|
3207
|
-
setLastFailedTimestamp: (baseUrl, timestamp) => {
|
|
3208
|
-
const normalized = normalizeBaseUrl5(baseUrl);
|
|
3209
|
-
const current = get().lastFailed;
|
|
3210
|
-
const updated = { ...current, [normalized]: timestamp };
|
|
3211
|
-
void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
|
|
3212
|
-
set({ lastFailed: updated });
|
|
3213
|
-
},
|
|
3214
|
-
setProvidersOnCooldown: (value) => {
|
|
3215
|
-
const normalized = value.map((entry) => ({
|
|
3216
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3217
|
-
timestamp: entry.timestamp
|
|
3218
|
-
}));
|
|
3219
|
-
void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
|
|
3220
|
-
set({ providersOnCooldown: normalized });
|
|
3221
|
-
},
|
|
3222
|
-
addProviderOnCooldown: (baseUrl, timestamp) => {
|
|
3223
|
-
const normalized = normalizeBaseUrl5(baseUrl);
|
|
3224
|
-
const current = get().providersOnCooldown;
|
|
3225
|
-
if (!current.some((entry) => entry.baseUrl === normalized)) {
|
|
3226
|
-
const updated = [...current, { baseUrl: normalized, timestamp }];
|
|
3227
|
-
void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
|
|
3228
|
-
set({ providersOnCooldown: updated });
|
|
3229
|
-
}
|
|
3230
|
-
},
|
|
3231
|
-
removeProviderFromCooldown: (baseUrl) => {
|
|
3232
|
-
const normalized = normalizeBaseUrl5(baseUrl);
|
|
3233
|
-
const current = get().providersOnCooldown;
|
|
3234
|
-
const updated = current.filter((entry) => entry.baseUrl !== normalized);
|
|
3235
|
-
void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
|
|
3236
|
-
set({ providersOnCooldown: updated });
|
|
3237
|
-
},
|
|
3238
|
-
clearProvidersOnCooldown: () => {
|
|
3239
|
-
void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, []);
|
|
3240
|
-
set({ providersOnCooldown: [] });
|
|
3241
|
-
}
|
|
3242
|
-
}));
|
|
3243
|
-
var hydrateStoreFromDriver = async (store, driver) => {
|
|
3244
|
-
const [
|
|
3245
|
-
rawModels,
|
|
3246
|
-
lastUsedModel,
|
|
3247
|
-
rawBaseUrls,
|
|
3248
|
-
lastBaseUrlsUpdate,
|
|
3249
|
-
rawDisabledProviders,
|
|
3250
|
-
rawMints,
|
|
3251
|
-
rawInfo,
|
|
3252
|
-
rawLastModelsUpdate,
|
|
3253
|
-
rawApiKeys,
|
|
3254
|
-
rawChildKeys,
|
|
3255
|
-
rawXcashuTokens,
|
|
3256
|
-
rawRoutstr21Models,
|
|
3257
|
-
rawLastRoutstr21ModelsUpdate,
|
|
3258
|
-
rawCachedReceiveTokens,
|
|
3259
|
-
rawClientIds,
|
|
3260
|
-
rawFailedProviders,
|
|
3261
|
-
rawLastFailed,
|
|
3262
|
-
rawProvidersOnCooldown
|
|
3263
|
-
] = await Promise.all([
|
|
3264
|
-
driver.getItem(
|
|
3265
|
-
SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
|
|
3266
|
-
{}
|
|
3267
|
-
),
|
|
3268
|
-
driver.getItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, null),
|
|
3269
|
-
driver.getItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, []),
|
|
3270
|
-
driver.getItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, null),
|
|
3271
|
-
driver.getItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, []),
|
|
3272
|
-
driver.getItem(
|
|
3273
|
-
SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
|
|
3274
|
-
{}
|
|
3275
|
-
),
|
|
3276
|
-
driver.getItem(
|
|
3277
|
-
SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS,
|
|
3278
|
-
{}
|
|
3279
|
-
),
|
|
3280
|
-
driver.getItem(
|
|
3281
|
-
SDK_STORAGE_KEYS.LAST_MODELS_UPDATE,
|
|
3282
|
-
{}
|
|
3283
|
-
),
|
|
3284
|
-
driver.getItem(SDK_STORAGE_KEYS.API_KEYS, []),
|
|
3285
|
-
driver.getItem(SDK_STORAGE_KEYS.CHILD_KEYS, []),
|
|
3286
|
-
driver.getItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, {}),
|
|
3287
|
-
driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
|
|
3288
|
-
driver.getItem(
|
|
3289
|
-
SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
|
|
3290
|
-
null
|
|
3291
|
-
),
|
|
3292
|
-
driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
|
|
3293
|
-
driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
|
|
3294
|
-
driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
|
|
3295
|
-
driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
|
|
3296
|
-
driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
|
|
3297
|
-
]);
|
|
3298
|
-
const modelsFromAllProviders = Object.fromEntries(
|
|
3299
|
-
Object.entries(rawModels).map(([baseUrl, models]) => [
|
|
3300
|
-
normalizeBaseUrl5(baseUrl),
|
|
3301
|
-
models
|
|
3302
|
-
])
|
|
3303
|
-
);
|
|
3304
|
-
const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
|
|
3305
|
-
const disabledProviders = rawDisabledProviders.map(
|
|
3306
|
-
(url) => normalizeBaseUrl5(url)
|
|
3307
|
-
);
|
|
3308
|
-
const mintsFromAllProviders = Object.fromEntries(
|
|
3309
|
-
Object.entries(rawMints).map(([baseUrl, mints]) => [
|
|
3310
|
-
normalizeBaseUrl5(baseUrl),
|
|
3311
|
-
mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
|
|
3312
|
-
])
|
|
3313
|
-
);
|
|
3314
|
-
const infoFromAllProviders = Object.fromEntries(
|
|
3315
|
-
Object.entries(rawInfo).map(([baseUrl, info]) => [
|
|
3316
|
-
normalizeBaseUrl5(baseUrl),
|
|
3317
|
-
info
|
|
3318
|
-
])
|
|
3319
|
-
);
|
|
3320
|
-
const lastModelsUpdate = Object.fromEntries(
|
|
3321
|
-
Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
|
|
3322
|
-
normalizeBaseUrl5(baseUrl),
|
|
3323
|
-
timestamp
|
|
3324
|
-
])
|
|
3325
|
-
);
|
|
3326
|
-
const apiKeys = rawApiKeys.map((entry) => ({
|
|
3327
|
-
...entry,
|
|
3328
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3329
|
-
balance: entry.balance ?? 0,
|
|
3330
|
-
lastUsed: entry.lastUsed ?? null
|
|
3331
|
-
}));
|
|
3332
|
-
const childKeys = rawChildKeys.map((entry) => ({
|
|
3333
|
-
parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
|
|
3334
|
-
childKey: entry.childKey,
|
|
3335
|
-
balance: entry.balance ?? 0,
|
|
3336
|
-
balanceLimit: entry.balanceLimit,
|
|
3337
|
-
validityDate: entry.validityDate,
|
|
3338
|
-
createdAt: entry.createdAt ?? Date.now()
|
|
3339
|
-
}));
|
|
3340
|
-
const xcashuTokens = Object.fromEntries(
|
|
3341
|
-
Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
|
|
3342
|
-
normalizeBaseUrl5(baseUrl),
|
|
3343
|
-
tokens.map((entry) => ({
|
|
3344
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3345
|
-
token: entry.token,
|
|
3346
|
-
createdAt: entry.createdAt ?? Date.now(),
|
|
3347
|
-
tryCount: entry.tryCount ?? 0
|
|
3348
|
-
}))
|
|
3349
|
-
])
|
|
3350
|
-
);
|
|
3351
|
-
const routstr21Models = rawRoutstr21Models;
|
|
3352
|
-
const lastRoutstr21ModelsUpdate = rawLastRoutstr21ModelsUpdate;
|
|
3353
|
-
const cachedReceiveTokens = rawCachedReceiveTokens?.map((entry) => ({
|
|
3354
|
-
token: entry.token,
|
|
3355
|
-
amount: entry.amount,
|
|
3356
|
-
unit: entry.unit || "sat",
|
|
3357
|
-
createdAt: entry.createdAt ?? Date.now()
|
|
3358
|
-
}));
|
|
3359
|
-
const clientIds = rawClientIds.map((entry) => ({
|
|
3360
|
-
...entry,
|
|
3361
|
-
createdAt: entry.createdAt ?? Date.now(),
|
|
3362
|
-
lastUsed: entry.lastUsed ?? null
|
|
3363
|
-
}));
|
|
3364
|
-
const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
|
|
3365
|
-
const lastFailed = Object.fromEntries(
|
|
3366
|
-
Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
|
|
3367
|
-
normalizeBaseUrl5(baseUrl),
|
|
3368
|
-
timestamp
|
|
3369
|
-
])
|
|
3370
|
-
);
|
|
3371
|
-
const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
|
|
3372
|
-
baseUrl: normalizeBaseUrl5(entry.baseUrl),
|
|
3373
|
-
timestamp: entry.timestamp
|
|
3374
|
-
}));
|
|
3375
|
-
store.setState({
|
|
3376
|
-
modelsFromAllProviders,
|
|
3377
|
-
lastUsedModel,
|
|
3378
|
-
baseUrlsList,
|
|
3379
|
-
lastBaseUrlsUpdate,
|
|
3380
|
-
disabledProviders,
|
|
3381
|
-
mintsFromAllProviders,
|
|
3382
|
-
infoFromAllProviders,
|
|
3383
|
-
lastModelsUpdate,
|
|
3384
|
-
apiKeys,
|
|
3385
|
-
childKeys,
|
|
3386
|
-
xcashuTokens,
|
|
3387
|
-
routstr21Models,
|
|
3388
|
-
lastRoutstr21ModelsUpdate,
|
|
3389
|
-
cachedReceiveTokens,
|
|
3390
|
-
clientIds,
|
|
3391
|
-
failedProviders,
|
|
3392
|
-
lastFailed,
|
|
3393
|
-
providersOnCooldown
|
|
3394
|
-
});
|
|
3395
|
-
};
|
|
3396
|
-
var createSdkStore = ({
|
|
3397
|
-
driver
|
|
3398
|
-
}) => {
|
|
3399
|
-
const store = createEmptyStore(driver);
|
|
3400
|
-
return {
|
|
3401
|
-
store,
|
|
3402
|
-
hydrate: hydrateStoreFromDriver(store, driver)
|
|
3403
|
-
};
|
|
3404
|
-
};
|
|
3405
|
-
|
|
3406
|
-
// storage/index.ts
|
|
3407
|
-
var isBrowser2 = () => {
|
|
3408
|
-
try {
|
|
3409
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
3410
|
-
} catch {
|
|
3411
|
-
return false;
|
|
3412
|
-
}
|
|
3413
|
-
};
|
|
3414
|
-
var isNode = () => {
|
|
3415
|
-
try {
|
|
3416
|
-
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
3417
|
-
} catch {
|
|
3418
|
-
return false;
|
|
3419
|
-
}
|
|
3420
|
-
};
|
|
3421
|
-
var defaultDriver = null;
|
|
3422
|
-
var isBun3 = () => {
|
|
3423
|
-
return typeof process.versions.bun !== "undefined";
|
|
3424
|
-
};
|
|
3425
|
-
var getDefaultSdkDriver = () => {
|
|
3426
|
-
if (defaultDriver) return defaultDriver;
|
|
3427
|
-
if (isBrowser2()) {
|
|
3428
|
-
defaultDriver = localStorageDriver;
|
|
3429
|
-
return defaultDriver;
|
|
3430
|
-
}
|
|
3431
|
-
if (isBun3()) {
|
|
3432
|
-
defaultDriver = createMemoryDriver();
|
|
3433
|
-
return defaultDriver;
|
|
3434
|
-
}
|
|
3435
|
-
if (isNode()) {
|
|
3436
|
-
defaultDriver = createSqliteDriver();
|
|
3437
|
-
return defaultDriver;
|
|
3438
|
-
}
|
|
3439
|
-
defaultDriver = createMemoryDriver();
|
|
3440
|
-
return defaultDriver;
|
|
3441
|
-
};
|
|
3442
|
-
var defaultStore = null;
|
|
3443
|
-
var defaultUsageTrackingDriver = null;
|
|
3444
|
-
var getDefaultSdkStore = () => {
|
|
3445
|
-
if (!defaultStore) {
|
|
3446
|
-
defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
|
|
3447
|
-
}
|
|
3448
|
-
return defaultStore.hydrate.then(() => defaultStore.store);
|
|
3449
|
-
};
|
|
3450
|
-
var getDefaultUsageTrackingDriver = () => {
|
|
3451
|
-
if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
|
|
3452
|
-
const storageDriver = getDefaultSdkDriver();
|
|
3453
|
-
if (isBrowser2()) {
|
|
3454
|
-
defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
|
|
3455
|
-
legacyStorageDriver: storageDriver
|
|
3456
|
-
});
|
|
3457
|
-
return defaultUsageTrackingDriver;
|
|
3458
|
-
}
|
|
3459
|
-
if (isBun3()) {
|
|
3460
|
-
defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
|
|
3461
|
-
return defaultUsageTrackingDriver;
|
|
3462
|
-
}
|
|
3463
|
-
if (isNode()) {
|
|
3464
|
-
defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
|
|
3465
|
-
legacyStorageDriver: storageDriver
|
|
3466
|
-
});
|
|
3467
|
-
return defaultUsageTrackingDriver;
|
|
3468
|
-
}
|
|
3469
|
-
defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
|
|
3470
|
-
return defaultUsageTrackingDriver;
|
|
3471
|
-
};
|
|
3472
|
-
function mergeUsage(previous, next) {
|
|
3473
|
-
if (!previous) return next;
|
|
3474
|
-
return {
|
|
3475
|
-
promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
|
|
3476
|
-
completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
|
|
3477
|
-
totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
|
|
3478
|
-
cost: next.cost > 0 ? next.cost : previous.cost,
|
|
3479
|
-
satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
|
|
3480
|
-
};
|
|
3481
|
-
}
|
|
3482
|
-
function hasUsageChanged(previous, next) {
|
|
3483
|
-
if (!previous) return true;
|
|
3484
|
-
return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
|
|
3485
|
-
}
|
|
3486
|
-
async function inspectSSEWebStream(stream, onUsage, onResponseId) {
|
|
3487
|
-
const reader = stream.getReader();
|
|
3488
|
-
const decoder = new TextDecoder("utf-8");
|
|
3489
|
-
let buffer = "";
|
|
3490
|
-
let capturedUsage = null;
|
|
3491
|
-
let capturedResponseId;
|
|
3492
|
-
let responseIdCaptured = false;
|
|
3493
|
-
const inspectDataPayload = (jsonText) => {
|
|
3494
|
-
if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
|
|
3495
|
-
return;
|
|
3496
|
-
}
|
|
3497
|
-
const trimmed = jsonText.trim();
|
|
3498
|
-
if (!trimmed || trimmed === "[DONE]") return;
|
|
3499
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
|
|
3500
|
-
try {
|
|
3501
|
-
const data = JSON.parse(trimmed);
|
|
3502
|
-
if (!responseIdCaptured) {
|
|
3503
|
-
const responseId = data?.id;
|
|
3504
|
-
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
3505
|
-
capturedResponseId = responseId.trim();
|
|
3506
|
-
onResponseId?.(capturedResponseId);
|
|
3507
|
-
responseIdCaptured = true;
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
const usage = extractUsageFromSSEJson(data);
|
|
3511
|
-
if (usage) {
|
|
3512
|
-
const merged = mergeUsage(capturedUsage, usage);
|
|
3513
|
-
if (hasUsageChanged(capturedUsage, merged)) {
|
|
3514
|
-
capturedUsage = merged;
|
|
3515
|
-
onUsage(merged);
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
} catch {
|
|
3519
|
-
}
|
|
3520
|
-
};
|
|
3521
|
-
const inspectEventBlock = (eventBlock) => {
|
|
3522
|
-
if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
|
|
3523
|
-
return;
|
|
3524
|
-
}
|
|
3525
|
-
const lines = eventBlock.split(/\r?\n/);
|
|
3526
|
-
const dataParts = [];
|
|
3527
|
-
for (const line of lines) {
|
|
3528
|
-
if (!line || line.startsWith(":")) continue;
|
|
3529
|
-
if (line.startsWith("data:")) {
|
|
3530
|
-
const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
|
|
3531
|
-
dataParts.push(value);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
if (dataParts.length === 0) return;
|
|
3535
|
-
inspectDataPayload(dataParts.join("\n"));
|
|
3536
|
-
};
|
|
3537
|
-
const drainBufferedEvents = () => {
|
|
3538
|
-
const terminator = /\r?\n\r?\n/g;
|
|
3539
|
-
let lastIndex = 0;
|
|
3540
|
-
let match;
|
|
3541
|
-
while ((match = terminator.exec(buffer)) !== null) {
|
|
3542
|
-
const block = buffer.slice(lastIndex, match.index);
|
|
3543
|
-
lastIndex = match.index + match[0].length;
|
|
3544
|
-
if (block.length > 0) inspectEventBlock(block);
|
|
3545
|
-
}
|
|
3546
|
-
if (lastIndex > 0) buffer = buffer.slice(lastIndex);
|
|
3547
|
-
};
|
|
3548
|
-
try {
|
|
3549
|
-
while (true) {
|
|
3550
|
-
const { value, done } = await reader.read();
|
|
3551
|
-
if (done) break;
|
|
3552
|
-
if (value && value.byteLength > 0) {
|
|
3553
|
-
buffer += decoder.decode(value, { stream: true });
|
|
3554
|
-
drainBufferedEvents();
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
buffer += decoder.decode();
|
|
3558
|
-
drainBufferedEvents();
|
|
3559
|
-
if (buffer.length > 0) {
|
|
3560
|
-
const tail = buffer.replace(/\r?\n+$/, "");
|
|
3561
|
-
if (tail.length > 0) inspectEventBlock(tail);
|
|
3562
|
-
buffer = "";
|
|
3563
|
-
}
|
|
3564
|
-
} catch {
|
|
3565
|
-
} finally {
|
|
3566
|
-
try {
|
|
3567
|
-
reader.releaseLock();
|
|
3568
|
-
} catch {
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
return {
|
|
3572
|
-
capturedUsage: capturedUsage ?? void 0,
|
|
3573
|
-
capturedResponseId
|
|
3574
|
-
};
|
|
3575
|
-
}
|
|
3576
|
-
function createSSEParserTransform(onUsage, onResponseId) {
|
|
3577
|
-
let buffer = "";
|
|
3578
|
-
const decoder = new string_decoder.StringDecoder("utf8");
|
|
3579
|
-
let capturedUsage = null;
|
|
3580
|
-
let responseIdCaptured = false;
|
|
3581
|
-
const inspectDataPayload = (jsonText) => {
|
|
3582
|
-
if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
|
|
3583
|
-
return;
|
|
3584
|
-
}
|
|
3585
|
-
const trimmed = jsonText.trim();
|
|
3586
|
-
if (!trimmed || trimmed === "[DONE]") return;
|
|
3587
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
|
|
3588
|
-
try {
|
|
3589
|
-
const data = JSON.parse(trimmed);
|
|
3590
|
-
if (!responseIdCaptured) {
|
|
3591
|
-
const responseId = data?.id;
|
|
3592
|
-
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
3593
|
-
onResponseId?.(responseId.trim());
|
|
3594
|
-
responseIdCaptured = true;
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3597
|
-
const usage = extractUsageFromSSEJson(data);
|
|
3598
|
-
if (usage) {
|
|
3599
|
-
const mergedUsage = mergeUsage(capturedUsage, usage);
|
|
3600
|
-
if (hasUsageChanged(capturedUsage, mergedUsage)) {
|
|
3601
|
-
capturedUsage = mergedUsage;
|
|
3602
|
-
onUsage(mergedUsage);
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
} catch {
|
|
3606
|
-
}
|
|
3607
|
-
};
|
|
3608
|
-
const inspectEventBlock = (eventBlock) => {
|
|
3609
|
-
if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
|
|
3610
|
-
return;
|
|
3611
|
-
}
|
|
3612
|
-
const lines = eventBlock.split(/\r?\n/);
|
|
3613
|
-
const dataParts = [];
|
|
3614
|
-
for (const line of lines) {
|
|
3615
|
-
if (!line || line.startsWith(":")) continue;
|
|
3616
|
-
if (line.startsWith("data:")) {
|
|
3617
|
-
const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
|
|
3618
|
-
dataParts.push(value);
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
if (dataParts.length === 0) return;
|
|
3622
|
-
const payload = dataParts.join("\n");
|
|
3623
|
-
inspectDataPayload(payload);
|
|
3624
|
-
};
|
|
3625
|
-
const processBufferedEvents = () => {
|
|
3626
|
-
const terminator = /\r?\n\r?\n/g;
|
|
3627
|
-
let lastIndex = 0;
|
|
3628
|
-
let match;
|
|
3629
|
-
while ((match = terminator.exec(buffer)) !== null) {
|
|
3630
|
-
const block = buffer.slice(lastIndex, match.index);
|
|
3631
|
-
lastIndex = match.index + match[0].length;
|
|
3632
|
-
if (block.length > 0) {
|
|
3633
|
-
inspectEventBlock(block);
|
|
3634
|
-
}
|
|
3635
|
-
}
|
|
3636
|
-
if (lastIndex > 0) {
|
|
3637
|
-
buffer = buffer.slice(lastIndex);
|
|
3638
|
-
}
|
|
3639
|
-
};
|
|
3640
|
-
return new stream.Transform({
|
|
3641
|
-
transform(chunk, _encoding, callback) {
|
|
3642
|
-
this.push(chunk);
|
|
3643
|
-
buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3644
|
-
processBufferedEvents();
|
|
3645
|
-
callback();
|
|
3646
|
-
},
|
|
3647
|
-
flush(callback) {
|
|
3648
|
-
buffer += decoder.end();
|
|
3649
|
-
processBufferedEvents();
|
|
3650
|
-
if (buffer.length > 0) {
|
|
3651
|
-
const tail = buffer.replace(/\r?\n+$/, "");
|
|
3652
|
-
if (tail.length > 0) {
|
|
3653
|
-
inspectEventBlock(tail);
|
|
3654
|
-
}
|
|
3655
|
-
buffer = "";
|
|
3656
|
-
}
|
|
3657
|
-
callback();
|
|
3658
|
-
}
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
|
|
3662
|
-
// client/RoutstrClient.ts
|
|
3663
|
-
var TOPUP_MARGIN = 1.2;
|
|
3664
|
-
var RoutstrClient = class {
|
|
3665
|
-
constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
|
|
3666
|
-
this.walletAdapter = walletAdapter;
|
|
3667
|
-
this.storageAdapter = storageAdapter;
|
|
3668
|
-
this.providerRegistry = providerRegistry;
|
|
3669
|
-
this.logger = (options.logger ?? consoleLogger).child("RoutstrClient");
|
|
3670
|
-
this.balanceManager = new BalanceManager(
|
|
3671
|
-
walletAdapter,
|
|
3672
|
-
storageAdapter,
|
|
3673
|
-
providerRegistry
|
|
3674
|
-
);
|
|
3675
|
-
this.cashuSpender = new CashuSpender(
|
|
3676
|
-
walletAdapter,
|
|
3677
|
-
storageAdapter,
|
|
3678
|
-
providerRegistry,
|
|
3679
|
-
this.balanceManager
|
|
3680
|
-
);
|
|
3681
|
-
this.streamProcessor = new StreamProcessor();
|
|
3682
|
-
this.alertLevel = alertLevel;
|
|
3683
|
-
this.mode = mode;
|
|
3684
|
-
this.usageTrackingDriver = options.usageTrackingDriver;
|
|
3685
|
-
this.sdkStore = options.sdkStore;
|
|
3686
|
-
this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore, this.logger);
|
|
3687
|
-
}
|
|
3688
|
-
cashuSpender;
|
|
3689
|
-
balanceManager;
|
|
3690
|
-
streamProcessor;
|
|
3691
|
-
providerManager;
|
|
3692
|
-
alertLevel;
|
|
3693
|
-
mode;
|
|
3694
|
-
debugLevel = "WARN";
|
|
3695
|
-
usageTrackingDriver;
|
|
3696
|
-
sdkStore;
|
|
3697
|
-
logger;
|
|
3698
|
-
/**
|
|
3699
|
-
* Get the current client mode
|
|
3700
|
-
*/
|
|
3701
|
-
getMode() {
|
|
3702
|
-
return this.mode;
|
|
3703
|
-
}
|
|
3704
|
-
getDebugLevel() {
|
|
3705
|
-
return this.debugLevel;
|
|
3706
|
-
}
|
|
3707
|
-
setDebugLevel(level) {
|
|
3708
|
-
this.debugLevel = level;
|
|
3709
|
-
}
|
|
3710
|
-
_log(level, ...args) {
|
|
3711
|
-
const levelPriority = {
|
|
3712
|
-
DEBUG: 0,
|
|
3713
|
-
WARN: 1,
|
|
3714
|
-
ERROR: 2
|
|
3715
|
-
};
|
|
3716
|
-
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
3717
|
-
switch (level) {
|
|
3718
|
-
case "DEBUG":
|
|
3719
|
-
this.logger.log(...args);
|
|
3720
|
-
break;
|
|
3721
|
-
case "WARN":
|
|
3722
|
-
this.logger.warn(...args);
|
|
3723
|
-
break;
|
|
3724
|
-
case "ERROR":
|
|
3725
|
-
this.logger.error(...args);
|
|
3726
|
-
break;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
/**
|
|
3731
|
-
* Get the CashuSpender instance
|
|
3732
|
-
*/
|
|
3733
|
-
getCashuSpender() {
|
|
3734
|
-
return this.cashuSpender;
|
|
3735
|
-
}
|
|
3736
|
-
/**
|
|
3737
|
-
* Get the BalanceManager instance
|
|
3738
|
-
*/
|
|
3739
|
-
getBalanceManager() {
|
|
3740
|
-
return this.balanceManager;
|
|
3741
|
-
}
|
|
3742
|
-
/**
|
|
3743
|
-
* Get the ProviderManager instance
|
|
3744
|
-
*/
|
|
3745
|
-
getProviderManager() {
|
|
3746
|
-
return this.providerManager;
|
|
3747
|
-
}
|
|
3748
|
-
/**
|
|
3749
|
-
* Check if the client is currently busy (in critical section)
|
|
3750
|
-
*/
|
|
3751
|
-
get isBusy() {
|
|
3752
|
-
return this.cashuSpender.isBusy;
|
|
3753
|
-
}
|
|
3754
|
-
/**
|
|
3755
|
-
* Route an API request to the upstream provider
|
|
3756
|
-
*
|
|
3757
|
-
* This is a simpler alternative to fetchAIResponse that just proxies
|
|
3758
|
-
* the request upstream without the streaming callback machinery.
|
|
3759
|
-
* Useful for daemon-style routing where you just need to forward
|
|
3760
|
-
* requests and get responses back.
|
|
3761
|
-
*/
|
|
3762
|
-
async routeRequest(params) {
|
|
3763
|
-
const prepared = await this._prepareRoutedRequest(params);
|
|
3764
|
-
const contentType = prepared.response.headers.get("content-type") || "";
|
|
3765
|
-
const isSSE = contentType.includes("text/event-stream");
|
|
3766
|
-
const runFinalize = async () => {
|
|
3767
|
-
const { capturedUsage, capturedResponseId } = await prepared.usagePromise;
|
|
3768
|
-
const usage = capturedUsage ?? prepared.capturedUsage;
|
|
3769
|
-
const requestId = capturedResponseId ?? prepared.capturedResponseId;
|
|
3770
|
-
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
3771
|
-
token: prepared.tokenUsed,
|
|
3772
|
-
baseUrl: prepared.baseUrlUsed,
|
|
3773
|
-
mintUrl: params.mintUrl,
|
|
3774
|
-
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
3775
|
-
response: prepared.response,
|
|
3776
|
-
modelId: prepared.modelId,
|
|
3777
|
-
usage,
|
|
3778
|
-
requestId,
|
|
3779
|
-
clientApiKey: prepared.clientApiKey
|
|
3780
|
-
});
|
|
3781
|
-
prepared.response.satsSpent = satsSpent;
|
|
3782
|
-
prepared.response.usage = usage;
|
|
3783
|
-
prepared.response.requestId = requestId;
|
|
3784
|
-
return satsSpent;
|
|
3785
|
-
};
|
|
3786
|
-
if (isSSE) {
|
|
3787
|
-
const finalizePromise = runFinalize().catch((error) => {
|
|
3788
|
-
this._log("ERROR", "[RoutstrClient] SSE finalize failed:", error);
|
|
3789
|
-
return 0;
|
|
3790
|
-
});
|
|
3791
|
-
prepared.response.finalize = () => finalizePromise;
|
|
3792
|
-
return prepared.response;
|
|
3793
|
-
}
|
|
3794
|
-
await runFinalize();
|
|
3795
|
-
return prepared.response;
|
|
3796
|
-
}
|
|
3797
|
-
async _prepareRoutedRequest(params) {
|
|
3798
|
-
const {
|
|
3799
|
-
path: requestPath,
|
|
3800
|
-
method,
|
|
3801
|
-
body,
|
|
3802
|
-
headers = {},
|
|
3803
|
-
baseUrl,
|
|
3804
|
-
mintUrl,
|
|
3805
|
-
modelId,
|
|
3806
|
-
clientApiKey: providedClientApiKey
|
|
3807
|
-
} = params;
|
|
3808
|
-
const clientApiKey = providedClientApiKey ?? this._extractClientApiKey(headers);
|
|
3809
|
-
await this._checkBalance();
|
|
3810
|
-
let requiredSats = 1;
|
|
3811
|
-
let selectedModel;
|
|
3812
|
-
if (modelId) {
|
|
3813
|
-
const providerModel = await this.providerManager.getModelForProvider(
|
|
3814
|
-
baseUrl,
|
|
3815
|
-
modelId
|
|
3816
|
-
);
|
|
3817
|
-
selectedModel = providerModel ?? void 0;
|
|
3818
|
-
if (selectedModel) {
|
|
3819
|
-
const requestMessages = Array.isArray(
|
|
3820
|
-
body?.messages
|
|
3821
|
-
) ? body.messages : [];
|
|
3822
|
-
const requestMaxTokens = typeof body?.max_tokens === "number" ? body.max_tokens : void 0;
|
|
3823
|
-
this._log(
|
|
3824
|
-
"DEBUG",
|
|
3825
|
-
"[RoutstrClient] generic request pricing input",
|
|
3826
|
-
{
|
|
3827
|
-
modelId: selectedModel.id,
|
|
3828
|
-
messageCount: requestMessages.length,
|
|
3829
|
-
maxTokens: requestMaxTokens
|
|
3830
|
-
}
|
|
3831
|
-
);
|
|
3832
|
-
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3833
|
-
selectedModel,
|
|
3834
|
-
requestMessages,
|
|
3835
|
-
requestMaxTokens
|
|
3836
|
-
);
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
|
|
3840
|
-
mintUrl,
|
|
3841
|
-
amount: requiredSats,
|
|
3842
|
-
baseUrl
|
|
3843
|
-
});
|
|
3844
|
-
let requestBody = body;
|
|
3845
|
-
if (body && typeof body === "object") {
|
|
3846
|
-
const bodyObj = body;
|
|
3847
|
-
if (!bodyObj.stream) {
|
|
3848
|
-
requestBody = { ...bodyObj, stream: false };
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
const baseHeaders = this._buildBaseHeaders();
|
|
3852
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
3853
|
-
const response = await this._makeRequest({
|
|
3854
|
-
path: requestPath,
|
|
3855
|
-
method,
|
|
3856
|
-
body: method === "GET" ? void 0 : requestBody,
|
|
3857
|
-
baseUrl,
|
|
3858
|
-
mintUrl,
|
|
3859
|
-
token,
|
|
3860
|
-
requiredSats,
|
|
3861
|
-
headers: requestHeaders,
|
|
3862
|
-
baseHeaders,
|
|
3863
|
-
selectedModel
|
|
3864
|
-
});
|
|
3865
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
3866
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
3867
|
-
const tokenUsed = response.token || token;
|
|
3868
|
-
const contentType = response.headers.get("content-type") || "";
|
|
3869
|
-
let processedResponse = response;
|
|
3870
|
-
let capturedUsage;
|
|
3871
|
-
let capturedResponseId;
|
|
3872
|
-
let usagePromise = Promise.resolve({});
|
|
3873
|
-
if (contentType.includes("text/event-stream") && response.body) {
|
|
3874
|
-
const [clientStream, inspectStream] = response.body.tee();
|
|
3875
|
-
processedResponse = new Response(clientStream, {
|
|
3876
|
-
status: response.status,
|
|
3877
|
-
statusText: response.statusText,
|
|
3878
|
-
headers: response.headers
|
|
3879
|
-
});
|
|
3880
|
-
processedResponse.baseUrl = response.baseUrl;
|
|
3881
|
-
processedResponse.token = response.token;
|
|
3882
|
-
usagePromise = inspectSSEWebStream(
|
|
3883
|
-
inspectStream,
|
|
3884
|
-
(usage) => {
|
|
3885
|
-
capturedUsage = usage;
|
|
3886
|
-
processedResponse.usage = usage;
|
|
3887
|
-
},
|
|
3888
|
-
(responseId) => {
|
|
3889
|
-
capturedResponseId = responseId;
|
|
3890
|
-
processedResponse.requestId = responseId;
|
|
3891
|
-
}
|
|
3892
|
-
);
|
|
3893
|
-
processedResponse.usagePromise = usagePromise;
|
|
3894
|
-
}
|
|
3895
|
-
return {
|
|
3896
|
-
response: processedResponse,
|
|
3897
|
-
tokenUsed,
|
|
3898
|
-
baseUrlUsed,
|
|
3899
|
-
tokenBalanceInSats,
|
|
3900
|
-
modelId,
|
|
3901
|
-
capturedUsage,
|
|
3902
|
-
capturedResponseId,
|
|
3903
|
-
clientApiKey,
|
|
3904
|
-
usagePromise
|
|
3905
|
-
};
|
|
3906
|
-
}
|
|
3907
|
-
/**
|
|
3908
|
-
* Extract clientApiKey from Authorization Bearer token if present
|
|
3909
|
-
*/
|
|
3910
|
-
_extractClientApiKey(headers) {
|
|
3911
|
-
const authHeader = headers["Authorization"] || headers["authorization"];
|
|
3912
|
-
if (authHeader?.startsWith("Bearer ")) {
|
|
3913
|
-
const extractedKey = authHeader.slice(7);
|
|
3914
|
-
return extractedKey;
|
|
3915
|
-
}
|
|
3916
|
-
return void 0;
|
|
3917
|
-
}
|
|
3918
|
-
/**
|
|
3919
|
-
* Fetch AI response with streaming
|
|
3920
|
-
*/
|
|
3921
|
-
async fetchAIResponse(options, callbacks) {
|
|
3922
|
-
const {
|
|
3923
|
-
messageHistory,
|
|
3924
|
-
selectedModel,
|
|
3925
|
-
baseUrl,
|
|
3926
|
-
mintUrl,
|
|
3927
|
-
balance,
|
|
3928
|
-
transactionHistory,
|
|
3929
|
-
maxTokens,
|
|
3930
|
-
headers
|
|
3931
|
-
} = options;
|
|
3932
|
-
const apiMessages = await this._convertMessages(messageHistory);
|
|
3933
|
-
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3934
|
-
selectedModel,
|
|
3935
|
-
apiMessages,
|
|
3936
|
-
maxTokens
|
|
3937
|
-
);
|
|
3938
|
-
try {
|
|
3939
|
-
await this._checkBalance();
|
|
3940
|
-
callbacks.onPaymentProcessing?.(true);
|
|
3941
|
-
const spendResult = await this._spendToken({
|
|
3942
|
-
mintUrl,
|
|
3943
|
-
amount: requiredSats,
|
|
3944
|
-
baseUrl
|
|
3945
|
-
});
|
|
3946
|
-
let token = spendResult.token;
|
|
3947
|
-
let tokenBalance = spendResult.tokenBalance;
|
|
3948
|
-
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
3949
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
3950
|
-
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
3951
|
-
const baseHeaders = this._buildBaseHeaders(headers);
|
|
3952
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
3953
|
-
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
3954
|
-
const providerVersion = providerInfo?.version ?? "";
|
|
3955
|
-
let modelIdForRequest = selectedModel.id;
|
|
3956
|
-
if (/^0\.1\./.test(providerVersion)) {
|
|
3957
|
-
const newModel = await this.providerManager.getModelForProvider(
|
|
3958
|
-
baseUrl,
|
|
3959
|
-
selectedModel.id
|
|
3960
|
-
);
|
|
3961
|
-
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
3962
|
-
}
|
|
3963
|
-
const body = {
|
|
3964
|
-
model: modelIdForRequest,
|
|
3965
|
-
messages: apiMessages,
|
|
3966
|
-
stream: true
|
|
3967
|
-
};
|
|
3968
|
-
if (maxTokens !== void 0) {
|
|
3969
|
-
body.max_tokens = maxTokens;
|
|
3970
|
-
}
|
|
3971
|
-
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
3972
|
-
body.tools = [{ type: "web_search" }];
|
|
3973
|
-
}
|
|
3974
|
-
const response = await this._makeRequest({
|
|
3975
|
-
path: "/v1/chat/completions",
|
|
3976
|
-
method: "POST",
|
|
3977
|
-
body,
|
|
3978
|
-
selectedModel,
|
|
3979
|
-
baseUrl,
|
|
3980
|
-
mintUrl,
|
|
3981
|
-
token,
|
|
3982
|
-
requiredSats,
|
|
3983
|
-
maxTokens,
|
|
3984
|
-
headers: requestHeaders,
|
|
3985
|
-
baseHeaders
|
|
3986
|
-
});
|
|
3987
|
-
if (!response.body) {
|
|
3988
|
-
throw new Error("Response body is not available");
|
|
3989
|
-
}
|
|
3990
|
-
if (response.status === 200) {
|
|
3991
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
3992
|
-
const streamingResult = await this.streamProcessor.process(
|
|
3993
|
-
response,
|
|
3994
|
-
{
|
|
3995
|
-
onContent: callbacks.onStreamingUpdate,
|
|
3996
|
-
onThinking: callbacks.onThinkingUpdate
|
|
3997
|
-
},
|
|
3998
|
-
selectedModel.id
|
|
3999
|
-
);
|
|
4000
|
-
if (streamingResult.finish_reason === "content_filter") {
|
|
4001
|
-
callbacks.onMessageAppend({
|
|
4002
|
-
role: "assistant",
|
|
4003
|
-
content: "Your request was denied due to content filtering."
|
|
4004
|
-
});
|
|
4005
|
-
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
4006
|
-
const message = await this._createAssistantMessage(streamingResult);
|
|
4007
|
-
callbacks.onMessageAppend(message);
|
|
4008
|
-
} else {
|
|
4009
|
-
callbacks.onMessageAppend({
|
|
4010
|
-
role: "system",
|
|
4011
|
-
content: "The provider did not respond to this request."
|
|
4012
|
-
});
|
|
4013
|
-
}
|
|
4014
|
-
callbacks.onStreamingUpdate("");
|
|
4015
|
-
callbacks.onThinkingUpdate("");
|
|
4016
|
-
const isApikeysEstimate = this.mode === "apikeys";
|
|
4017
|
-
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4018
|
-
token,
|
|
4019
|
-
baseUrl: baseUrlUsed,
|
|
4020
|
-
mintUrl,
|
|
4021
|
-
initialTokenBalance: tokenBalanceInSats,
|
|
4022
|
-
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
4023
|
-
response,
|
|
4024
|
-
modelId: selectedModel.id,
|
|
4025
|
-
usage: streamingResult.usage ? {
|
|
4026
|
-
promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
|
|
4027
|
-
completionTokens: Number(
|
|
4028
|
-
streamingResult.usage.completion_tokens ?? 0
|
|
4029
|
-
),
|
|
4030
|
-
totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
|
|
4031
|
-
cost: Number(streamingResult.usage.cost ?? 0),
|
|
4032
|
-
satsCost: Number(streamingResult.usage.sats_cost ?? 0)
|
|
4033
|
-
} : void 0,
|
|
4034
|
-
requestId: streamingResult.responseId
|
|
4035
|
-
});
|
|
4036
|
-
const estimatedCosts = this._getEstimatedCosts(
|
|
4037
|
-
selectedModel,
|
|
4038
|
-
streamingResult
|
|
4039
|
-
);
|
|
4040
|
-
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
4041
|
-
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
4042
|
-
} else {
|
|
4043
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
4044
|
-
}
|
|
4045
|
-
} catch (error) {
|
|
4046
|
-
this._handleError(error, callbacks);
|
|
4047
|
-
} finally {
|
|
4048
|
-
callbacks.onPaymentProcessing?.(false);
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
/**
|
|
4052
|
-
* Make the API request with failover support
|
|
4053
|
-
*/
|
|
4054
|
-
async _makeRequest(params) {
|
|
4055
|
-
const { path, method, body, baseUrl, token, headers } = params;
|
|
4056
|
-
try {
|
|
4057
|
-
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
4058
|
-
if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
|
|
4059
|
-
const response = await fetch(url, {
|
|
4060
|
-
method,
|
|
4061
|
-
headers,
|
|
4062
|
-
body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
|
|
4063
|
-
});
|
|
4064
|
-
if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
|
|
4065
|
-
response.baseUrl = baseUrl;
|
|
4066
|
-
response.token = token;
|
|
4067
|
-
if (!response.ok) {
|
|
4068
|
-
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
4069
|
-
let bodyText;
|
|
4070
|
-
try {
|
|
4071
|
-
bodyText = await response.text();
|
|
4072
|
-
} catch (e) {
|
|
4073
|
-
bodyText = void 0;
|
|
4074
|
-
}
|
|
4075
|
-
return await this._handleErrorResponse(
|
|
4076
|
-
params,
|
|
4077
|
-
token,
|
|
4078
|
-
response.status,
|
|
4079
|
-
requestId,
|
|
4080
|
-
this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
|
|
4081
|
-
bodyText,
|
|
4082
|
-
params.retryCount ?? 0
|
|
4083
|
-
);
|
|
4084
|
-
}
|
|
4085
|
-
return response;
|
|
4086
|
-
} catch (error) {
|
|
4087
|
-
if (isNetworkErrorMessage(error?.message || "")) {
|
|
4088
|
-
return await this._handleErrorResponse(
|
|
4089
|
-
params,
|
|
4090
|
-
token,
|
|
4091
|
-
-1,
|
|
4092
|
-
// just for Network Error to skip all statuses
|
|
4093
|
-
void 0,
|
|
4094
|
-
void 0,
|
|
4095
|
-
void 0,
|
|
4096
|
-
params.retryCount ?? 0
|
|
4097
|
-
);
|
|
4098
|
-
}
|
|
4099
|
-
throw error;
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
/**
|
|
4103
|
-
* Handle error responses with failover
|
|
4104
|
-
*/
|
|
4105
|
-
async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
|
|
4106
|
-
const MAX_RETRIES_PER_PROVIDER = 2;
|
|
4107
|
-
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
4108
|
-
let tryNextProvider = false;
|
|
4109
|
-
const errorMessage = responseBody;
|
|
4110
|
-
this._log(
|
|
4111
|
-
"DEBUG",
|
|
4112
|
-
`[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
|
|
4113
|
-
);
|
|
4114
|
-
this._log(
|
|
4115
|
-
"DEBUG",
|
|
4116
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
|
|
4117
|
-
);
|
|
4118
|
-
if (params.token.startsWith("cashu")) {
|
|
4119
|
-
const receiveResult = await this.cashuSpender.receiveToken(
|
|
4120
|
-
params.token
|
|
4121
|
-
);
|
|
4122
|
-
if (receiveResult.success) {
|
|
4123
|
-
this._log(
|
|
4124
|
-
"DEBUG",
|
|
4125
|
-
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
|
|
4126
|
-
);
|
|
4127
|
-
tryNextProvider = true;
|
|
4128
|
-
} else {
|
|
4129
|
-
this._log(
|
|
4130
|
-
"DEBUG",
|
|
4131
|
-
`[RoutstrClient] _handleErrorResponse: Failed to receive token: ${receiveResult.message}`
|
|
4132
|
-
);
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
if (this.mode === "xcashu") {
|
|
4136
|
-
if (xCashuRefundToken) {
|
|
4137
|
-
this._log(
|
|
4138
|
-
"DEBUG",
|
|
4139
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
|
|
4140
|
-
);
|
|
4141
|
-
const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
|
|
4142
|
-
if (receiveResult.success) {
|
|
4143
|
-
this._log(
|
|
4144
|
-
"DEBUG",
|
|
4145
|
-
`[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
|
|
4146
|
-
);
|
|
4147
|
-
tryNextProvider = true;
|
|
4148
|
-
} else {
|
|
4149
|
-
this._log(
|
|
4150
|
-
"ERROR",
|
|
4151
|
-
`[xcashu] Failed to receive refund token: ${receiveResult.message}`
|
|
4152
|
-
);
|
|
4153
|
-
throw new ProviderError(
|
|
4154
|
-
baseUrl,
|
|
4155
|
-
status,
|
|
4156
|
-
"[xcashu] Failed to receive refund token",
|
|
4157
|
-
requestId
|
|
4158
|
-
);
|
|
4159
|
-
}
|
|
4160
|
-
} else {
|
|
4161
|
-
if (!tryNextProvider)
|
|
4162
|
-
throw new ProviderError(
|
|
4163
|
-
baseUrl,
|
|
4164
|
-
status,
|
|
4165
|
-
"[xcashu] Failed to receive refund token",
|
|
4166
|
-
requestId
|
|
4167
|
-
);
|
|
4168
|
-
}
|
|
4169
|
-
}
|
|
4170
|
-
if (status === 402 && !tryNextProvider && this.mode === "apikeys") {
|
|
4171
|
-
this.storageAdapter.getApiKey(baseUrl);
|
|
4172
|
-
let topupAmount = params.requiredSats;
|
|
4173
|
-
try {
|
|
4174
|
-
const currentBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4175
|
-
params.token,
|
|
4176
|
-
baseUrl
|
|
4177
|
-
);
|
|
4178
|
-
const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
|
|
4179
|
-
const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
|
|
4180
|
-
const shortfall = Math.max(0, params.requiredSats - currentBalance + reservedBalance);
|
|
4181
|
-
topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
|
|
4182
|
-
this._log(
|
|
4183
|
-
"DEBUG",
|
|
4184
|
-
`The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
|
|
4185
|
-
);
|
|
4186
|
-
} catch (e) {
|
|
4187
|
-
this._log(
|
|
4188
|
-
"WARN",
|
|
4189
|
-
"Could not get current token balance for topup calculation:",
|
|
4190
|
-
e
|
|
4191
|
-
);
|
|
4192
|
-
}
|
|
4193
|
-
const topupResult = await this.balanceManager.topUp({
|
|
4194
|
-
mintUrl,
|
|
4195
|
-
baseUrl,
|
|
4196
|
-
amount: topupAmount * TOPUP_MARGIN,
|
|
4197
|
-
token: params.token
|
|
4198
|
-
});
|
|
4199
|
-
this._log(
|
|
4200
|
-
"DEBUG",
|
|
4201
|
-
`[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
|
|
4202
|
-
);
|
|
4203
|
-
if (!topupResult.success) {
|
|
4204
|
-
const message = topupResult.message || "";
|
|
4205
|
-
if (message.includes("Insufficient balance")) {
|
|
4206
|
-
const needMatch = message.match(/need (\d+)/);
|
|
4207
|
-
const haveMatch = message.match(/have (\d+)/);
|
|
4208
|
-
const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
|
|
4209
|
-
const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
|
|
4210
|
-
this._log(
|
|
4211
|
-
"DEBUG",
|
|
4212
|
-
`[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
|
|
4213
|
-
);
|
|
4214
|
-
throw new InsufficientBalanceError(
|
|
4215
|
-
required,
|
|
4216
|
-
available,
|
|
4217
|
-
0,
|
|
4218
|
-
"",
|
|
4219
|
-
message
|
|
4220
|
-
);
|
|
4221
|
-
} else {
|
|
4222
|
-
this._log(
|
|
4223
|
-
"DEBUG",
|
|
4224
|
-
`[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
|
|
4225
|
-
);
|
|
4226
|
-
tryNextProvider = true;
|
|
4227
|
-
}
|
|
4228
|
-
} else {
|
|
4229
|
-
this._log(
|
|
4230
|
-
"DEBUG",
|
|
4231
|
-
`[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
|
|
4232
|
-
);
|
|
4233
|
-
}
|
|
4234
|
-
if (!tryNextProvider) {
|
|
4235
|
-
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4236
|
-
this._log(
|
|
4237
|
-
"DEBUG",
|
|
4238
|
-
`[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4239
|
-
);
|
|
4240
|
-
return this._makeRequest({
|
|
4241
|
-
...params,
|
|
4242
|
-
token: params.token,
|
|
4243
|
-
headers: this._withAuthHeader(params.baseHeaders, params.token),
|
|
4244
|
-
retryCount: retryCount + 1
|
|
4245
|
-
});
|
|
4246
|
-
} else {
|
|
4247
|
-
this._log(
|
|
4248
|
-
"DEBUG",
|
|
4249
|
-
`[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4250
|
-
);
|
|
4251
|
-
tryNextProvider = true;
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
}
|
|
4255
|
-
const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
|
|
4256
|
-
if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
|
|
4257
|
-
let retryToken = params.token;
|
|
4258
|
-
try {
|
|
4259
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4260
|
-
params.token,
|
|
4261
|
-
baseUrl
|
|
4262
|
-
);
|
|
4263
|
-
if (latestBalanceInfo.isInvalidApiKey) {
|
|
4264
|
-
this._log(
|
|
4265
|
-
"DEBUG",
|
|
4266
|
-
`[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
|
|
4267
|
-
);
|
|
4268
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
4269
|
-
tryNextProvider = true;
|
|
4270
|
-
} else {
|
|
4271
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
4272
|
-
if (latestBalanceInfo.apiKey) {
|
|
4273
|
-
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
4274
|
-
if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
|
|
4275
|
-
if (storedApiKeyEntry) {
|
|
4276
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
4277
|
-
}
|
|
4278
|
-
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
4279
|
-
}
|
|
4280
|
-
retryToken = latestBalanceInfo.apiKey;
|
|
4281
|
-
}
|
|
4282
|
-
if (latestTokenBalance >= 0) {
|
|
4283
|
-
this.storageAdapter.updateApiKeyBalance(
|
|
4284
|
-
baseUrl,
|
|
4285
|
-
latestTokenBalance
|
|
4286
|
-
);
|
|
4287
|
-
}
|
|
4288
|
-
}
|
|
4289
|
-
} catch (error) {
|
|
4290
|
-
this._log(
|
|
4291
|
-
"WARN",
|
|
4292
|
-
`[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
|
|
4293
|
-
error
|
|
4294
|
-
);
|
|
4295
|
-
}
|
|
4296
|
-
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4297
|
-
this._log(
|
|
4298
|
-
"DEBUG",
|
|
4299
|
-
`[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4300
|
-
);
|
|
4301
|
-
return this._makeRequest({
|
|
4302
|
-
...params,
|
|
4303
|
-
token: retryToken,
|
|
4304
|
-
headers: this._withAuthHeader(params.baseHeaders, retryToken),
|
|
4305
|
-
retryCount: retryCount + 1
|
|
4306
|
-
});
|
|
4307
|
-
} else {
|
|
4308
|
-
this._log(
|
|
4309
|
-
"DEBUG",
|
|
4310
|
-
`[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4311
|
-
);
|
|
4312
|
-
tryNextProvider = true;
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
if (status === 401 && this.mode === "apikeys") {
|
|
4316
|
-
this._log(
|
|
4317
|
-
"DEBUG",
|
|
4318
|
-
`[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
|
|
4319
|
-
);
|
|
4320
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4321
|
-
token,
|
|
4322
|
-
baseUrl
|
|
4323
|
-
);
|
|
4324
|
-
if (latestBalanceInfo.isInvalidApiKey) {
|
|
4325
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
4326
|
-
tryNextProvider = true;
|
|
4327
|
-
}
|
|
4328
|
-
}
|
|
4329
|
-
if ((status === 401 || status === 403 || status === 404 || status === 413 || status === 400 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
|
|
4330
|
-
this._log(
|
|
4331
|
-
"DEBUG",
|
|
4332
|
-
`[RoutstrClient] _handleErrorResponse: Status ${status} (${status === 429 ? "rate limited" : "auth/server error"}), attempting refund for ${baseUrl}, mode=${this.mode}`
|
|
4333
|
-
);
|
|
4334
|
-
if (this.mode === "apikeys") {
|
|
4335
|
-
this._log(
|
|
4336
|
-
"DEBUG",
|
|
4337
|
-
`[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
|
|
4338
|
-
);
|
|
4339
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4340
|
-
token,
|
|
4341
|
-
baseUrl
|
|
4342
|
-
);
|
|
4343
|
-
this._log(
|
|
4344
|
-
"DEBUG",
|
|
4345
|
-
`[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
|
|
4346
|
-
);
|
|
4347
|
-
const refundResult = await this.balanceManager.refundApiKey({
|
|
4348
|
-
mintUrl,
|
|
4349
|
-
baseUrl,
|
|
4350
|
-
apiKey: token,
|
|
4351
|
-
forceRefund: true
|
|
4352
|
-
});
|
|
4353
|
-
this._log(
|
|
4354
|
-
"DEBUG",
|
|
4355
|
-
`[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
|
|
4356
|
-
);
|
|
4357
|
-
if (!refundResult.success && latestBalanceInfo.amount > 0) {
|
|
4358
|
-
throw new ProviderError(
|
|
4359
|
-
baseUrl,
|
|
4360
|
-
status,
|
|
4361
|
-
refundResult.message ?? "Unknown error"
|
|
4362
|
-
);
|
|
4363
|
-
}
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
this.providerManager.markFailed(baseUrl);
|
|
4367
|
-
this._log(
|
|
4368
|
-
"DEBUG",
|
|
4369
|
-
`[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
|
|
4370
|
-
);
|
|
4371
|
-
if (!selectedModel) {
|
|
4372
|
-
throw new ProviderError(
|
|
4373
|
-
baseUrl,
|
|
4374
|
-
status,
|
|
4375
|
-
"Funny, no selected model. HMM. "
|
|
4376
|
-
);
|
|
4377
|
-
}
|
|
4378
|
-
const nextProvider = this.providerManager.findNextBestProvider(
|
|
4379
|
-
selectedModel.id,
|
|
4380
|
-
baseUrl
|
|
4381
|
-
);
|
|
4382
|
-
if (nextProvider) {
|
|
4383
|
-
this._log(
|
|
4384
|
-
"DEBUG",
|
|
4385
|
-
`[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
|
|
4386
|
-
);
|
|
4387
|
-
const newModel = await this.providerManager.getModelForProvider(
|
|
4388
|
-
nextProvider,
|
|
4389
|
-
selectedModel.id
|
|
4390
|
-
) ?? selectedModel;
|
|
4391
|
-
const messagesForPricing = Array.isArray(
|
|
4392
|
-
body?.messages
|
|
4393
|
-
) ? body.messages : [];
|
|
4394
|
-
const newRequiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4395
|
-
newModel,
|
|
4396
|
-
messagesForPricing,
|
|
4397
|
-
params.maxTokens
|
|
4398
|
-
);
|
|
4399
|
-
this._log(
|
|
4400
|
-
"DEBUG",
|
|
4401
|
-
`[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
|
|
4402
|
-
);
|
|
4403
|
-
const spendResult = await this._spendToken({
|
|
4404
|
-
mintUrl,
|
|
4405
|
-
amount: newRequiredSats,
|
|
4406
|
-
baseUrl: nextProvider
|
|
4407
|
-
});
|
|
4408
|
-
return this._makeRequest({
|
|
4409
|
-
...params,
|
|
4410
|
-
path,
|
|
4411
|
-
method,
|
|
4412
|
-
body,
|
|
4413
|
-
baseUrl: nextProvider,
|
|
4414
|
-
selectedModel: newModel,
|
|
4415
|
-
token: spendResult.token,
|
|
4416
|
-
requiredSats: newRequiredSats,
|
|
4417
|
-
headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
|
|
4418
|
-
retryCount: 0
|
|
4419
|
-
});
|
|
4420
|
-
}
|
|
4421
|
-
throw new FailoverError(
|
|
4422
|
-
baseUrl,
|
|
4423
|
-
Array.from(this.providerManager.getFailedProviders())
|
|
4424
|
-
);
|
|
4425
|
-
}
|
|
4426
|
-
/**
|
|
4427
|
-
* Handle post-response balance update for all modes
|
|
4428
|
-
*/
|
|
4429
|
-
async _handlePostResponseBalanceUpdate(params) {
|
|
4430
|
-
const {
|
|
4431
|
-
token,
|
|
4432
|
-
baseUrl,
|
|
4433
|
-
mintUrl,
|
|
4434
|
-
initialTokenBalance,
|
|
4435
|
-
fallbackSatsSpent,
|
|
4436
|
-
response,
|
|
4437
|
-
modelId,
|
|
4438
|
-
usage,
|
|
4439
|
-
requestId,
|
|
4440
|
-
clientApiKey
|
|
4441
|
-
} = params;
|
|
4442
|
-
let satsSpent = initialTokenBalance;
|
|
4443
|
-
if (this.mode === "xcashu" && response) {
|
|
4444
|
-
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
4445
|
-
if (refundToken) {
|
|
4446
|
-
const receiveResult = await this.cashuSpender.receiveToken(refundToken);
|
|
4447
|
-
if (receiveResult.success) {
|
|
4448
|
-
this.storageAdapter.removeXcashuToken(baseUrl, token);
|
|
4449
|
-
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
4450
|
-
} else {
|
|
4451
|
-
this._log(
|
|
4452
|
-
"ERROR",
|
|
4453
|
-
`[xcashu] Failed to receive refund token: ${receiveResult.message}`
|
|
4454
|
-
);
|
|
4455
|
-
}
|
|
4456
|
-
}
|
|
4457
|
-
} else if (this.mode === "apikeys") {
|
|
4458
|
-
try {
|
|
4459
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4460
|
-
token,
|
|
4461
|
-
baseUrl
|
|
4462
|
-
);
|
|
4463
|
-
this._log(
|
|
4464
|
-
"DEBUG",
|
|
4465
|
-
"LATEST Balance",
|
|
4466
|
-
latestBalanceInfo.amount,
|
|
4467
|
-
latestBalanceInfo.reserved,
|
|
4468
|
-
latestBalanceInfo.apiKey,
|
|
4469
|
-
baseUrl
|
|
4470
|
-
);
|
|
4471
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
4472
|
-
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
4473
|
-
if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
|
|
4474
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
4475
|
-
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
4476
|
-
}
|
|
4477
|
-
this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
|
|
4478
|
-
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
4479
|
-
} catch (e) {
|
|
4480
|
-
this._log("WARN", "Could not get updated API key balance:", e);
|
|
4481
|
-
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
4482
|
-
}
|
|
4483
|
-
}
|
|
4484
|
-
await this._trackResponseUsage({
|
|
4485
|
-
token,
|
|
4486
|
-
baseUrl,
|
|
4487
|
-
response,
|
|
4488
|
-
modelId,
|
|
4489
|
-
satsSpent,
|
|
4490
|
-
usage,
|
|
4491
|
-
requestId,
|
|
4492
|
-
clientApiKey
|
|
4493
|
-
});
|
|
4494
|
-
(async () => {
|
|
4495
|
-
})();
|
|
4496
|
-
return satsSpent;
|
|
4497
|
-
}
|
|
4498
|
-
async _trackResponseUsage(params) {
|
|
4499
|
-
const {
|
|
4500
|
-
token,
|
|
4501
|
-
baseUrl,
|
|
4502
|
-
response,
|
|
4503
|
-
modelId,
|
|
4504
|
-
satsSpent,
|
|
4505
|
-
usage: providedUsage,
|
|
4506
|
-
requestId: providedRequestId,
|
|
4507
|
-
clientApiKey
|
|
4508
|
-
} = params;
|
|
4509
|
-
if (!response || !modelId) {
|
|
4510
|
-
return;
|
|
4511
|
-
}
|
|
4512
|
-
try {
|
|
4513
|
-
let usage = providedUsage;
|
|
4514
|
-
let requestId = providedRequestId;
|
|
4515
|
-
if (!usage || !requestId) {
|
|
4516
|
-
const contentType = response.headers.get("content-type") || "";
|
|
4517
|
-
if (contentType.includes("text/event-stream")) {
|
|
4518
|
-
usage = usage ?? response.usage;
|
|
4519
|
-
requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
4520
|
-
if (!usage) {
|
|
4521
|
-
return;
|
|
4522
|
-
}
|
|
4523
|
-
} else {
|
|
4524
|
-
const cloned = response.clone();
|
|
4525
|
-
const responseBody = await cloned.json();
|
|
4526
|
-
usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
|
|
4527
|
-
requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
if (!usage) {
|
|
4531
|
-
return;
|
|
4532
|
-
}
|
|
4533
|
-
const finalRequestId = requestId || "unknown";
|
|
4534
|
-
const store = this.sdkStore ?? await getDefaultSdkStore();
|
|
4535
|
-
const state = store.getState();
|
|
4536
|
-
const matchKey = clientApiKey ?? token;
|
|
4537
|
-
const matchingClient = state.clientIds.find(
|
|
4538
|
-
(client) => client.apiKey === matchKey
|
|
4539
|
-
);
|
|
4540
|
-
const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
|
|
4541
|
-
const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
|
|
4542
|
-
const entry = {
|
|
4543
|
-
id: entryId,
|
|
4544
|
-
timestamp: Date.now(),
|
|
4545
|
-
modelId,
|
|
4546
|
-
baseUrl,
|
|
4547
|
-
requestId: finalRequestId,
|
|
4548
|
-
client: matchingClient?.clientId,
|
|
4549
|
-
...usage
|
|
4550
|
-
};
|
|
4551
|
-
if (this.mode === "xcashu") {
|
|
4552
|
-
entry.satsCost = satsSpent;
|
|
4553
|
-
}
|
|
4554
|
-
await usageTracking.append(entry);
|
|
4555
|
-
} catch (error) {
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
/**
|
|
4559
|
-
* Convert messages for API format
|
|
4560
|
-
*/
|
|
4561
|
-
async _convertMessages(messages) {
|
|
4562
|
-
return Promise.all(
|
|
4563
|
-
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
4564
|
-
role: m.role,
|
|
4565
|
-
content: typeof m.content === "string" ? m.content : m.content
|
|
4566
|
-
}))
|
|
4567
|
-
);
|
|
4568
|
-
}
|
|
4569
|
-
/**
|
|
4570
|
-
* Create assistant message from streaming result
|
|
4571
|
-
*/
|
|
4572
|
-
async _createAssistantMessage(result) {
|
|
4573
|
-
if (result.images && result.images.length > 0) {
|
|
4574
|
-
const content = [];
|
|
4575
|
-
if (result.content) {
|
|
4576
|
-
content.push({
|
|
4577
|
-
type: "text",
|
|
4578
|
-
text: result.content,
|
|
4579
|
-
thinking: result.thinking,
|
|
4580
|
-
citations: result.citations,
|
|
4581
|
-
annotations: result.annotations
|
|
4582
|
-
});
|
|
4583
|
-
}
|
|
4584
|
-
for (const img of result.images) {
|
|
4585
|
-
content.push({
|
|
4586
|
-
type: "image_url",
|
|
4587
|
-
image_url: {
|
|
4588
|
-
url: img.image_url.url
|
|
4589
|
-
}
|
|
4590
|
-
});
|
|
4591
|
-
}
|
|
4592
|
-
return {
|
|
4593
|
-
role: "assistant",
|
|
4594
|
-
content
|
|
4595
|
-
};
|
|
4596
|
-
}
|
|
4597
|
-
return {
|
|
4598
|
-
role: "assistant",
|
|
4599
|
-
content: result.content || ""
|
|
4600
|
-
};
|
|
4601
|
-
}
|
|
4602
|
-
/**
|
|
4603
|
-
* Calculate estimated costs from usage
|
|
4604
|
-
*/
|
|
4605
|
-
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
4606
|
-
let estimatedCosts = 0;
|
|
4607
|
-
if (streamingResult.usage) {
|
|
4608
|
-
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
4609
|
-
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
4610
|
-
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
return estimatedCosts;
|
|
4614
|
-
}
|
|
4615
|
-
/**
|
|
4616
|
-
* Get pending API key amount
|
|
4617
|
-
*/
|
|
4618
|
-
_getPendingCashuTokenAmount() {
|
|
4619
|
-
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
4620
|
-
return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
|
|
4621
|
-
}
|
|
4622
|
-
/**
|
|
4623
|
-
* Handle errors and notify callbacks
|
|
4624
|
-
*/
|
|
4625
|
-
_handleError(error, callbacks) {
|
|
4626
|
-
this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
|
|
4627
|
-
if (error instanceof Error) {
|
|
4628
|
-
const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
|
|
4629
|
-
const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
|
|
4630
|
-
this._log(
|
|
4631
|
-
"ERROR",
|
|
4632
|
-
`[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
|
|
4633
|
-
);
|
|
4634
|
-
callbacks.onMessageAppend({
|
|
4635
|
-
role: "system",
|
|
4636
|
-
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
4637
|
-
});
|
|
4638
|
-
} else {
|
|
4639
|
-
callbacks.onMessageAppend({
|
|
4640
|
-
role: "system",
|
|
4641
|
-
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4645
|
-
/**
|
|
4646
|
-
* Check wallet balance and throw if insufficient
|
|
4647
|
-
*/
|
|
4648
|
-
async _checkBalance() {
|
|
4649
|
-
const balances = await this.walletAdapter.getBalances();
|
|
4650
|
-
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
4651
|
-
if (totalBalance <= 0) {
|
|
4652
|
-
throw new InsufficientBalanceError(1, 0);
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
4655
|
-
/**
|
|
4656
|
-
* Spend a token using CashuSpender with standardized error handling
|
|
4657
|
-
*/
|
|
4658
|
-
async _spendToken(params) {
|
|
4659
|
-
const { mintUrl, amount, baseUrl } = params;
|
|
4660
|
-
this._log(
|
|
4661
|
-
"DEBUG",
|
|
4662
|
-
`[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
|
|
4663
|
-
);
|
|
4664
|
-
if (this.mode === "apikeys") {
|
|
4665
|
-
let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
4666
|
-
if (!parentApiKey) {
|
|
4667
|
-
this._log(
|
|
4668
|
-
"DEBUG",
|
|
4669
|
-
`[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
|
|
4670
|
-
);
|
|
4671
|
-
const spendResult2 = await this.cashuSpender.spend({
|
|
4672
|
-
mintUrl,
|
|
4673
|
-
amount: amount * TOPUP_MARGIN,
|
|
4674
|
-
baseUrl: "",
|
|
4675
|
-
reuseToken: false
|
|
4676
|
-
});
|
|
4677
|
-
if (!spendResult2.token) {
|
|
4678
|
-
this._log(
|
|
4679
|
-
"ERROR",
|
|
4680
|
-
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
|
|
4681
|
-
spendResult2.error
|
|
4682
|
-
);
|
|
4683
|
-
throw new Error(
|
|
4684
|
-
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
|
|
4685
|
-
);
|
|
4686
|
-
} else {
|
|
4687
|
-
this._log(
|
|
4688
|
-
"DEBUG",
|
|
4689
|
-
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
|
|
4690
|
-
);
|
|
4691
|
-
}
|
|
4692
|
-
this._log(
|
|
4693
|
-
"DEBUG",
|
|
4694
|
-
`[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
|
|
4695
|
-
);
|
|
4696
|
-
try {
|
|
4697
|
-
this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
|
|
4698
|
-
} catch (error) {
|
|
4699
|
-
if (error instanceof Error && error.message.includes("ApiKey already exists")) {
|
|
4700
|
-
const receiveResult = await this.cashuSpender.receiveToken(
|
|
4701
|
-
spendResult2.token
|
|
4702
|
-
);
|
|
4703
|
-
if (receiveResult.success) {
|
|
4704
|
-
this._log(
|
|
4705
|
-
"DEBUG",
|
|
4706
|
-
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
|
|
4707
|
-
);
|
|
4708
|
-
} else {
|
|
4709
|
-
this._log(
|
|
4710
|
-
"DEBUG",
|
|
4711
|
-
`[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
|
|
4712
|
-
);
|
|
4713
|
-
}
|
|
4714
|
-
this._log(
|
|
4715
|
-
"DEBUG",
|
|
4716
|
-
`[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
|
|
4717
|
-
);
|
|
4718
|
-
} else {
|
|
4719
|
-
throw error;
|
|
4720
|
-
}
|
|
4721
|
-
}
|
|
4722
|
-
parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
4723
|
-
} else {
|
|
4724
|
-
this._log(
|
|
4725
|
-
"DEBUG",
|
|
4726
|
-
`[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
|
|
4727
|
-
);
|
|
4728
|
-
}
|
|
4729
|
-
let tokenBalance = 0;
|
|
4730
|
-
let tokenBalanceUnit = "sat";
|
|
4731
|
-
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
4732
|
-
const distributionForBaseUrl = apiKeyDistribution.find(
|
|
4733
|
-
(d) => d.baseUrl === baseUrl
|
|
4734
|
-
);
|
|
4735
|
-
if (distributionForBaseUrl) {
|
|
4736
|
-
tokenBalance = distributionForBaseUrl.amount;
|
|
4737
|
-
}
|
|
4738
|
-
if (tokenBalance === 0 && parentApiKey) {
|
|
4739
|
-
try {
|
|
4740
|
-
const balanceInfo = await this.balanceManager.getTokenBalance(
|
|
4741
|
-
parentApiKey.key,
|
|
4742
|
-
baseUrl
|
|
4743
|
-
);
|
|
4744
|
-
tokenBalance = balanceInfo.amount;
|
|
4745
|
-
tokenBalanceUnit = balanceInfo.unit;
|
|
4746
|
-
} catch (e) {
|
|
4747
|
-
this._log("WARN", "Could not get initial API key balance:", e);
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
this._log(
|
|
4751
|
-
"DEBUG",
|
|
4752
|
-
`[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
|
|
4753
|
-
);
|
|
4754
|
-
return {
|
|
4755
|
-
token: parentApiKey?.key ?? "",
|
|
4756
|
-
tokenBalance,
|
|
4757
|
-
tokenBalanceUnit
|
|
4758
|
-
};
|
|
4759
|
-
}
|
|
4760
|
-
this._log(
|
|
4761
|
-
"DEBUG",
|
|
4762
|
-
`[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
|
|
4763
|
-
);
|
|
4764
|
-
const spendResult = await this.cashuSpender.spend({
|
|
4765
|
-
mintUrl,
|
|
4766
|
-
amount,
|
|
4767
|
-
baseUrl: "",
|
|
4768
|
-
reuseToken: false
|
|
4769
|
-
});
|
|
4770
|
-
if (!spendResult.token) {
|
|
4771
|
-
this._log(
|
|
4772
|
-
"ERROR",
|
|
4773
|
-
`[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
|
|
4774
|
-
spendResult.error
|
|
4775
|
-
);
|
|
4776
|
-
} else {
|
|
4777
|
-
this._log(
|
|
4778
|
-
"DEBUG",
|
|
4779
|
-
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
|
|
4780
|
-
);
|
|
4781
|
-
this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
|
|
4782
|
-
}
|
|
4783
|
-
return {
|
|
4784
|
-
token: spendResult.token,
|
|
4785
|
-
tokenBalance: spendResult.balance,
|
|
4786
|
-
tokenBalanceUnit: spendResult.unit ?? "sat"
|
|
4787
|
-
};
|
|
4788
|
-
}
|
|
4789
|
-
/**
|
|
4790
|
-
* Build request headers with common defaults and dev mock controls
|
|
4791
|
-
*/
|
|
4792
|
-
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
4793
|
-
const headers = {
|
|
4794
|
-
...additionalHeaders,
|
|
4795
|
-
"Content-Type": "application/json"
|
|
4796
|
-
};
|
|
4797
|
-
return headers;
|
|
4798
|
-
}
|
|
4799
|
-
/**
|
|
4800
|
-
* Attach auth headers using the active client mode
|
|
4801
|
-
*/
|
|
4802
|
-
_withAuthHeader(headers, token) {
|
|
4803
|
-
const nextHeaders = { ...headers };
|
|
4804
|
-
if (this.mode === "xcashu") {
|
|
4805
|
-
nextHeaders["X-Cashu"] = token;
|
|
4806
|
-
} else {
|
|
4807
|
-
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
4808
|
-
}
|
|
4809
|
-
return nextHeaders;
|
|
4810
|
-
}
|
|
4811
|
-
};
|
|
4812
|
-
|
|
4813
|
-
exports.ProviderManager = ProviderManager;
|
|
4814
|
-
exports.RoutstrClient = RoutstrClient;
|
|
4815
|
-
exports.StreamProcessor = StreamProcessor;
|
|
4816
|
-
exports.createSSEParserTransform = createSSEParserTransform;
|
|
4817
|
-
exports.inspectSSEWebStream = inspectSSEWebStream;
|
|
4818
|
-
//# sourceMappingURL=index.js.map
|
|
4819
|
-
//# sourceMappingURL=index.js.map
|