@unicitylabs/sphere-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/core/index.cjs +97 -2589
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +11 -222
- package/dist/core/index.d.ts +11 -222
- package/dist/core/index.js +93 -2585
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +233 -195
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +234 -198
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +6 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +6 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +205 -142
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +2 -70
- package/dist/impl/nodejs/index.d.ts +2 -70
- package/dist/impl/nodejs/index.js +206 -145
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +239 -2600
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -226
- package/dist/index.d.ts +60 -226
- package/dist/index.js +235 -2593
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,11 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
40
40
|
/** Cached token registry JSON (fetched from remote) */
|
|
41
41
|
TOKEN_REGISTRY_CACHE: "token_registry_cache",
|
|
42
42
|
/** Timestamp of last token registry cache update (ms since epoch) */
|
|
43
|
-
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
|
|
43
|
+
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
|
|
44
|
+
/** Cached price data JSON (from CoinGecko or other provider) */
|
|
45
|
+
PRICE_CACHE: "price_cache",
|
|
46
|
+
/** Timestamp of last price cache update (ms since epoch) */
|
|
47
|
+
PRICE_CACHE_TS: "price_cache_ts"
|
|
44
48
|
};
|
|
45
49
|
var STORAGE_KEYS_ADDRESS = {
|
|
46
50
|
/** Pending transfers for this address */
|
|
@@ -165,7 +169,6 @@ var TIMEOUTS = {
|
|
|
165
169
|
/** Sync interval */
|
|
166
170
|
SYNC_INTERVAL: 6e4
|
|
167
171
|
};
|
|
168
|
-
var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
|
|
169
172
|
|
|
170
173
|
// impl/browser/storage/LocalStorageProvider.ts
|
|
171
174
|
var LocalStorageProvider = class {
|
|
@@ -533,61 +536,36 @@ var IndexedDBTokenStorageProvider = class {
|
|
|
533
536
|
return meta !== null;
|
|
534
537
|
}
|
|
535
538
|
async clear() {
|
|
536
|
-
|
|
539
|
+
if (this.db) {
|
|
540
|
+
this.db.close();
|
|
541
|
+
this.db = null;
|
|
542
|
+
}
|
|
543
|
+
this.status = "disconnected";
|
|
544
|
+
const CLEAR_TIMEOUT = 1500;
|
|
545
|
+
const withTimeout = (promise, ms, label) => Promise.race([
|
|
546
|
+
promise,
|
|
547
|
+
new Promise(
|
|
548
|
+
(_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
|
|
549
|
+
)
|
|
550
|
+
]);
|
|
551
|
+
const deleteDb = (name) => new Promise((resolve) => {
|
|
552
|
+
const req = indexedDB.deleteDatabase(name);
|
|
553
|
+
req.onsuccess = () => resolve();
|
|
554
|
+
req.onerror = () => resolve();
|
|
555
|
+
req.onblocked = () => resolve();
|
|
556
|
+
});
|
|
537
557
|
try {
|
|
538
|
-
if (this.db) {
|
|
539
|
-
await this.clearStore(STORE_TOKENS);
|
|
540
|
-
await this.clearStore(STORE_META);
|
|
541
|
-
this.db.close();
|
|
542
|
-
this.db = null;
|
|
543
|
-
}
|
|
544
|
-
this.status = "disconnected";
|
|
545
558
|
if (typeof indexedDB.databases === "function") {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
const db = await new Promise((resolve, reject) => {
|
|
558
|
-
const req = indexedDB.open(dbInfo.name, DB_VERSION);
|
|
559
|
-
req.onsuccess = () => resolve(req.result);
|
|
560
|
-
req.onerror = () => reject(req.error);
|
|
561
|
-
req.onupgradeneeded = (e) => {
|
|
562
|
-
const d = e.target.result;
|
|
563
|
-
if (!d.objectStoreNames.contains(STORE_TOKENS)) d.createObjectStore(STORE_TOKENS, { keyPath: "id" });
|
|
564
|
-
if (!d.objectStoreNames.contains(STORE_META)) d.createObjectStore(STORE_META);
|
|
565
|
-
};
|
|
566
|
-
});
|
|
567
|
-
const clearTx = db.transaction([STORE_TOKENS, STORE_META], "readwrite");
|
|
568
|
-
clearTx.objectStore(STORE_TOKENS).clear();
|
|
569
|
-
clearTx.objectStore(STORE_META).clear();
|
|
570
|
-
await new Promise((resolve) => {
|
|
571
|
-
clearTx.oncomplete = () => resolve();
|
|
572
|
-
clearTx.onerror = () => resolve();
|
|
573
|
-
});
|
|
574
|
-
db.close();
|
|
575
|
-
} catch {
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} catch {
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
for (const name of dbNames) {
|
|
583
|
-
try {
|
|
584
|
-
const req = indexedDB.deleteDatabase(name);
|
|
585
|
-
req.onerror = () => {
|
|
586
|
-
};
|
|
587
|
-
req.onblocked = () => {
|
|
588
|
-
};
|
|
589
|
-
} catch {
|
|
590
|
-
}
|
|
559
|
+
const dbs = await withTimeout(
|
|
560
|
+
indexedDB.databases(),
|
|
561
|
+
CLEAR_TIMEOUT,
|
|
562
|
+
"indexedDB.databases()"
|
|
563
|
+
);
|
|
564
|
+
await Promise.all(
|
|
565
|
+
dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
|
|
566
|
+
);
|
|
567
|
+
} else {
|
|
568
|
+
await deleteDb(this.dbName);
|
|
591
569
|
}
|
|
592
570
|
return true;
|
|
593
571
|
} catch (err) {
|
|
@@ -1136,9 +1114,7 @@ import {
|
|
|
1136
1114
|
EventKinds,
|
|
1137
1115
|
hashNametag,
|
|
1138
1116
|
NostrClient,
|
|
1139
|
-
Filter
|
|
1140
|
-
isChatMessage,
|
|
1141
|
-
isReadReceipt
|
|
1117
|
+
Filter
|
|
1142
1118
|
} from "@unicitylabs/nostr-js-sdk";
|
|
1143
1119
|
|
|
1144
1120
|
// core/crypto.ts
|
|
@@ -1356,8 +1332,6 @@ var NostrTransportProvider = class {
|
|
|
1356
1332
|
transferHandlers = /* @__PURE__ */ new Set();
|
|
1357
1333
|
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
1358
1334
|
paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
|
|
1359
|
-
readReceiptHandlers = /* @__PURE__ */ new Set();
|
|
1360
|
-
typingIndicatorHandlers = /* @__PURE__ */ new Set();
|
|
1361
1335
|
broadcastHandlers = /* @__PURE__ */ new Map();
|
|
1362
1336
|
eventCallbacks = /* @__PURE__ */ new Set();
|
|
1363
1337
|
constructor(config) {
|
|
@@ -1609,18 +1583,6 @@ var NostrTransportProvider = class {
|
|
|
1609
1583
|
const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
|
|
1610
1584
|
const giftWrap = NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
|
|
1611
1585
|
await this.publishEvent(giftWrap);
|
|
1612
|
-
const selfWrapContent = JSON.stringify({
|
|
1613
|
-
selfWrap: true,
|
|
1614
|
-
originalId: giftWrap.id,
|
|
1615
|
-
recipientPubkey,
|
|
1616
|
-
senderNametag,
|
|
1617
|
-
text: content
|
|
1618
|
-
});
|
|
1619
|
-
const selfPubkey = this.keyManager.getPublicKeyHex();
|
|
1620
|
-
const selfGiftWrap = NIP17.createGiftWrap(this.keyManager, selfPubkey, selfWrapContent);
|
|
1621
|
-
this.publishEvent(selfGiftWrap).catch((err) => {
|
|
1622
|
-
this.log("Self-wrap publish failed:", err);
|
|
1623
|
-
});
|
|
1624
1586
|
this.emitEvent({
|
|
1625
1587
|
type: "message:sent",
|
|
1626
1588
|
timestamp: Date.now(),
|
|
@@ -1719,37 +1681,6 @@ var NostrTransportProvider = class {
|
|
|
1719
1681
|
this.paymentRequestResponseHandlers.add(handler);
|
|
1720
1682
|
return () => this.paymentRequestResponseHandlers.delete(handler);
|
|
1721
1683
|
}
|
|
1722
|
-
// ===========================================================================
|
|
1723
|
-
// Read Receipts
|
|
1724
|
-
// ===========================================================================
|
|
1725
|
-
async sendReadReceipt(recipientTransportPubkey, messageEventId) {
|
|
1726
|
-
if (!this.keyManager) throw new Error("Not initialized");
|
|
1727
|
-
const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
|
|
1728
|
-
const event = NIP17.createReadReceipt(this.keyManager, nostrRecipient, messageEventId);
|
|
1729
|
-
await this.publishEvent(event);
|
|
1730
|
-
this.log("Sent read receipt for:", messageEventId, "to:", nostrRecipient.slice(0, 16));
|
|
1731
|
-
}
|
|
1732
|
-
onReadReceipt(handler) {
|
|
1733
|
-
this.readReceiptHandlers.add(handler);
|
|
1734
|
-
return () => this.readReceiptHandlers.delete(handler);
|
|
1735
|
-
}
|
|
1736
|
-
// ===========================================================================
|
|
1737
|
-
// Typing Indicators
|
|
1738
|
-
// ===========================================================================
|
|
1739
|
-
async sendTypingIndicator(recipientTransportPubkey) {
|
|
1740
|
-
if (!this.keyManager) throw new Error("Not initialized");
|
|
1741
|
-
const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
|
|
1742
|
-
const content = JSON.stringify({
|
|
1743
|
-
type: "typing",
|
|
1744
|
-
senderNametag: this.identity?.nametag
|
|
1745
|
-
});
|
|
1746
|
-
const event = NIP17.createGiftWrap(this.keyManager, nostrRecipient, content);
|
|
1747
|
-
await this.publishEvent(event);
|
|
1748
|
-
}
|
|
1749
|
-
onTypingIndicator(handler) {
|
|
1750
|
-
this.typingIndicatorHandlers.add(handler);
|
|
1751
|
-
return () => this.typingIndicatorHandlers.delete(handler);
|
|
1752
|
-
}
|
|
1753
1684
|
/**
|
|
1754
1685
|
* Resolve any identifier to full peer information.
|
|
1755
1686
|
* Routes to the appropriate specific resolve method based on identifier format.
|
|
@@ -2203,74 +2134,11 @@ var NostrTransportProvider = class {
|
|
|
2203
2134
|
const pm = NIP17.unwrap(event, this.keyManager);
|
|
2204
2135
|
this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
|
|
2205
2136
|
if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
|
|
2206
|
-
|
|
2207
|
-
const parsed = JSON.parse(pm.content);
|
|
2208
|
-
if (parsed?.selfWrap && parsed.recipientPubkey) {
|
|
2209
|
-
this.log("Self-wrap replay for recipient:", parsed.recipientPubkey?.slice(0, 16));
|
|
2210
|
-
const message2 = {
|
|
2211
|
-
id: parsed.originalId || pm.eventId,
|
|
2212
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2213
|
-
senderNametag: parsed.senderNametag,
|
|
2214
|
-
recipientTransportPubkey: parsed.recipientPubkey,
|
|
2215
|
-
content: parsed.text ?? "",
|
|
2216
|
-
timestamp: pm.timestamp * 1e3,
|
|
2217
|
-
encrypted: true,
|
|
2218
|
-
isSelfWrap: true
|
|
2219
|
-
};
|
|
2220
|
-
for (const handler of this.messageHandlers) {
|
|
2221
|
-
try {
|
|
2222
|
-
handler(message2);
|
|
2223
|
-
} catch (e) {
|
|
2224
|
-
this.log("Self-wrap handler error:", e);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
} catch {
|
|
2230
|
-
}
|
|
2231
|
-
this.log("Skipping own non-self-wrap message");
|
|
2137
|
+
this.log("Skipping own message");
|
|
2232
2138
|
return;
|
|
2233
2139
|
}
|
|
2234
|
-
if (
|
|
2235
|
-
this.log("
|
|
2236
|
-
if (pm.replyToEventId) {
|
|
2237
|
-
const receipt = {
|
|
2238
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2239
|
-
messageEventId: pm.replyToEventId,
|
|
2240
|
-
timestamp: pm.timestamp * 1e3
|
|
2241
|
-
};
|
|
2242
|
-
for (const handler of this.readReceiptHandlers) {
|
|
2243
|
-
try {
|
|
2244
|
-
handler(receipt);
|
|
2245
|
-
} catch (e) {
|
|
2246
|
-
this.log("Read receipt handler error:", e);
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
|
-
try {
|
|
2253
|
-
const parsed = JSON.parse(pm.content);
|
|
2254
|
-
if (parsed?.type === "typing") {
|
|
2255
|
-
this.log("Typing indicator from:", pm.senderPubkey?.slice(0, 16));
|
|
2256
|
-
const indicator = {
|
|
2257
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2258
|
-
senderNametag: parsed.senderNametag,
|
|
2259
|
-
timestamp: pm.timestamp * 1e3
|
|
2260
|
-
};
|
|
2261
|
-
for (const handler of this.typingIndicatorHandlers) {
|
|
2262
|
-
try {
|
|
2263
|
-
handler(indicator);
|
|
2264
|
-
} catch (e) {
|
|
2265
|
-
this.log("Typing handler error:", e);
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
} catch {
|
|
2271
|
-
}
|
|
2272
|
-
if (!isChatMessage(pm)) {
|
|
2273
|
-
this.log("Skipping unknown message kind:", pm.kind);
|
|
2140
|
+
if (pm.kind !== EventKinds.CHAT_MESSAGE) {
|
|
2141
|
+
this.log("Skipping non-chat message, kind:", pm.kind);
|
|
2274
2142
|
return;
|
|
2275
2143
|
}
|
|
2276
2144
|
let content = pm.content;
|
|
@@ -2285,9 +2153,7 @@ var NostrTransportProvider = class {
|
|
|
2285
2153
|
}
|
|
2286
2154
|
this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
|
|
2287
2155
|
const message = {
|
|
2288
|
-
|
|
2289
|
-
// This ensures read receipts reference an ID the sender recognizes.
|
|
2290
|
-
id: event.id,
|
|
2156
|
+
id: pm.eventId,
|
|
2291
2157
|
senderTransportPubkey: pm.senderPubkey,
|
|
2292
2158
|
senderNametag,
|
|
2293
2159
|
content,
|
|
@@ -3365,6 +3231,7 @@ async function loadIpnsModule() {
|
|
|
3365
3231
|
async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
|
|
3366
3232
|
const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
|
|
3367
3233
|
const record = await createIPNSRecord(
|
|
3234
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3368
3235
|
keyPair,
|
|
3369
3236
|
`/ipfs/${cid}`,
|
|
3370
3237
|
sequenceNumber,
|
|
@@ -5109,26 +4976,37 @@ var CoinGeckoPriceProvider = class {
|
|
|
5109
4976
|
timeout;
|
|
5110
4977
|
debug;
|
|
5111
4978
|
baseUrl;
|
|
4979
|
+
storage;
|
|
4980
|
+
/** In-flight fetch promise for deduplication of concurrent getPrices() calls */
|
|
4981
|
+
fetchPromise = null;
|
|
4982
|
+
/** Token names being fetched in the current in-flight request */
|
|
4983
|
+
fetchNames = null;
|
|
4984
|
+
/** Whether persistent cache has been loaded into memory */
|
|
4985
|
+
persistentCacheLoaded = false;
|
|
4986
|
+
/** Promise for loading persistent cache (deduplication) */
|
|
4987
|
+
loadCachePromise = null;
|
|
5112
4988
|
constructor(config) {
|
|
5113
4989
|
this.apiKey = config?.apiKey;
|
|
5114
4990
|
this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
|
|
5115
4991
|
this.timeout = config?.timeout ?? 1e4;
|
|
5116
4992
|
this.debug = config?.debug ?? false;
|
|
4993
|
+
this.storage = config?.storage ?? null;
|
|
5117
4994
|
this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
|
|
5118
4995
|
}
|
|
5119
4996
|
async getPrices(tokenNames) {
|
|
5120
4997
|
if (tokenNames.length === 0) {
|
|
5121
4998
|
return /* @__PURE__ */ new Map();
|
|
5122
4999
|
}
|
|
5000
|
+
if (!this.persistentCacheLoaded && this.storage) {
|
|
5001
|
+
await this.loadFromStorage();
|
|
5002
|
+
}
|
|
5123
5003
|
const now = Date.now();
|
|
5124
5004
|
const result = /* @__PURE__ */ new Map();
|
|
5125
5005
|
const uncachedNames = [];
|
|
5126
5006
|
for (const name of tokenNames) {
|
|
5127
5007
|
const cached = this.cache.get(name);
|
|
5128
5008
|
if (cached && cached.expiresAt > now) {
|
|
5129
|
-
|
|
5130
|
-
result.set(name, cached.price);
|
|
5131
|
-
}
|
|
5009
|
+
result.set(name, cached.price);
|
|
5132
5010
|
} else {
|
|
5133
5011
|
uncachedNames.push(name);
|
|
5134
5012
|
}
|
|
@@ -5136,6 +5014,41 @@ var CoinGeckoPriceProvider = class {
|
|
|
5136
5014
|
if (uncachedNames.length === 0) {
|
|
5137
5015
|
return result;
|
|
5138
5016
|
}
|
|
5017
|
+
if (this.fetchPromise && this.fetchNames) {
|
|
5018
|
+
const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
|
|
5019
|
+
if (allCovered) {
|
|
5020
|
+
if (this.debug) {
|
|
5021
|
+
console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
|
|
5022
|
+
}
|
|
5023
|
+
const fetched = await this.fetchPromise;
|
|
5024
|
+
for (const name of uncachedNames) {
|
|
5025
|
+
const price = fetched.get(name);
|
|
5026
|
+
if (price) {
|
|
5027
|
+
result.set(name, price);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
return result;
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
const fetchPromise = this.doFetch(uncachedNames);
|
|
5034
|
+
this.fetchPromise = fetchPromise;
|
|
5035
|
+
this.fetchNames = new Set(uncachedNames);
|
|
5036
|
+
try {
|
|
5037
|
+
const fetched = await fetchPromise;
|
|
5038
|
+
for (const [name, price] of fetched) {
|
|
5039
|
+
result.set(name, price);
|
|
5040
|
+
}
|
|
5041
|
+
} finally {
|
|
5042
|
+
if (this.fetchPromise === fetchPromise) {
|
|
5043
|
+
this.fetchPromise = null;
|
|
5044
|
+
this.fetchNames = null;
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
return result;
|
|
5048
|
+
}
|
|
5049
|
+
async doFetch(uncachedNames) {
|
|
5050
|
+
const result = /* @__PURE__ */ new Map();
|
|
5051
|
+
const now = Date.now();
|
|
5139
5052
|
try {
|
|
5140
5053
|
const ids = uncachedNames.join(",");
|
|
5141
5054
|
const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
|
|
@@ -5151,6 +5064,9 @@ var CoinGeckoPriceProvider = class {
|
|
|
5151
5064
|
signal: AbortSignal.timeout(this.timeout)
|
|
5152
5065
|
});
|
|
5153
5066
|
if (!response.ok) {
|
|
5067
|
+
if (response.status === 429) {
|
|
5068
|
+
this.extendCacheOnRateLimit(uncachedNames);
|
|
5069
|
+
}
|
|
5154
5070
|
throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
|
|
5155
5071
|
}
|
|
5156
5072
|
const data = await response.json();
|
|
@@ -5169,25 +5085,113 @@ var CoinGeckoPriceProvider = class {
|
|
|
5169
5085
|
}
|
|
5170
5086
|
for (const name of uncachedNames) {
|
|
5171
5087
|
if (!result.has(name)) {
|
|
5172
|
-
|
|
5088
|
+
const zeroPrice = {
|
|
5089
|
+
tokenName: name,
|
|
5090
|
+
priceUsd: 0,
|
|
5091
|
+
priceEur: 0,
|
|
5092
|
+
change24h: 0,
|
|
5093
|
+
timestamp: now
|
|
5094
|
+
};
|
|
5095
|
+
this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
|
|
5096
|
+
result.set(name, zeroPrice);
|
|
5173
5097
|
}
|
|
5174
5098
|
}
|
|
5175
5099
|
if (this.debug) {
|
|
5176
5100
|
console.log(`[CoinGecko] Fetched ${result.size} prices`);
|
|
5177
5101
|
}
|
|
5102
|
+
this.saveToStorage();
|
|
5178
5103
|
} catch (error) {
|
|
5179
5104
|
if (this.debug) {
|
|
5180
5105
|
console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
|
|
5181
5106
|
}
|
|
5182
5107
|
for (const name of uncachedNames) {
|
|
5183
5108
|
const stale = this.cache.get(name);
|
|
5184
|
-
if (stale
|
|
5109
|
+
if (stale) {
|
|
5185
5110
|
result.set(name, stale.price);
|
|
5186
5111
|
}
|
|
5187
5112
|
}
|
|
5188
5113
|
}
|
|
5189
5114
|
return result;
|
|
5190
5115
|
}
|
|
5116
|
+
// ===========================================================================
|
|
5117
|
+
// Persistent Storage
|
|
5118
|
+
// ===========================================================================
|
|
5119
|
+
/**
|
|
5120
|
+
* Load cached prices from StorageProvider into in-memory cache.
|
|
5121
|
+
* Only loads entries that are still within cacheTtlMs.
|
|
5122
|
+
*/
|
|
5123
|
+
async loadFromStorage() {
|
|
5124
|
+
if (this.loadCachePromise) {
|
|
5125
|
+
return this.loadCachePromise;
|
|
5126
|
+
}
|
|
5127
|
+
this.loadCachePromise = this.doLoadFromStorage();
|
|
5128
|
+
try {
|
|
5129
|
+
await this.loadCachePromise;
|
|
5130
|
+
} finally {
|
|
5131
|
+
this.loadCachePromise = null;
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
async doLoadFromStorage() {
|
|
5135
|
+
this.persistentCacheLoaded = true;
|
|
5136
|
+
if (!this.storage) return;
|
|
5137
|
+
try {
|
|
5138
|
+
const [cached, cachedTs] = await Promise.all([
|
|
5139
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
|
|
5140
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
|
|
5141
|
+
]);
|
|
5142
|
+
if (!cached || !cachedTs) return;
|
|
5143
|
+
const ts = parseInt(cachedTs, 10);
|
|
5144
|
+
if (isNaN(ts)) return;
|
|
5145
|
+
const age = Date.now() - ts;
|
|
5146
|
+
if (age > this.cacheTtlMs) return;
|
|
5147
|
+
const data = JSON.parse(cached);
|
|
5148
|
+
const expiresAt = ts + this.cacheTtlMs;
|
|
5149
|
+
for (const [name, price] of Object.entries(data)) {
|
|
5150
|
+
if (!this.cache.has(name)) {
|
|
5151
|
+
this.cache.set(name, { price, expiresAt });
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
if (this.debug) {
|
|
5155
|
+
console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
|
|
5156
|
+
}
|
|
5157
|
+
} catch {
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
/**
|
|
5161
|
+
* Save current prices to StorageProvider (fire-and-forget).
|
|
5162
|
+
*/
|
|
5163
|
+
saveToStorage() {
|
|
5164
|
+
if (!this.storage) return;
|
|
5165
|
+
const data = {};
|
|
5166
|
+
for (const [name, entry] of this.cache) {
|
|
5167
|
+
data[name] = entry.price;
|
|
5168
|
+
}
|
|
5169
|
+
Promise.all([
|
|
5170
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
|
|
5171
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
|
|
5172
|
+
]).catch(() => {
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
// ===========================================================================
|
|
5176
|
+
// Rate-limit handling
|
|
5177
|
+
// ===========================================================================
|
|
5178
|
+
/**
|
|
5179
|
+
* On 429 rate-limit, extend stale cache entries so subsequent calls
|
|
5180
|
+
* don't immediately retry and hammer the API.
|
|
5181
|
+
*/
|
|
5182
|
+
extendCacheOnRateLimit(names) {
|
|
5183
|
+
const backoffMs = 6e4;
|
|
5184
|
+
const extendedExpiry = Date.now() + backoffMs;
|
|
5185
|
+
for (const name of names) {
|
|
5186
|
+
const existing = this.cache.get(name);
|
|
5187
|
+
if (existing) {
|
|
5188
|
+
existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
if (this.debug) {
|
|
5192
|
+
console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5191
5195
|
async getPrice(tokenName) {
|
|
5192
5196
|
const prices = await this.getPrices([tokenName]);
|
|
5193
5197
|
return prices.get(tokenName) ?? null;
|
|
@@ -5221,6 +5225,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5221
5225
|
refreshTimer = null;
|
|
5222
5226
|
lastRefreshAt = 0;
|
|
5223
5227
|
refreshPromise = null;
|
|
5228
|
+
initialLoadPromise = null;
|
|
5224
5229
|
constructor() {
|
|
5225
5230
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
5226
5231
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -5259,13 +5264,8 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5259
5264
|
if (options.refreshIntervalMs !== void 0) {
|
|
5260
5265
|
instance.refreshIntervalMs = options.refreshIntervalMs;
|
|
5261
5266
|
}
|
|
5262
|
-
if (instance.storage) {
|
|
5263
|
-
instance.loadFromCache();
|
|
5264
|
-
}
|
|
5265
5267
|
const autoRefresh = options.autoRefresh ?? true;
|
|
5266
|
-
|
|
5267
|
-
instance.startAutoRefresh();
|
|
5268
|
-
}
|
|
5268
|
+
instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
|
|
5269
5269
|
}
|
|
5270
5270
|
/**
|
|
5271
5271
|
* Reset the singleton instance (useful for testing).
|
|
@@ -5283,6 +5283,53 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5283
5283
|
static destroy() {
|
|
5284
5284
|
_TokenRegistry.resetInstance();
|
|
5285
5285
|
}
|
|
5286
|
+
/**
|
|
5287
|
+
* Wait for the initial data load (cache or remote) to complete.
|
|
5288
|
+
* Returns true if data was loaded, false if not (timeout or no data source).
|
|
5289
|
+
*
|
|
5290
|
+
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
5291
|
+
*/
|
|
5292
|
+
static async waitForReady(timeoutMs = 1e4) {
|
|
5293
|
+
const instance = _TokenRegistry.getInstance();
|
|
5294
|
+
if (!instance.initialLoadPromise) {
|
|
5295
|
+
return instance.definitionsById.size > 0;
|
|
5296
|
+
}
|
|
5297
|
+
if (timeoutMs <= 0) {
|
|
5298
|
+
return instance.initialLoadPromise;
|
|
5299
|
+
}
|
|
5300
|
+
return Promise.race([
|
|
5301
|
+
instance.initialLoadPromise,
|
|
5302
|
+
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
5303
|
+
]);
|
|
5304
|
+
}
|
|
5305
|
+
// ===========================================================================
|
|
5306
|
+
// Initial Load
|
|
5307
|
+
// ===========================================================================
|
|
5308
|
+
/**
|
|
5309
|
+
* Perform initial data load: try cache first, fall back to remote fetch.
|
|
5310
|
+
* After initial data is available, start periodic auto-refresh if configured.
|
|
5311
|
+
*/
|
|
5312
|
+
async performInitialLoad(autoRefresh) {
|
|
5313
|
+
let loaded = false;
|
|
5314
|
+
if (this.storage) {
|
|
5315
|
+
loaded = await this.loadFromCache();
|
|
5316
|
+
}
|
|
5317
|
+
if (loaded) {
|
|
5318
|
+
if (autoRefresh && this.remoteUrl) {
|
|
5319
|
+
this.startAutoRefresh();
|
|
5320
|
+
}
|
|
5321
|
+
return true;
|
|
5322
|
+
}
|
|
5323
|
+
if (autoRefresh && this.remoteUrl) {
|
|
5324
|
+
loaded = await this.refreshFromRemote();
|
|
5325
|
+
this.stopAutoRefresh();
|
|
5326
|
+
this.refreshTimer = setInterval(() => {
|
|
5327
|
+
this.refreshFromRemote();
|
|
5328
|
+
}, this.refreshIntervalMs);
|
|
5329
|
+
return loaded;
|
|
5330
|
+
}
|
|
5331
|
+
return false;
|
|
5332
|
+
}
|
|
5286
5333
|
// ===========================================================================
|
|
5287
5334
|
// Cache (StorageProvider)
|
|
5288
5335
|
// ===========================================================================
|
|
@@ -5612,7 +5659,7 @@ function resolveL1Config(network, config) {
|
|
|
5612
5659
|
enableVesting: config.enableVesting
|
|
5613
5660
|
};
|
|
5614
5661
|
}
|
|
5615
|
-
function resolvePriceConfig(config) {
|
|
5662
|
+
function resolvePriceConfig(config, storage) {
|
|
5616
5663
|
if (config === void 0) {
|
|
5617
5664
|
return void 0;
|
|
5618
5665
|
}
|
|
@@ -5622,7 +5669,8 @@ function resolvePriceConfig(config) {
|
|
|
5622
5669
|
baseUrl: config.baseUrl,
|
|
5623
5670
|
cacheTtlMs: config.cacheTtlMs,
|
|
5624
5671
|
timeout: config.timeout,
|
|
5625
|
-
debug: config.debug
|
|
5672
|
+
debug: config.debug,
|
|
5673
|
+
storage
|
|
5626
5674
|
};
|
|
5627
5675
|
}
|
|
5628
5676
|
function resolveArrayConfig(defaults, replace, additional) {
|
|
@@ -5649,16 +5697,6 @@ function resolveGroupChatConfig(network, config) {
|
|
|
5649
5697
|
relays: config.relays ?? [...netConfig.groupRelays]
|
|
5650
5698
|
};
|
|
5651
5699
|
}
|
|
5652
|
-
function resolveMarketConfig(config) {
|
|
5653
|
-
if (!config) return void 0;
|
|
5654
|
-
if (config === true) {
|
|
5655
|
-
return { apiUrl: DEFAULT_MARKET_API_URL };
|
|
5656
|
-
}
|
|
5657
|
-
return {
|
|
5658
|
-
apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
|
|
5659
|
-
timeout: config.timeout
|
|
5660
|
-
};
|
|
5661
|
-
}
|
|
5662
5700
|
|
|
5663
5701
|
// impl/browser/index.ts
|
|
5664
5702
|
if (typeof globalThis.Buffer === "undefined") {
|
|
@@ -5716,8 +5754,8 @@ function createBrowserProviders(config) {
|
|
|
5716
5754
|
const oracleConfig = resolveOracleConfig(network, config?.oracle);
|
|
5717
5755
|
const l1Config = resolveL1Config(network, config?.l1);
|
|
5718
5756
|
const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
|
|
5719
|
-
const priceConfig = resolvePriceConfig(config?.price);
|
|
5720
5757
|
const storage = createLocalStorageProvider(config?.storage);
|
|
5758
|
+
const priceConfig = resolvePriceConfig(config?.price, storage);
|
|
5721
5759
|
const ipfsConfig = tokenSyncConfig?.ipfs;
|
|
5722
5760
|
const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
|
|
5723
5761
|
gateways: ipfsConfig.gateways,
|
|
@@ -5727,11 +5765,9 @@ function createBrowserProviders(config) {
|
|
|
5727
5765
|
const groupChat = resolveGroupChatConfig(network, config?.groupChat);
|
|
5728
5766
|
const networkConfig = getNetworkConfig(network);
|
|
5729
5767
|
TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
|
|
5730
|
-
const market = resolveMarketConfig(config?.market);
|
|
5731
5768
|
return {
|
|
5732
5769
|
storage,
|
|
5733
5770
|
groupChat,
|
|
5734
|
-
market,
|
|
5735
5771
|
transport: createNostrTransportProvider({
|
|
5736
5772
|
relays: transportConfig.relays,
|
|
5737
5773
|
timeout: transportConfig.timeout,
|