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