@routstr/sdk 0.3.7 → 0.3.9
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 +4938 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +4932 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/discovery/index.d.mts +247 -0
- package/dist/discovery/index.d.ts +247 -0
- package/dist/discovery/index.js +894 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/index.mjs +891 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/index.d.mts +195 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +6797 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6746 -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 +113 -0
- package/dist/storage/index.d.ts +113 -0
- package/dist/storage/index.js +2019 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +1995 -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 +249 -0
- package/dist/wallet/index.d.ts +249 -0
- package/dist/wallet/index.js +1383 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/index.mjs +1380 -0
- package/dist/wallet/index.mjs.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var applesauceRelay = require('applesauce-relay');
|
|
4
|
+
var applesauceCore = require('applesauce-core');
|
|
5
|
+
var rxjs = require('rxjs');
|
|
6
|
+
|
|
7
|
+
// core/types.ts
|
|
8
|
+
function makeConsoleLogger(prefix) {
|
|
9
|
+
const fmt = (args) => prefix ? [prefix, ...args] : args;
|
|
10
|
+
return {
|
|
11
|
+
log: (...args) => console.log(...fmt(args)),
|
|
12
|
+
warn: (...args) => console.warn(...fmt(args)),
|
|
13
|
+
error: (...args) => console.error(...fmt(args)),
|
|
14
|
+
debug: (...args) => console.log(...fmt(args)),
|
|
15
|
+
child: (p) => makeConsoleLogger(prefix ? `${prefix}:${p}` : p)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var consoleLogger = makeConsoleLogger();
|
|
19
|
+
|
|
20
|
+
// core/errors.ts
|
|
21
|
+
var ProviderBootstrapError = class extends Error {
|
|
22
|
+
constructor(failedProviders, message) {
|
|
23
|
+
super(
|
|
24
|
+
message || `Failed to bootstrap providers. Tried: ${failedProviders.join(", ")}`
|
|
25
|
+
);
|
|
26
|
+
this.failedProviders = failedProviders;
|
|
27
|
+
this.name = "ProviderBootstrapError";
|
|
28
|
+
}
|
|
29
|
+
failedProviders;
|
|
30
|
+
};
|
|
31
|
+
var NoProvidersAvailableError = class extends Error {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("No providers are available for model discovery");
|
|
34
|
+
this.name = "NoProvidersAvailableError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function isBunRuntime() {
|
|
38
|
+
return typeof Bun !== "undefined";
|
|
39
|
+
}
|
|
40
|
+
var ModelManager = class _ModelManager {
|
|
41
|
+
constructor(adapter, config = {}) {
|
|
42
|
+
this.adapter = adapter;
|
|
43
|
+
this.providerDirectoryUrl = config.providerDirectoryUrl || "https://api.routstr.com/v1/providers/";
|
|
44
|
+
this.cacheTTL = config.cacheTTL || 210 * 60 * 1e3;
|
|
45
|
+
this.includeProviderUrls = config.includeProviderUrls || [];
|
|
46
|
+
this.excludeProviderUrls = config.excludeProviderUrls || [];
|
|
47
|
+
this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
|
|
48
|
+
this.logger = (config.logger ?? consoleLogger).child("ModelManager");
|
|
49
|
+
this.eventStoreDbPath = config.eventStoreDbPath;
|
|
50
|
+
}
|
|
51
|
+
adapter;
|
|
52
|
+
cacheTTL;
|
|
53
|
+
providerDirectoryUrl;
|
|
54
|
+
includeProviderUrls;
|
|
55
|
+
excludeProviderUrls;
|
|
56
|
+
routstrPubkey;
|
|
57
|
+
logger;
|
|
58
|
+
providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
|
|
59
|
+
/** Persistent event store for relay-fetched events (null if not configured/initialized) */
|
|
60
|
+
eventStore = null;
|
|
61
|
+
eventStoreDb = null;
|
|
62
|
+
eventStoreInitPromise = null;
|
|
63
|
+
eventStoreDbPath;
|
|
64
|
+
/**
|
|
65
|
+
* Get the list of bootstrapped provider base URLs
|
|
66
|
+
* @returns Array of provider base URLs
|
|
67
|
+
*/
|
|
68
|
+
getBaseUrls() {
|
|
69
|
+
return this.adapter.getBaseUrlsList();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Lazily initialize the persistent event store.
|
|
73
|
+
* Returns null if no eventStoreDbPath was provided.
|
|
74
|
+
*/
|
|
75
|
+
async ensureEventStore() {
|
|
76
|
+
if (!this.eventStoreDbPath) return null;
|
|
77
|
+
if (this.eventStore) return this.eventStore;
|
|
78
|
+
if (!this.eventStoreInitPromise) {
|
|
79
|
+
this.eventStoreInitPromise = (async () => {
|
|
80
|
+
try {
|
|
81
|
+
const db = await this.createPersistentEventDatabase();
|
|
82
|
+
this.eventStoreDb = db;
|
|
83
|
+
this.eventStore = new applesauceCore.EventStore({ database: db });
|
|
84
|
+
this.initializeEventStoreMetadata();
|
|
85
|
+
this.logger.log(
|
|
86
|
+
`Persistent event store initialized at ${this.eventStoreDbPath}`
|
|
87
|
+
);
|
|
88
|
+
return this.eventStore;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.eventStoreInitPromise = null;
|
|
91
|
+
throw new Error(
|
|
92
|
+
`applesauce-sqlite with a supported SQLite driver is required for persistent Nostr event storage. Bun uses bun:sqlite; Node.js uses better-sqlite3. Install optional dependencies or omit eventStoreDbPath. (${error})`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
}
|
|
97
|
+
return this.eventStoreInitPromise;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the persistent event store, initializing it if configured.
|
|
101
|
+
* Returns null if no eventStoreDbPath was provided.
|
|
102
|
+
*/
|
|
103
|
+
async getEventStore() {
|
|
104
|
+
return this.ensureEventStore();
|
|
105
|
+
}
|
|
106
|
+
async createPersistentEventDatabase() {
|
|
107
|
+
if (isBunRuntime()) {
|
|
108
|
+
const { BunSqliteEventDatabase } = await import('applesauce-sqlite/bun');
|
|
109
|
+
return new BunSqliteEventDatabase(
|
|
110
|
+
this.eventStoreDbPath
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const { BetterSqlite3EventDatabase } = await import('applesauce-sqlite/better-sqlite3');
|
|
114
|
+
return new BetterSqlite3EventDatabase(
|
|
115
|
+
this.eventStoreDbPath
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
/** Close the persistent event store database handle, if configured. */
|
|
119
|
+
closeEventStore() {
|
|
120
|
+
this.eventStoreDb?.close?.();
|
|
121
|
+
this.eventStore = null;
|
|
122
|
+
this.eventStoreDb = null;
|
|
123
|
+
this.eventStoreInitPromise = null;
|
|
124
|
+
}
|
|
125
|
+
initializeEventStoreMetadata() {
|
|
126
|
+
this.eventStoreDb?.db?.exec(
|
|
127
|
+
`CREATE TABLE IF NOT EXISTS routstr_event_cache_metadata (
|
|
128
|
+
event_id TEXT PRIMARY KEY,
|
|
129
|
+
fetched_at INTEGER NOT NULL
|
|
130
|
+
)`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
markEventFetched(event, fetchedAt = Date.now()) {
|
|
134
|
+
const db = this.eventStoreDb?.db;
|
|
135
|
+
if (!db) return;
|
|
136
|
+
db.prepare(
|
|
137
|
+
`INSERT INTO routstr_event_cache_metadata (event_id, fetched_at)
|
|
138
|
+
VALUES (?, ?)
|
|
139
|
+
ON CONFLICT(event_id) DO UPDATE SET fetched_at = excluded.fetched_at`
|
|
140
|
+
).run?.(event.id, fetchedAt);
|
|
141
|
+
}
|
|
142
|
+
getEventFetchedAt(event) {
|
|
143
|
+
const db = this.eventStoreDb?.db;
|
|
144
|
+
if (!db) return void 0;
|
|
145
|
+
const row = db.prepare(
|
|
146
|
+
`SELECT fetched_at FROM routstr_event_cache_metadata WHERE event_id = ?`
|
|
147
|
+
).get?.(event.id);
|
|
148
|
+
return typeof row?.fetched_at === "number" ? row.fetched_at : void 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check the persistent event store for fresh cached events.
|
|
152
|
+
* Returns events from SQLite if they were fetched within `maxAge`, otherwise
|
|
153
|
+
* returns empty array (caller should hit relays). Events without local fetch
|
|
154
|
+
* metadata fall back to Nostr created_at for backwards compatibility.
|
|
155
|
+
*/
|
|
156
|
+
async getCachedNostrEvents(filter, maxAge, forceRefresh = false) {
|
|
157
|
+
const eventStore = await this.ensureEventStore();
|
|
158
|
+
if (forceRefresh) return [];
|
|
159
|
+
if (!eventStore) return [];
|
|
160
|
+
const timeline = eventStore.getTimeline(filter);
|
|
161
|
+
if (timeline.length === 0) return [];
|
|
162
|
+
const cutoff = Date.now() - maxAge;
|
|
163
|
+
const freshest = Math.max(
|
|
164
|
+
...timeline.map((e) => this.getEventFetchedAt(e) ?? e.created_at * 1e3)
|
|
165
|
+
);
|
|
166
|
+
if (freshest < cutoff) return [];
|
|
167
|
+
return timeline;
|
|
168
|
+
}
|
|
169
|
+
static async init(adapter, config = {}, options = {}) {
|
|
170
|
+
const manager = new _ModelManager(adapter, config);
|
|
171
|
+
const torMode = options.torMode ?? false;
|
|
172
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
173
|
+
const providers = await manager.bootstrapProviders(torMode, forceRefresh);
|
|
174
|
+
await manager.fetchModels(providers, forceRefresh);
|
|
175
|
+
return manager;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Bootstrap provider list from the provider directory
|
|
179
|
+
* First tries to fetch from Nostr (kind 30421), falls back to HTTP
|
|
180
|
+
* @param torMode Whether running in Tor context
|
|
181
|
+
* @param forceRefresh Ignore provider cache and refresh provider sources
|
|
182
|
+
* @returns Array of provider base URLs
|
|
183
|
+
* @throws ProviderBootstrapError if all providers fail to fetch
|
|
184
|
+
*/
|
|
185
|
+
async bootstrapProviders(torMode = false, forceRefresh = false) {
|
|
186
|
+
if (!forceRefresh) {
|
|
187
|
+
const cachedUrls = this.adapter.getBaseUrlsList();
|
|
188
|
+
if (cachedUrls.length > 0) {
|
|
189
|
+
const lastUpdate = this.adapter.getBaseUrlsLastUpdate();
|
|
190
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
191
|
+
if (cacheValid) {
|
|
192
|
+
const filteredCachedUrls = this.filterBaseUrlsForTor(
|
|
193
|
+
cachedUrls,
|
|
194
|
+
torMode
|
|
195
|
+
);
|
|
196
|
+
await this.fetchRoutstr21Models(forceRefresh);
|
|
197
|
+
await this.syncReviewedProvidersFromNostr(
|
|
198
|
+
filteredCachedUrls,
|
|
199
|
+
this.providerNodePubkeysByUrl,
|
|
200
|
+
forceRefresh
|
|
201
|
+
);
|
|
202
|
+
return filteredCachedUrls;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const nostrProviders = await this.bootstrapFromNostr(
|
|
208
|
+
38421,
|
|
209
|
+
torMode,
|
|
210
|
+
forceRefresh
|
|
211
|
+
);
|
|
212
|
+
if (nostrProviders.length > 0) {
|
|
213
|
+
const filtered = this.filterBaseUrlsForTor(nostrProviders, torMode);
|
|
214
|
+
this.adapter.setBaseUrlsList(filtered);
|
|
215
|
+
this.adapter.setBaseUrlsLastUpdate(Date.now());
|
|
216
|
+
await this.fetchRoutstr21Models(forceRefresh);
|
|
217
|
+
await this.syncReviewedProvidersFromNostr(
|
|
218
|
+
filtered,
|
|
219
|
+
this.providerNodePubkeysByUrl,
|
|
220
|
+
forceRefresh
|
|
221
|
+
);
|
|
222
|
+
return filtered;
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
this.logger.warn("Nostr bootstrap failed, falling back to HTTP:", e);
|
|
226
|
+
}
|
|
227
|
+
return this.bootstrapFromHttp(torMode, forceRefresh);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Bootstrap providers from Nostr network (kind 38421)
|
|
231
|
+
* @param kind The Nostr kind to fetch
|
|
232
|
+
* @param torMode Whether running in Tor context
|
|
233
|
+
* @returns Array of provider base URLs
|
|
234
|
+
*/
|
|
235
|
+
async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
|
|
236
|
+
const DEFAULT_RELAYS = [
|
|
237
|
+
"wss://relay.primal.net",
|
|
238
|
+
"wss://nos.lol",
|
|
239
|
+
"wss://relay.damus.io"
|
|
240
|
+
];
|
|
241
|
+
const cached = await this.getCachedNostrEvents(
|
|
242
|
+
{ kinds: [kind] },
|
|
243
|
+
this.cacheTTL,
|
|
244
|
+
forceRefresh
|
|
245
|
+
);
|
|
246
|
+
let sessionEvents = cached;
|
|
247
|
+
if (cached.length === 0) {
|
|
248
|
+
const pool = new applesauceRelay.RelayPool();
|
|
249
|
+
const timeoutMs = 5e3;
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
pool.req(DEFAULT_RELAYS, {
|
|
252
|
+
kinds: [kind],
|
|
253
|
+
limit: 100
|
|
254
|
+
}).pipe(
|
|
255
|
+
applesauceRelay.onlyEvents(),
|
|
256
|
+
rxjs.tap((event) => {
|
|
257
|
+
sessionEvents.push(event);
|
|
258
|
+
this.eventStore?.add(event);
|
|
259
|
+
this.markEventFetched(event);
|
|
260
|
+
})
|
|
261
|
+
).subscribe({
|
|
262
|
+
complete: () => {
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
resolve();
|
|
268
|
+
}, timeoutMs);
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
this.logger.log(`Using ${cached.length} cached kind ${kind} events from persistent store`);
|
|
272
|
+
}
|
|
273
|
+
const bases = /* @__PURE__ */ new Set();
|
|
274
|
+
this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
|
|
275
|
+
for (const event of sessionEvents) {
|
|
276
|
+
const eventUrls = [];
|
|
277
|
+
for (const tag of event.tags) {
|
|
278
|
+
if (tag[0] === "u" && typeof tag[1] === "string") {
|
|
279
|
+
eventUrls.push(tag[1]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (eventUrls.length > 0) {
|
|
283
|
+
for (const url of eventUrls) {
|
|
284
|
+
const normalized = this.normalizeUrl(url);
|
|
285
|
+
if (!torMode || normalized.includes(".onion")) {
|
|
286
|
+
bases.add(normalized);
|
|
287
|
+
this.addProviderNode(
|
|
288
|
+
this.providerNodePubkeysByUrl,
|
|
289
|
+
normalized,
|
|
290
|
+
event.pubkey
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const content = JSON.parse(event.content);
|
|
298
|
+
const providers = Array.isArray(content) ? content : content.providers || [];
|
|
299
|
+
for (const p of providers) {
|
|
300
|
+
const endpoints = this.getProviderEndpoints(p, torMode);
|
|
301
|
+
for (const endpoint of endpoints) {
|
|
302
|
+
bases.add(endpoint);
|
|
303
|
+
this.addProviderNode(
|
|
304
|
+
this.providerNodePubkeysByUrl,
|
|
305
|
+
endpoint,
|
|
306
|
+
p?.pubkey || event.pubkey
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
try {
|
|
312
|
+
const providers = JSON.parse(event.content);
|
|
313
|
+
if (Array.isArray(providers)) {
|
|
314
|
+
for (const p of providers) {
|
|
315
|
+
const endpoints = this.getProviderEndpoints(p, torMode);
|
|
316
|
+
for (const endpoint of endpoints) {
|
|
317
|
+
bases.add(endpoint);
|
|
318
|
+
this.addProviderNode(
|
|
319
|
+
this.providerNodePubkeysByUrl,
|
|
320
|
+
endpoint,
|
|
321
|
+
p?.pubkey || event.pubkey
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
this.logger.warn(
|
|
328
|
+
"NostrBootstrap: failed to parse event content:",
|
|
329
|
+
event.id
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const url of this.includeProviderUrls) {
|
|
335
|
+
const normalized = this.normalizeUrl(url);
|
|
336
|
+
if (!torMode || normalized.includes(".onion")) {
|
|
337
|
+
bases.add(normalized);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const excluded = new Set(
|
|
341
|
+
this.excludeProviderUrls.map((url) => this.normalizeUrl(url))
|
|
342
|
+
);
|
|
343
|
+
const result = Array.from(bases).filter((base) => !excluded.has(base));
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Bootstrap providers from HTTP endpoint
|
|
348
|
+
* @param torMode Whether running in Tor context
|
|
349
|
+
* @param forceRefresh Ignore routstr21 cache and fetch fresh data
|
|
350
|
+
* @returns Array of provider base URLs
|
|
351
|
+
*/
|
|
352
|
+
async bootstrapFromHttp(torMode, forceRefresh = false) {
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(this.providerDirectoryUrl);
|
|
355
|
+
if (!res.ok) {
|
|
356
|
+
throw new Error(`Failed to fetch providers: ${res.status}`);
|
|
357
|
+
}
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
const providers = Array.isArray(data?.providers) ? data.providers : [];
|
|
360
|
+
const bases = /* @__PURE__ */ new Set();
|
|
361
|
+
this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
|
|
362
|
+
for (const p of providers) {
|
|
363
|
+
const endpoints = this.getProviderEndpoints(p, torMode);
|
|
364
|
+
for (const endpoint of endpoints) {
|
|
365
|
+
bases.add(endpoint);
|
|
366
|
+
this.addProviderNode(this.providerNodePubkeysByUrl, endpoint, p?.pubkey);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const url of this.includeProviderUrls) {
|
|
370
|
+
const normalized = this.normalizeUrl(url);
|
|
371
|
+
if (!torMode || normalized.includes(".onion")) {
|
|
372
|
+
bases.add(normalized);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const excluded = new Set(
|
|
376
|
+
this.excludeProviderUrls.map((url) => this.normalizeUrl(url))
|
|
377
|
+
);
|
|
378
|
+
const list = Array.from(bases).filter((base) => !excluded.has(base));
|
|
379
|
+
if (list.length > 0) {
|
|
380
|
+
this.adapter.setBaseUrlsList(list);
|
|
381
|
+
this.adapter.setBaseUrlsLastUpdate(Date.now());
|
|
382
|
+
await this.fetchRoutstr21Models(forceRefresh);
|
|
383
|
+
await this.syncReviewedProvidersFromNostr(
|
|
384
|
+
list,
|
|
385
|
+
this.providerNodePubkeysByUrl,
|
|
386
|
+
forceRefresh
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return list;
|
|
390
|
+
} catch (e) {
|
|
391
|
+
this.logger.error("Failed to bootstrap providers", e);
|
|
392
|
+
throw new ProviderBootstrapError([], `Provider bootstrap failed: ${e}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Fetch Routstr review events from Nostr (kind 38425) and disable providers
|
|
397
|
+
* whose 38421 node pubkey does not have at least one review tagged `t=lgtm`.
|
|
398
|
+
*
|
|
399
|
+
* Review events are expected to have:
|
|
400
|
+
* - `node`: the reviewed 38421 provider event pubkey
|
|
401
|
+
* - `t`: review label, where `lgtm` means the node looks good
|
|
402
|
+
*
|
|
403
|
+
* @param baseUrls Current provider base URLs to evaluate
|
|
404
|
+
* @returns Array of provider base URLs disabled by the review set
|
|
405
|
+
*/
|
|
406
|
+
async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl, forceRefresh = false) {
|
|
407
|
+
if (baseUrls.length === 0) return [];
|
|
408
|
+
if (!this.adapter.setDisabledProviders) {
|
|
409
|
+
this.logger.warn(
|
|
410
|
+
"NostrReviews: adapter does not support setDisabledProviders; skipping provider disable sync"
|
|
411
|
+
);
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
const reviewedNodePubkeys = /* @__PURE__ */ new Set();
|
|
415
|
+
{
|
|
416
|
+
const cached = await this.getCachedNostrEvents(
|
|
417
|
+
{ kinds: [38425], "#t": ["lgtm"], authors: [this.routstrPubkey] },
|
|
418
|
+
this.cacheTTL,
|
|
419
|
+
forceRefresh
|
|
420
|
+
);
|
|
421
|
+
let sessionEvents = cached;
|
|
422
|
+
if (cached.length === 0) {
|
|
423
|
+
const LGTM_RELAYS = [
|
|
424
|
+
"wss://relay.primal.net",
|
|
425
|
+
"wss://nos.lol",
|
|
426
|
+
"wss://relay.damus.io",
|
|
427
|
+
"wss://relay.routstr.com"
|
|
428
|
+
];
|
|
429
|
+
const pool = new applesauceRelay.RelayPool();
|
|
430
|
+
const timeoutMs = 5e3;
|
|
431
|
+
await new Promise((resolve) => {
|
|
432
|
+
pool.req(LGTM_RELAYS, {
|
|
433
|
+
kinds: [38425],
|
|
434
|
+
"#t": ["lgtm"],
|
|
435
|
+
limit: 500,
|
|
436
|
+
authors: [this.routstrPubkey]
|
|
437
|
+
}).pipe(
|
|
438
|
+
applesauceRelay.onlyEvents(),
|
|
439
|
+
rxjs.tap((event) => {
|
|
440
|
+
sessionEvents.push(event);
|
|
441
|
+
this.eventStore?.add(event);
|
|
442
|
+
this.markEventFetched(event);
|
|
443
|
+
})
|
|
444
|
+
).subscribe({ complete: () => resolve() });
|
|
445
|
+
setTimeout(() => resolve(), timeoutMs);
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
this.logger.log(`Using ${cached.length} cached kind 38425 events from persistent store`);
|
|
449
|
+
}
|
|
450
|
+
for (const event of sessionEvents) {
|
|
451
|
+
const hasLgtmTag = event.tags.some(
|
|
452
|
+
(tag) => tag[0] === "t" && tag[1]?.toLowerCase() === "lgtm"
|
|
453
|
+
);
|
|
454
|
+
if (!hasLgtmTag) continue;
|
|
455
|
+
for (const tag of event.tags) {
|
|
456
|
+
if (tag[0] === "node" && typeof tag[1] === "string" && tag[1]) {
|
|
457
|
+
reviewedNodePubkeys.add(tag[1]);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (reviewedNodePubkeys.size === 0) {
|
|
463
|
+
this.logger.warn(
|
|
464
|
+
"NostrReviews: no kind 38425 lgtm reviews found; keeping disabled providers unchanged"
|
|
465
|
+
);
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
if (providerNodes.size === 0) {
|
|
469
|
+
this.logger.warn(
|
|
470
|
+
"NostrReviews: no kind 38421 provider node metadata found; keeping disabled providers unchanged"
|
|
471
|
+
);
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
const disabledByReview = [];
|
|
475
|
+
for (const url of baseUrls) {
|
|
476
|
+
const normalized = this.normalizeUrl(url);
|
|
477
|
+
const nodePubkeys = providerNodes.get(normalized) || /* @__PURE__ */ new Set();
|
|
478
|
+
const hasLgtmReview = Array.from(nodePubkeys).some(
|
|
479
|
+
(pubkey) => reviewedNodePubkeys.has(pubkey)
|
|
480
|
+
);
|
|
481
|
+
if (!hasLgtmReview) {
|
|
482
|
+
disabledByReview.push(normalized);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
this.adapter.setDisabledProviders(Array.from(new Set(disabledByReview)));
|
|
486
|
+
return disabledByReview;
|
|
487
|
+
}
|
|
488
|
+
addProviderNode(map, url, pubkey) {
|
|
489
|
+
if (!pubkey) return;
|
|
490
|
+
const normalized = this.normalizeUrl(url);
|
|
491
|
+
const existing = map.get(normalized) || /* @__PURE__ */ new Set();
|
|
492
|
+
existing.add(pubkey);
|
|
493
|
+
map.set(normalized, existing);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Fetch models from all providers and select best-priced options
|
|
497
|
+
* Uses cache if available and not expired
|
|
498
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
499
|
+
* @param forceRefresh Ignore cache and fetch fresh data
|
|
500
|
+
* @param onProgress Callback fired after each provider completes with current combined models
|
|
501
|
+
* @returns Array of unique models with best prices selected
|
|
502
|
+
*/
|
|
503
|
+
async fetchModels(baseUrls, forceRefresh = false, onProgress) {
|
|
504
|
+
if (baseUrls.length === 0) {
|
|
505
|
+
throw new NoProvidersAvailableError();
|
|
506
|
+
}
|
|
507
|
+
const bestById = /* @__PURE__ */ new Map();
|
|
508
|
+
const modelsFromAllProviders = {};
|
|
509
|
+
const disabledProviders = this.adapter.getDisabledProviders();
|
|
510
|
+
const estimateMinCost = (m) => {
|
|
511
|
+
return m?.sats_pricing?.completion ?? 0;
|
|
512
|
+
};
|
|
513
|
+
const emitProgress = () => {
|
|
514
|
+
if (onProgress) {
|
|
515
|
+
const currentModels = Array.from(bestById.values()).map((v) => v.model);
|
|
516
|
+
onProgress(currentModels);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const fetchPromises = baseUrls.map(async (url) => {
|
|
520
|
+
const base = url.endsWith("/") ? url : `${url}/`;
|
|
521
|
+
try {
|
|
522
|
+
let list;
|
|
523
|
+
if (!forceRefresh) {
|
|
524
|
+
const lastUpdate = this.adapter.getProviderLastUpdate(base);
|
|
525
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
526
|
+
if (cacheValid) {
|
|
527
|
+
const cachedModels = this.adapter.getCachedModels();
|
|
528
|
+
const cachedList = cachedModels[base] || [];
|
|
529
|
+
list = cachedList;
|
|
530
|
+
} else {
|
|
531
|
+
list = await this.fetchModelsFromProvider(base);
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
list = await this.fetchModelsFromProvider(base);
|
|
535
|
+
}
|
|
536
|
+
modelsFromAllProviders[base] = list;
|
|
537
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
538
|
+
if (!disabledProviders.includes(base)) {
|
|
539
|
+
for (const m of list) {
|
|
540
|
+
const existing = bestById.get(m.id);
|
|
541
|
+
if (!m.sats_pricing) continue;
|
|
542
|
+
if (!existing) {
|
|
543
|
+
bestById.set(m.id, { model: m, base });
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const currentCost = estimateMinCost(m);
|
|
547
|
+
const existingCost = estimateMinCost(existing.model);
|
|
548
|
+
if (currentCost < existingCost && m.sats_pricing) {
|
|
549
|
+
bestById.set(m.id, { model: m, base });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
emitProgress();
|
|
554
|
+
return { success: true, base, list };
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (this.isProviderDownError(error)) {
|
|
557
|
+
this.logger.warn(`Provider ${base} is down right now.`);
|
|
558
|
+
} else {
|
|
559
|
+
this.logger.warn(`Provider ${base} unreachable: ${error.message}`);
|
|
560
|
+
}
|
|
561
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
562
|
+
return { success: false, base };
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
await Promise.allSettled(fetchPromises);
|
|
566
|
+
const existingCache = this.adapter.getCachedModels();
|
|
567
|
+
this.adapter.setCachedModels({
|
|
568
|
+
...existingCache,
|
|
569
|
+
...modelsFromAllProviders
|
|
570
|
+
});
|
|
571
|
+
return Array.from(bestById.values()).map((v) => v.model);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Fetch models from a single provider
|
|
575
|
+
* @param baseUrl Provider base URL
|
|
576
|
+
* @returns Array of models from provider
|
|
577
|
+
*/
|
|
578
|
+
async fetchModelsFromProvider(baseUrl) {
|
|
579
|
+
const res = await fetch(`${baseUrl}v1/models`);
|
|
580
|
+
if (!res.ok) {
|
|
581
|
+
throw new Error(`Failed to fetch models: ${res.status}`);
|
|
582
|
+
}
|
|
583
|
+
const json = await res.json();
|
|
584
|
+
const list = Array.isArray(json?.data) ? json.data : [];
|
|
585
|
+
return list;
|
|
586
|
+
}
|
|
587
|
+
isProviderDownError(error) {
|
|
588
|
+
if (!(error instanceof Error)) return false;
|
|
589
|
+
const msg = error.message.toLowerCase();
|
|
590
|
+
if (msg.includes("fetch failed")) return true;
|
|
591
|
+
if (msg.includes("429")) return true;
|
|
592
|
+
if (msg.includes("502")) return true;
|
|
593
|
+
if (msg.includes("503")) return true;
|
|
594
|
+
if (msg.includes("504")) return true;
|
|
595
|
+
const cause = error.cause;
|
|
596
|
+
return cause?.code === "ENOTFOUND";
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get all cached models from all providers
|
|
600
|
+
* @returns Record mapping baseUrl -> models
|
|
601
|
+
*/
|
|
602
|
+
getAllCachedModels() {
|
|
603
|
+
return this.adapter.getCachedModels();
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Clear cache for a specific provider
|
|
607
|
+
* @param baseUrl Provider base URL
|
|
608
|
+
*/
|
|
609
|
+
clearProviderCache(baseUrl) {
|
|
610
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
611
|
+
const cached = this.adapter.getCachedModels();
|
|
612
|
+
delete cached[base];
|
|
613
|
+
this.adapter.setCachedModels(cached);
|
|
614
|
+
this.adapter.setProviderLastUpdate(base, 0);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Clear all model caches
|
|
618
|
+
*/
|
|
619
|
+
clearAllCache() {
|
|
620
|
+
this.adapter.setCachedModels({});
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Filter base URLs based on Tor context
|
|
624
|
+
* @param baseUrls Provider URLs to filter
|
|
625
|
+
* @param torMode Whether in Tor context
|
|
626
|
+
* @returns Filtered URLs appropriate for Tor mode
|
|
627
|
+
*/
|
|
628
|
+
filterBaseUrlsForTor(baseUrls, torMode) {
|
|
629
|
+
if (!torMode) {
|
|
630
|
+
return baseUrls.filter((url) => !url.includes(".onion"));
|
|
631
|
+
}
|
|
632
|
+
return baseUrls.filter((url) => url.includes(".onion"));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Get provider endpoints from provider info
|
|
636
|
+
* @param provider Provider object from directory
|
|
637
|
+
* @param torMode Whether in Tor context
|
|
638
|
+
* @returns Array of endpoint URLs
|
|
639
|
+
*/
|
|
640
|
+
getProviderEndpoints(provider, torMode) {
|
|
641
|
+
const endpoints = [];
|
|
642
|
+
if (torMode && provider.onion_url) {
|
|
643
|
+
endpoints.push(this.normalizeUrl(provider.onion_url));
|
|
644
|
+
} else if (provider.endpoint_url) {
|
|
645
|
+
endpoints.push(this.normalizeUrl(provider.endpoint_url));
|
|
646
|
+
}
|
|
647
|
+
return endpoints;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Normalize provider URL with trailing slash
|
|
651
|
+
* @param url URL to normalize
|
|
652
|
+
* @returns Normalized URL
|
|
653
|
+
*/
|
|
654
|
+
normalizeUrl(url) {
|
|
655
|
+
if (!url.startsWith("http")) {
|
|
656
|
+
url = `https://${url}`;
|
|
657
|
+
}
|
|
658
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Fetch routstr21 models from Nostr network (kind 38423)
|
|
662
|
+
* Uses cache if available and not expired
|
|
663
|
+
* @returns Array of model IDs or empty array if not found
|
|
664
|
+
*/
|
|
665
|
+
async fetchRoutstr21Models(forceRefresh = false) {
|
|
666
|
+
const cachedModels = this.adapter.getRoutstr21Models();
|
|
667
|
+
if (!forceRefresh && cachedModels.length > 0) {
|
|
668
|
+
const lastUpdate = this.adapter.getRoutstr21ModelsLastUpdate();
|
|
669
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
670
|
+
if (cacheValid) {
|
|
671
|
+
return cachedModels;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const DEFAULT_RELAYS = [
|
|
675
|
+
"wss://relay.damus.io",
|
|
676
|
+
"wss://nos.lol",
|
|
677
|
+
"wss://relay.routstr.com"
|
|
678
|
+
];
|
|
679
|
+
const cached = await this.getCachedNostrEvents(
|
|
680
|
+
{ kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] },
|
|
681
|
+
this.cacheTTL,
|
|
682
|
+
forceRefresh
|
|
683
|
+
);
|
|
684
|
+
let sessionEvents = cached;
|
|
685
|
+
if (cached.length === 0) {
|
|
686
|
+
const pool = new applesauceRelay.RelayPool();
|
|
687
|
+
const timeoutMs = 5e3;
|
|
688
|
+
await new Promise((resolve) => {
|
|
689
|
+
pool.req(DEFAULT_RELAYS, {
|
|
690
|
+
kinds: [38423],
|
|
691
|
+
"#d": ["routstr-21-models"],
|
|
692
|
+
limit: 1,
|
|
693
|
+
authors: [this.routstrPubkey]
|
|
694
|
+
}).pipe(
|
|
695
|
+
applesauceRelay.onlyEvents(),
|
|
696
|
+
rxjs.tap((event2) => {
|
|
697
|
+
sessionEvents.push(event2);
|
|
698
|
+
this.eventStore?.add(event2);
|
|
699
|
+
this.markEventFetched(event2);
|
|
700
|
+
})
|
|
701
|
+
).subscribe({
|
|
702
|
+
complete: () => {
|
|
703
|
+
resolve();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
setTimeout(() => {
|
|
707
|
+
resolve();
|
|
708
|
+
}, timeoutMs);
|
|
709
|
+
});
|
|
710
|
+
} else {
|
|
711
|
+
this.logger.log(`Using ${cached.length} cached kind 38423 events from persistent store`);
|
|
712
|
+
}
|
|
713
|
+
if (sessionEvents.length === 0) {
|
|
714
|
+
return cachedModels.length > 0 ? cachedModels : [];
|
|
715
|
+
}
|
|
716
|
+
const event = sessionEvents[0];
|
|
717
|
+
try {
|
|
718
|
+
const content = JSON.parse(event.content);
|
|
719
|
+
const models = Array.isArray(content?.models) ? content.models : [];
|
|
720
|
+
this.adapter.setRoutstr21Models(models);
|
|
721
|
+
this.adapter.setRoutstr21ModelsLastUpdate(Date.now());
|
|
722
|
+
return models;
|
|
723
|
+
} catch {
|
|
724
|
+
this.logger.warn(
|
|
725
|
+
"Routstr21Models: failed to parse Nostr event content:",
|
|
726
|
+
event.id
|
|
727
|
+
);
|
|
728
|
+
return cachedModels.length > 0 ? cachedModels : [];
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// discovery/MintDiscovery.ts
|
|
734
|
+
var MintDiscovery = class {
|
|
735
|
+
constructor(adapter, config = {}) {
|
|
736
|
+
this.adapter = adapter;
|
|
737
|
+
this.cacheTTL = config.cacheTTL || 21 * 60 * 1e3;
|
|
738
|
+
this.logger = (config.logger ?? consoleLogger).child("MintDiscovery");
|
|
739
|
+
}
|
|
740
|
+
adapter;
|
|
741
|
+
cacheTTL;
|
|
742
|
+
logger;
|
|
743
|
+
/**
|
|
744
|
+
* Fetch mints from all providers via their /v1/info endpoints
|
|
745
|
+
* Caches mints and full provider info for later access
|
|
746
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
747
|
+
* @returns Object with mints and provider info from all providers
|
|
748
|
+
*/
|
|
749
|
+
async discoverMints(baseUrls, options = {}) {
|
|
750
|
+
if (baseUrls.length === 0) {
|
|
751
|
+
return { mintsFromProviders: {}, infoFromProviders: {} };
|
|
752
|
+
}
|
|
753
|
+
const mintsFromAllProviders = {};
|
|
754
|
+
const infoFromAllProviders = {};
|
|
755
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
756
|
+
const fetchPromises = baseUrls.map(async (url) => {
|
|
757
|
+
const base = url.endsWith("/") ? url : `${url}/`;
|
|
758
|
+
try {
|
|
759
|
+
if (!forceRefresh) {
|
|
760
|
+
const lastUpdate = this.adapter.getProviderLastUpdate(base);
|
|
761
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
762
|
+
if (cacheValid) {
|
|
763
|
+
const cachedMints = this.adapter.getCachedMints()[base] || [];
|
|
764
|
+
const cachedInfo = this.adapter.getCachedProviderInfo()[base];
|
|
765
|
+
mintsFromAllProviders[base] = cachedMints;
|
|
766
|
+
if (cachedInfo) {
|
|
767
|
+
infoFromAllProviders[base] = cachedInfo;
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
success: true,
|
|
771
|
+
base,
|
|
772
|
+
mints: cachedMints,
|
|
773
|
+
info: cachedInfo
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const res = await fetch(`${base}v1/info`);
|
|
778
|
+
if (!res.ok) {
|
|
779
|
+
throw new Error(`Failed to fetch info: ${res.status}`);
|
|
780
|
+
}
|
|
781
|
+
const json = await res.json();
|
|
782
|
+
const mints = Array.isArray(json?.mints) ? json.mints : [];
|
|
783
|
+
const normalizedMints = mints.map(
|
|
784
|
+
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
785
|
+
);
|
|
786
|
+
mintsFromAllProviders[base] = normalizedMints;
|
|
787
|
+
infoFromAllProviders[base] = json;
|
|
788
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
789
|
+
return { success: true, base, mints: normalizedMints, info: json };
|
|
790
|
+
} catch (error) {
|
|
791
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
792
|
+
if (this.isProviderDownError(error)) {
|
|
793
|
+
this.logger.warn(`Provider ${base} is down right now.`);
|
|
794
|
+
} else {
|
|
795
|
+
this.logger.warn(`Failed to fetch mints from ${base}:`, error);
|
|
796
|
+
}
|
|
797
|
+
return { success: false, base, mints: [], info: null };
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
const results = await Promise.allSettled(fetchPromises);
|
|
801
|
+
for (const result of results) {
|
|
802
|
+
if (result.status === "fulfilled") {
|
|
803
|
+
const { base, mints, info } = result.value;
|
|
804
|
+
mintsFromAllProviders[base] = mints;
|
|
805
|
+
if (info) {
|
|
806
|
+
infoFromAllProviders[base] = info;
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
this.logger.error("Mint discovery error:", result.reason);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
this.adapter.setCachedMints(mintsFromAllProviders);
|
|
814
|
+
this.adapter.setCachedProviderInfo(infoFromAllProviders);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
this.logger.error("Error caching mint discovery results:", error);
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
mintsFromProviders: mintsFromAllProviders,
|
|
820
|
+
infoFromProviders: infoFromAllProviders
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Get cached mints from all providers
|
|
825
|
+
* @returns Record mapping baseUrl -> mint URLs
|
|
826
|
+
*/
|
|
827
|
+
getCachedMints() {
|
|
828
|
+
return this.adapter.getCachedMints();
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get cached provider info from all providers
|
|
832
|
+
* @returns Record mapping baseUrl -> provider info
|
|
833
|
+
*/
|
|
834
|
+
getCachedProviderInfo() {
|
|
835
|
+
return this.adapter.getCachedProviderInfo();
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Get mints for a specific provider
|
|
839
|
+
* @param baseUrl Provider base URL
|
|
840
|
+
* @returns Array of mint URLs for the provider
|
|
841
|
+
*/
|
|
842
|
+
getProviderMints(baseUrl) {
|
|
843
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
844
|
+
const allMints = this.getCachedMints();
|
|
845
|
+
return allMints[normalized] || [];
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get info for a specific provider
|
|
849
|
+
* @param baseUrl Provider base URL
|
|
850
|
+
* @returns Provider info object or null if not found
|
|
851
|
+
*/
|
|
852
|
+
getProviderInfo(baseUrl) {
|
|
853
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
854
|
+
const allInfo = this.getCachedProviderInfo();
|
|
855
|
+
return allInfo[normalized] || null;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Clear mint cache for a specific provider
|
|
859
|
+
* @param baseUrl Provider base URL
|
|
860
|
+
*/
|
|
861
|
+
clearProviderMintCache(baseUrl) {
|
|
862
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
863
|
+
const mints = this.getCachedMints();
|
|
864
|
+
delete mints[normalized];
|
|
865
|
+
this.adapter.setCachedMints(mints);
|
|
866
|
+
const info = this.getCachedProviderInfo();
|
|
867
|
+
delete info[normalized];
|
|
868
|
+
this.adapter.setCachedProviderInfo(info);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Clear all mint caches
|
|
872
|
+
*/
|
|
873
|
+
clearAllCache() {
|
|
874
|
+
this.adapter.setCachedMints({});
|
|
875
|
+
this.adapter.setCachedProviderInfo({});
|
|
876
|
+
}
|
|
877
|
+
isProviderDownError(error) {
|
|
878
|
+
if (!(error instanceof Error)) return false;
|
|
879
|
+
const msg = error.message.toLowerCase();
|
|
880
|
+
if (msg.includes("fetch failed")) return true;
|
|
881
|
+
if (msg.includes("429")) return true;
|
|
882
|
+
if (msg.includes("502")) return true;
|
|
883
|
+
if (msg.includes("503")) return true;
|
|
884
|
+
if (msg.includes("504")) return true;
|
|
885
|
+
const cause = error.cause;
|
|
886
|
+
if (cause?.code === "ENOTFOUND") return true;
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
exports.MintDiscovery = MintDiscovery;
|
|
892
|
+
exports.ModelManager = ModelManager;
|
|
893
|
+
//# sourceMappingURL=index.js.map
|
|
894
|
+
//# sourceMappingURL=index.js.map
|