@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
|
@@ -98,7 +98,11 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
98
98
|
/** Cached token registry JSON (fetched from remote) */
|
|
99
99
|
TOKEN_REGISTRY_CACHE: "token_registry_cache",
|
|
100
100
|
/** Timestamp of last token registry cache update (ms since epoch) */
|
|
101
|
-
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
|
|
101
|
+
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
|
|
102
|
+
/** Cached price data JSON (from CoinGecko or other provider) */
|
|
103
|
+
PRICE_CACHE: "price_cache",
|
|
104
|
+
/** Timestamp of last price cache update (ms since epoch) */
|
|
105
|
+
PRICE_CACHE_TS: "price_cache_ts"
|
|
102
106
|
};
|
|
103
107
|
var STORAGE_KEYS_ADDRESS = {
|
|
104
108
|
/** Pending transfers for this address */
|
|
@@ -223,7 +227,6 @@ var TIMEOUTS = {
|
|
|
223
227
|
/** Sync interval */
|
|
224
228
|
SYNC_INTERVAL: 6e4
|
|
225
229
|
};
|
|
226
|
-
var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
|
|
227
230
|
|
|
228
231
|
// impl/browser/storage/LocalStorageProvider.ts
|
|
229
232
|
var LocalStorageProvider = class {
|
|
@@ -591,61 +594,36 @@ var IndexedDBTokenStorageProvider = class {
|
|
|
591
594
|
return meta !== null;
|
|
592
595
|
}
|
|
593
596
|
async clear() {
|
|
594
|
-
|
|
597
|
+
if (this.db) {
|
|
598
|
+
this.db.close();
|
|
599
|
+
this.db = null;
|
|
600
|
+
}
|
|
601
|
+
this.status = "disconnected";
|
|
602
|
+
const CLEAR_TIMEOUT = 1500;
|
|
603
|
+
const withTimeout = (promise, ms, label) => Promise.race([
|
|
604
|
+
promise,
|
|
605
|
+
new Promise(
|
|
606
|
+
(_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
|
|
607
|
+
)
|
|
608
|
+
]);
|
|
609
|
+
const deleteDb = (name) => new Promise((resolve) => {
|
|
610
|
+
const req = indexedDB.deleteDatabase(name);
|
|
611
|
+
req.onsuccess = () => resolve();
|
|
612
|
+
req.onerror = () => resolve();
|
|
613
|
+
req.onblocked = () => resolve();
|
|
614
|
+
});
|
|
595
615
|
try {
|
|
596
|
-
if (this.db) {
|
|
597
|
-
await this.clearStore(STORE_TOKENS);
|
|
598
|
-
await this.clearStore(STORE_META);
|
|
599
|
-
this.db.close();
|
|
600
|
-
this.db = null;
|
|
601
|
-
}
|
|
602
|
-
this.status = "disconnected";
|
|
603
616
|
if (typeof indexedDB.databases === "function") {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
const db = await new Promise((resolve, reject) => {
|
|
616
|
-
const req = indexedDB.open(dbInfo.name, DB_VERSION);
|
|
617
|
-
req.onsuccess = () => resolve(req.result);
|
|
618
|
-
req.onerror = () => reject(req.error);
|
|
619
|
-
req.onupgradeneeded = (e) => {
|
|
620
|
-
const d = e.target.result;
|
|
621
|
-
if (!d.objectStoreNames.contains(STORE_TOKENS)) d.createObjectStore(STORE_TOKENS, { keyPath: "id" });
|
|
622
|
-
if (!d.objectStoreNames.contains(STORE_META)) d.createObjectStore(STORE_META);
|
|
623
|
-
};
|
|
624
|
-
});
|
|
625
|
-
const clearTx = db.transaction([STORE_TOKENS, STORE_META], "readwrite");
|
|
626
|
-
clearTx.objectStore(STORE_TOKENS).clear();
|
|
627
|
-
clearTx.objectStore(STORE_META).clear();
|
|
628
|
-
await new Promise((resolve) => {
|
|
629
|
-
clearTx.oncomplete = () => resolve();
|
|
630
|
-
clearTx.onerror = () => resolve();
|
|
631
|
-
});
|
|
632
|
-
db.close();
|
|
633
|
-
} catch {
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
} catch {
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
for (const name of dbNames) {
|
|
641
|
-
try {
|
|
642
|
-
const req = indexedDB.deleteDatabase(name);
|
|
643
|
-
req.onerror = () => {
|
|
644
|
-
};
|
|
645
|
-
req.onblocked = () => {
|
|
646
|
-
};
|
|
647
|
-
} catch {
|
|
648
|
-
}
|
|
617
|
+
const dbs = await withTimeout(
|
|
618
|
+
indexedDB.databases(),
|
|
619
|
+
CLEAR_TIMEOUT,
|
|
620
|
+
"indexedDB.databases()"
|
|
621
|
+
);
|
|
622
|
+
await Promise.all(
|
|
623
|
+
dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
await deleteDb(this.dbName);
|
|
649
627
|
}
|
|
650
628
|
return true;
|
|
651
629
|
} catch (err) {
|
|
@@ -1403,8 +1381,6 @@ var NostrTransportProvider = class {
|
|
|
1403
1381
|
transferHandlers = /* @__PURE__ */ new Set();
|
|
1404
1382
|
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
1405
1383
|
paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
|
|
1406
|
-
readReceiptHandlers = /* @__PURE__ */ new Set();
|
|
1407
|
-
typingIndicatorHandlers = /* @__PURE__ */ new Set();
|
|
1408
1384
|
broadcastHandlers = /* @__PURE__ */ new Map();
|
|
1409
1385
|
eventCallbacks = /* @__PURE__ */ new Set();
|
|
1410
1386
|
constructor(config) {
|
|
@@ -1656,18 +1632,6 @@ var NostrTransportProvider = class {
|
|
|
1656
1632
|
const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
|
|
1657
1633
|
const giftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
|
|
1658
1634
|
await this.publishEvent(giftWrap);
|
|
1659
|
-
const selfWrapContent = JSON.stringify({
|
|
1660
|
-
selfWrap: true,
|
|
1661
|
-
originalId: giftWrap.id,
|
|
1662
|
-
recipientPubkey,
|
|
1663
|
-
senderNametag,
|
|
1664
|
-
text: content
|
|
1665
|
-
});
|
|
1666
|
-
const selfPubkey = this.keyManager.getPublicKeyHex();
|
|
1667
|
-
const selfGiftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, selfPubkey, selfWrapContent);
|
|
1668
|
-
this.publishEvent(selfGiftWrap).catch((err) => {
|
|
1669
|
-
this.log("Self-wrap publish failed:", err);
|
|
1670
|
-
});
|
|
1671
1635
|
this.emitEvent({
|
|
1672
1636
|
type: "message:sent",
|
|
1673
1637
|
timestamp: Date.now(),
|
|
@@ -1766,37 +1730,6 @@ var NostrTransportProvider = class {
|
|
|
1766
1730
|
this.paymentRequestResponseHandlers.add(handler);
|
|
1767
1731
|
return () => this.paymentRequestResponseHandlers.delete(handler);
|
|
1768
1732
|
}
|
|
1769
|
-
// ===========================================================================
|
|
1770
|
-
// Read Receipts
|
|
1771
|
-
// ===========================================================================
|
|
1772
|
-
async sendReadReceipt(recipientTransportPubkey, messageEventId) {
|
|
1773
|
-
if (!this.keyManager) throw new Error("Not initialized");
|
|
1774
|
-
const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
|
|
1775
|
-
const event = import_nostr_js_sdk.NIP17.createReadReceipt(this.keyManager, nostrRecipient, messageEventId);
|
|
1776
|
-
await this.publishEvent(event);
|
|
1777
|
-
this.log("Sent read receipt for:", messageEventId, "to:", nostrRecipient.slice(0, 16));
|
|
1778
|
-
}
|
|
1779
|
-
onReadReceipt(handler) {
|
|
1780
|
-
this.readReceiptHandlers.add(handler);
|
|
1781
|
-
return () => this.readReceiptHandlers.delete(handler);
|
|
1782
|
-
}
|
|
1783
|
-
// ===========================================================================
|
|
1784
|
-
// Typing Indicators
|
|
1785
|
-
// ===========================================================================
|
|
1786
|
-
async sendTypingIndicator(recipientTransportPubkey) {
|
|
1787
|
-
if (!this.keyManager) throw new Error("Not initialized");
|
|
1788
|
-
const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
|
|
1789
|
-
const content = JSON.stringify({
|
|
1790
|
-
type: "typing",
|
|
1791
|
-
senderNametag: this.identity?.nametag
|
|
1792
|
-
});
|
|
1793
|
-
const event = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, content);
|
|
1794
|
-
await this.publishEvent(event);
|
|
1795
|
-
}
|
|
1796
|
-
onTypingIndicator(handler) {
|
|
1797
|
-
this.typingIndicatorHandlers.add(handler);
|
|
1798
|
-
return () => this.typingIndicatorHandlers.delete(handler);
|
|
1799
|
-
}
|
|
1800
1733
|
/**
|
|
1801
1734
|
* Resolve any identifier to full peer information.
|
|
1802
1735
|
* Routes to the appropriate specific resolve method based on identifier format.
|
|
@@ -2250,74 +2183,11 @@ var NostrTransportProvider = class {
|
|
|
2250
2183
|
const pm = import_nostr_js_sdk.NIP17.unwrap(event, this.keyManager);
|
|
2251
2184
|
this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
|
|
2252
2185
|
if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
|
|
2253
|
-
|
|
2254
|
-
const parsed = JSON.parse(pm.content);
|
|
2255
|
-
if (parsed?.selfWrap && parsed.recipientPubkey) {
|
|
2256
|
-
this.log("Self-wrap replay for recipient:", parsed.recipientPubkey?.slice(0, 16));
|
|
2257
|
-
const message2 = {
|
|
2258
|
-
id: parsed.originalId || pm.eventId,
|
|
2259
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2260
|
-
senderNametag: parsed.senderNametag,
|
|
2261
|
-
recipientTransportPubkey: parsed.recipientPubkey,
|
|
2262
|
-
content: parsed.text ?? "",
|
|
2263
|
-
timestamp: pm.timestamp * 1e3,
|
|
2264
|
-
encrypted: true,
|
|
2265
|
-
isSelfWrap: true
|
|
2266
|
-
};
|
|
2267
|
-
for (const handler of this.messageHandlers) {
|
|
2268
|
-
try {
|
|
2269
|
-
handler(message2);
|
|
2270
|
-
} catch (e) {
|
|
2271
|
-
this.log("Self-wrap handler error:", e);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
return;
|
|
2275
|
-
}
|
|
2276
|
-
} catch {
|
|
2277
|
-
}
|
|
2278
|
-
this.log("Skipping own non-self-wrap message");
|
|
2186
|
+
this.log("Skipping own message");
|
|
2279
2187
|
return;
|
|
2280
2188
|
}
|
|
2281
|
-
if (
|
|
2282
|
-
this.log("
|
|
2283
|
-
if (pm.replyToEventId) {
|
|
2284
|
-
const receipt = {
|
|
2285
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2286
|
-
messageEventId: pm.replyToEventId,
|
|
2287
|
-
timestamp: pm.timestamp * 1e3
|
|
2288
|
-
};
|
|
2289
|
-
for (const handler of this.readReceiptHandlers) {
|
|
2290
|
-
try {
|
|
2291
|
-
handler(receipt);
|
|
2292
|
-
} catch (e) {
|
|
2293
|
-
this.log("Read receipt handler error:", e);
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
try {
|
|
2300
|
-
const parsed = JSON.parse(pm.content);
|
|
2301
|
-
if (parsed?.type === "typing") {
|
|
2302
|
-
this.log("Typing indicator from:", pm.senderPubkey?.slice(0, 16));
|
|
2303
|
-
const indicator = {
|
|
2304
|
-
senderTransportPubkey: pm.senderPubkey,
|
|
2305
|
-
senderNametag: parsed.senderNametag,
|
|
2306
|
-
timestamp: pm.timestamp * 1e3
|
|
2307
|
-
};
|
|
2308
|
-
for (const handler of this.typingIndicatorHandlers) {
|
|
2309
|
-
try {
|
|
2310
|
-
handler(indicator);
|
|
2311
|
-
} catch (e) {
|
|
2312
|
-
this.log("Typing handler error:", e);
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
} catch {
|
|
2318
|
-
}
|
|
2319
|
-
if (!(0, import_nostr_js_sdk.isChatMessage)(pm)) {
|
|
2320
|
-
this.log("Skipping unknown message kind:", pm.kind);
|
|
2189
|
+
if (pm.kind !== import_nostr_js_sdk.EventKinds.CHAT_MESSAGE) {
|
|
2190
|
+
this.log("Skipping non-chat message, kind:", pm.kind);
|
|
2321
2191
|
return;
|
|
2322
2192
|
}
|
|
2323
2193
|
let content = pm.content;
|
|
@@ -2332,9 +2202,7 @@ var NostrTransportProvider = class {
|
|
|
2332
2202
|
}
|
|
2333
2203
|
this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
|
|
2334
2204
|
const message = {
|
|
2335
|
-
|
|
2336
|
-
// This ensures read receipts reference an ID the sender recognizes.
|
|
2337
|
-
id: event.id,
|
|
2205
|
+
id: pm.eventId,
|
|
2338
2206
|
senderTransportPubkey: pm.senderPubkey,
|
|
2339
2207
|
senderNametag,
|
|
2340
2208
|
content,
|
|
@@ -3412,6 +3280,7 @@ async function loadIpnsModule() {
|
|
|
3412
3280
|
async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
|
|
3413
3281
|
const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
|
|
3414
3282
|
const record = await createIPNSRecord(
|
|
3283
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3415
3284
|
keyPair,
|
|
3416
3285
|
`/ipfs/${cid}`,
|
|
3417
3286
|
sequenceNumber,
|
|
@@ -5156,26 +5025,37 @@ var CoinGeckoPriceProvider = class {
|
|
|
5156
5025
|
timeout;
|
|
5157
5026
|
debug;
|
|
5158
5027
|
baseUrl;
|
|
5028
|
+
storage;
|
|
5029
|
+
/** In-flight fetch promise for deduplication of concurrent getPrices() calls */
|
|
5030
|
+
fetchPromise = null;
|
|
5031
|
+
/** Token names being fetched in the current in-flight request */
|
|
5032
|
+
fetchNames = null;
|
|
5033
|
+
/** Whether persistent cache has been loaded into memory */
|
|
5034
|
+
persistentCacheLoaded = false;
|
|
5035
|
+
/** Promise for loading persistent cache (deduplication) */
|
|
5036
|
+
loadCachePromise = null;
|
|
5159
5037
|
constructor(config) {
|
|
5160
5038
|
this.apiKey = config?.apiKey;
|
|
5161
5039
|
this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
|
|
5162
5040
|
this.timeout = config?.timeout ?? 1e4;
|
|
5163
5041
|
this.debug = config?.debug ?? false;
|
|
5042
|
+
this.storage = config?.storage ?? null;
|
|
5164
5043
|
this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
|
|
5165
5044
|
}
|
|
5166
5045
|
async getPrices(tokenNames) {
|
|
5167
5046
|
if (tokenNames.length === 0) {
|
|
5168
5047
|
return /* @__PURE__ */ new Map();
|
|
5169
5048
|
}
|
|
5049
|
+
if (!this.persistentCacheLoaded && this.storage) {
|
|
5050
|
+
await this.loadFromStorage();
|
|
5051
|
+
}
|
|
5170
5052
|
const now = Date.now();
|
|
5171
5053
|
const result = /* @__PURE__ */ new Map();
|
|
5172
5054
|
const uncachedNames = [];
|
|
5173
5055
|
for (const name of tokenNames) {
|
|
5174
5056
|
const cached = this.cache.get(name);
|
|
5175
5057
|
if (cached && cached.expiresAt > now) {
|
|
5176
|
-
|
|
5177
|
-
result.set(name, cached.price);
|
|
5178
|
-
}
|
|
5058
|
+
result.set(name, cached.price);
|
|
5179
5059
|
} else {
|
|
5180
5060
|
uncachedNames.push(name);
|
|
5181
5061
|
}
|
|
@@ -5183,6 +5063,41 @@ var CoinGeckoPriceProvider = class {
|
|
|
5183
5063
|
if (uncachedNames.length === 0) {
|
|
5184
5064
|
return result;
|
|
5185
5065
|
}
|
|
5066
|
+
if (this.fetchPromise && this.fetchNames) {
|
|
5067
|
+
const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
|
|
5068
|
+
if (allCovered) {
|
|
5069
|
+
if (this.debug) {
|
|
5070
|
+
console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
|
|
5071
|
+
}
|
|
5072
|
+
const fetched = await this.fetchPromise;
|
|
5073
|
+
for (const name of uncachedNames) {
|
|
5074
|
+
const price = fetched.get(name);
|
|
5075
|
+
if (price) {
|
|
5076
|
+
result.set(name, price);
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
return result;
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
const fetchPromise = this.doFetch(uncachedNames);
|
|
5083
|
+
this.fetchPromise = fetchPromise;
|
|
5084
|
+
this.fetchNames = new Set(uncachedNames);
|
|
5085
|
+
try {
|
|
5086
|
+
const fetched = await fetchPromise;
|
|
5087
|
+
for (const [name, price] of fetched) {
|
|
5088
|
+
result.set(name, price);
|
|
5089
|
+
}
|
|
5090
|
+
} finally {
|
|
5091
|
+
if (this.fetchPromise === fetchPromise) {
|
|
5092
|
+
this.fetchPromise = null;
|
|
5093
|
+
this.fetchNames = null;
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
return result;
|
|
5097
|
+
}
|
|
5098
|
+
async doFetch(uncachedNames) {
|
|
5099
|
+
const result = /* @__PURE__ */ new Map();
|
|
5100
|
+
const now = Date.now();
|
|
5186
5101
|
try {
|
|
5187
5102
|
const ids = uncachedNames.join(",");
|
|
5188
5103
|
const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
|
|
@@ -5198,6 +5113,9 @@ var CoinGeckoPriceProvider = class {
|
|
|
5198
5113
|
signal: AbortSignal.timeout(this.timeout)
|
|
5199
5114
|
});
|
|
5200
5115
|
if (!response.ok) {
|
|
5116
|
+
if (response.status === 429) {
|
|
5117
|
+
this.extendCacheOnRateLimit(uncachedNames);
|
|
5118
|
+
}
|
|
5201
5119
|
throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
|
|
5202
5120
|
}
|
|
5203
5121
|
const data = await response.json();
|
|
@@ -5216,25 +5134,113 @@ var CoinGeckoPriceProvider = class {
|
|
|
5216
5134
|
}
|
|
5217
5135
|
for (const name of uncachedNames) {
|
|
5218
5136
|
if (!result.has(name)) {
|
|
5219
|
-
|
|
5137
|
+
const zeroPrice = {
|
|
5138
|
+
tokenName: name,
|
|
5139
|
+
priceUsd: 0,
|
|
5140
|
+
priceEur: 0,
|
|
5141
|
+
change24h: 0,
|
|
5142
|
+
timestamp: now
|
|
5143
|
+
};
|
|
5144
|
+
this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
|
|
5145
|
+
result.set(name, zeroPrice);
|
|
5220
5146
|
}
|
|
5221
5147
|
}
|
|
5222
5148
|
if (this.debug) {
|
|
5223
5149
|
console.log(`[CoinGecko] Fetched ${result.size} prices`);
|
|
5224
5150
|
}
|
|
5151
|
+
this.saveToStorage();
|
|
5225
5152
|
} catch (error) {
|
|
5226
5153
|
if (this.debug) {
|
|
5227
5154
|
console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
|
|
5228
5155
|
}
|
|
5229
5156
|
for (const name of uncachedNames) {
|
|
5230
5157
|
const stale = this.cache.get(name);
|
|
5231
|
-
if (stale
|
|
5158
|
+
if (stale) {
|
|
5232
5159
|
result.set(name, stale.price);
|
|
5233
5160
|
}
|
|
5234
5161
|
}
|
|
5235
5162
|
}
|
|
5236
5163
|
return result;
|
|
5237
5164
|
}
|
|
5165
|
+
// ===========================================================================
|
|
5166
|
+
// Persistent Storage
|
|
5167
|
+
// ===========================================================================
|
|
5168
|
+
/**
|
|
5169
|
+
* Load cached prices from StorageProvider into in-memory cache.
|
|
5170
|
+
* Only loads entries that are still within cacheTtlMs.
|
|
5171
|
+
*/
|
|
5172
|
+
async loadFromStorage() {
|
|
5173
|
+
if (this.loadCachePromise) {
|
|
5174
|
+
return this.loadCachePromise;
|
|
5175
|
+
}
|
|
5176
|
+
this.loadCachePromise = this.doLoadFromStorage();
|
|
5177
|
+
try {
|
|
5178
|
+
await this.loadCachePromise;
|
|
5179
|
+
} finally {
|
|
5180
|
+
this.loadCachePromise = null;
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
async doLoadFromStorage() {
|
|
5184
|
+
this.persistentCacheLoaded = true;
|
|
5185
|
+
if (!this.storage) return;
|
|
5186
|
+
try {
|
|
5187
|
+
const [cached, cachedTs] = await Promise.all([
|
|
5188
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
|
|
5189
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
|
|
5190
|
+
]);
|
|
5191
|
+
if (!cached || !cachedTs) return;
|
|
5192
|
+
const ts = parseInt(cachedTs, 10);
|
|
5193
|
+
if (isNaN(ts)) return;
|
|
5194
|
+
const age = Date.now() - ts;
|
|
5195
|
+
if (age > this.cacheTtlMs) return;
|
|
5196
|
+
const data = JSON.parse(cached);
|
|
5197
|
+
const expiresAt = ts + this.cacheTtlMs;
|
|
5198
|
+
for (const [name, price] of Object.entries(data)) {
|
|
5199
|
+
if (!this.cache.has(name)) {
|
|
5200
|
+
this.cache.set(name, { price, expiresAt });
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
if (this.debug) {
|
|
5204
|
+
console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
|
|
5205
|
+
}
|
|
5206
|
+
} catch {
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
/**
|
|
5210
|
+
* Save current prices to StorageProvider (fire-and-forget).
|
|
5211
|
+
*/
|
|
5212
|
+
saveToStorage() {
|
|
5213
|
+
if (!this.storage) return;
|
|
5214
|
+
const data = {};
|
|
5215
|
+
for (const [name, entry] of this.cache) {
|
|
5216
|
+
data[name] = entry.price;
|
|
5217
|
+
}
|
|
5218
|
+
Promise.all([
|
|
5219
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
|
|
5220
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
|
|
5221
|
+
]).catch(() => {
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
// ===========================================================================
|
|
5225
|
+
// Rate-limit handling
|
|
5226
|
+
// ===========================================================================
|
|
5227
|
+
/**
|
|
5228
|
+
* On 429 rate-limit, extend stale cache entries so subsequent calls
|
|
5229
|
+
* don't immediately retry and hammer the API.
|
|
5230
|
+
*/
|
|
5231
|
+
extendCacheOnRateLimit(names) {
|
|
5232
|
+
const backoffMs = 6e4;
|
|
5233
|
+
const extendedExpiry = Date.now() + backoffMs;
|
|
5234
|
+
for (const name of names) {
|
|
5235
|
+
const existing = this.cache.get(name);
|
|
5236
|
+
if (existing) {
|
|
5237
|
+
existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
if (this.debug) {
|
|
5241
|
+
console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5238
5244
|
async getPrice(tokenName) {
|
|
5239
5245
|
const prices = await this.getPrices([tokenName]);
|
|
5240
5246
|
return prices.get(tokenName) ?? null;
|
|
@@ -5268,6 +5274,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5268
5274
|
refreshTimer = null;
|
|
5269
5275
|
lastRefreshAt = 0;
|
|
5270
5276
|
refreshPromise = null;
|
|
5277
|
+
initialLoadPromise = null;
|
|
5271
5278
|
constructor() {
|
|
5272
5279
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
5273
5280
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -5306,13 +5313,8 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5306
5313
|
if (options.refreshIntervalMs !== void 0) {
|
|
5307
5314
|
instance.refreshIntervalMs = options.refreshIntervalMs;
|
|
5308
5315
|
}
|
|
5309
|
-
if (instance.storage) {
|
|
5310
|
-
instance.loadFromCache();
|
|
5311
|
-
}
|
|
5312
5316
|
const autoRefresh = options.autoRefresh ?? true;
|
|
5313
|
-
|
|
5314
|
-
instance.startAutoRefresh();
|
|
5315
|
-
}
|
|
5317
|
+
instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
|
|
5316
5318
|
}
|
|
5317
5319
|
/**
|
|
5318
5320
|
* Reset the singleton instance (useful for testing).
|
|
@@ -5330,6 +5332,53 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
5330
5332
|
static destroy() {
|
|
5331
5333
|
_TokenRegistry.resetInstance();
|
|
5332
5334
|
}
|
|
5335
|
+
/**
|
|
5336
|
+
* Wait for the initial data load (cache or remote) to complete.
|
|
5337
|
+
* Returns true if data was loaded, false if not (timeout or no data source).
|
|
5338
|
+
*
|
|
5339
|
+
* @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
|
|
5340
|
+
*/
|
|
5341
|
+
static async waitForReady(timeoutMs = 1e4) {
|
|
5342
|
+
const instance = _TokenRegistry.getInstance();
|
|
5343
|
+
if (!instance.initialLoadPromise) {
|
|
5344
|
+
return instance.definitionsById.size > 0;
|
|
5345
|
+
}
|
|
5346
|
+
if (timeoutMs <= 0) {
|
|
5347
|
+
return instance.initialLoadPromise;
|
|
5348
|
+
}
|
|
5349
|
+
return Promise.race([
|
|
5350
|
+
instance.initialLoadPromise,
|
|
5351
|
+
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
|
|
5352
|
+
]);
|
|
5353
|
+
}
|
|
5354
|
+
// ===========================================================================
|
|
5355
|
+
// Initial Load
|
|
5356
|
+
// ===========================================================================
|
|
5357
|
+
/**
|
|
5358
|
+
* Perform initial data load: try cache first, fall back to remote fetch.
|
|
5359
|
+
* After initial data is available, start periodic auto-refresh if configured.
|
|
5360
|
+
*/
|
|
5361
|
+
async performInitialLoad(autoRefresh) {
|
|
5362
|
+
let loaded = false;
|
|
5363
|
+
if (this.storage) {
|
|
5364
|
+
loaded = await this.loadFromCache();
|
|
5365
|
+
}
|
|
5366
|
+
if (loaded) {
|
|
5367
|
+
if (autoRefresh && this.remoteUrl) {
|
|
5368
|
+
this.startAutoRefresh();
|
|
5369
|
+
}
|
|
5370
|
+
return true;
|
|
5371
|
+
}
|
|
5372
|
+
if (autoRefresh && this.remoteUrl) {
|
|
5373
|
+
loaded = await this.refreshFromRemote();
|
|
5374
|
+
this.stopAutoRefresh();
|
|
5375
|
+
this.refreshTimer = setInterval(() => {
|
|
5376
|
+
this.refreshFromRemote();
|
|
5377
|
+
}, this.refreshIntervalMs);
|
|
5378
|
+
return loaded;
|
|
5379
|
+
}
|
|
5380
|
+
return false;
|
|
5381
|
+
}
|
|
5333
5382
|
// ===========================================================================
|
|
5334
5383
|
// Cache (StorageProvider)
|
|
5335
5384
|
// ===========================================================================
|
|
@@ -5659,7 +5708,7 @@ function resolveL1Config(network, config) {
|
|
|
5659
5708
|
enableVesting: config.enableVesting
|
|
5660
5709
|
};
|
|
5661
5710
|
}
|
|
5662
|
-
function resolvePriceConfig(config) {
|
|
5711
|
+
function resolvePriceConfig(config, storage) {
|
|
5663
5712
|
if (config === void 0) {
|
|
5664
5713
|
return void 0;
|
|
5665
5714
|
}
|
|
@@ -5669,7 +5718,8 @@ function resolvePriceConfig(config) {
|
|
|
5669
5718
|
baseUrl: config.baseUrl,
|
|
5670
5719
|
cacheTtlMs: config.cacheTtlMs,
|
|
5671
5720
|
timeout: config.timeout,
|
|
5672
|
-
debug: config.debug
|
|
5721
|
+
debug: config.debug,
|
|
5722
|
+
storage
|
|
5673
5723
|
};
|
|
5674
5724
|
}
|
|
5675
5725
|
function resolveArrayConfig(defaults, replace, additional) {
|
|
@@ -5696,16 +5746,6 @@ function resolveGroupChatConfig(network, config) {
|
|
|
5696
5746
|
relays: config.relays ?? [...netConfig.groupRelays]
|
|
5697
5747
|
};
|
|
5698
5748
|
}
|
|
5699
|
-
function resolveMarketConfig(config) {
|
|
5700
|
-
if (!config) return void 0;
|
|
5701
|
-
if (config === true) {
|
|
5702
|
-
return { apiUrl: DEFAULT_MARKET_API_URL };
|
|
5703
|
-
}
|
|
5704
|
-
return {
|
|
5705
|
-
apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
|
|
5706
|
-
timeout: config.timeout
|
|
5707
|
-
};
|
|
5708
|
-
}
|
|
5709
5749
|
|
|
5710
5750
|
// impl/browser/index.ts
|
|
5711
5751
|
if (typeof globalThis.Buffer === "undefined") {
|
|
@@ -5763,8 +5803,8 @@ function createBrowserProviders(config) {
|
|
|
5763
5803
|
const oracleConfig = resolveOracleConfig(network, config?.oracle);
|
|
5764
5804
|
const l1Config = resolveL1Config(network, config?.l1);
|
|
5765
5805
|
const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
|
|
5766
|
-
const priceConfig = resolvePriceConfig(config?.price);
|
|
5767
5806
|
const storage = createLocalStorageProvider(config?.storage);
|
|
5807
|
+
const priceConfig = resolvePriceConfig(config?.price, storage);
|
|
5768
5808
|
const ipfsConfig = tokenSyncConfig?.ipfs;
|
|
5769
5809
|
const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
|
|
5770
5810
|
gateways: ipfsConfig.gateways,
|
|
@@ -5774,11 +5814,9 @@ function createBrowserProviders(config) {
|
|
|
5774
5814
|
const groupChat = resolveGroupChatConfig(network, config?.groupChat);
|
|
5775
5815
|
const networkConfig = getNetworkConfig(network);
|
|
5776
5816
|
TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
|
|
5777
|
-
const market = resolveMarketConfig(config?.market);
|
|
5778
5817
|
return {
|
|
5779
5818
|
storage,
|
|
5780
5819
|
groupChat,
|
|
5781
|
-
market,
|
|
5782
5820
|
transport: createNostrTransportProvider({
|
|
5783
5821
|
relays: transportConfig.relays,
|
|
5784
5822
|
timeout: transportConfig.timeout,
|