@unicitylabs/sphere-sdk 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.cjs +2779 -152
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +226 -1
- package/dist/core/index.d.ts +226 -1
- package/dist/core/index.js +2775 -148
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +552 -36
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +555 -37
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +5 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +5 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +499 -8
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +72 -0
- package/dist/impl/nodejs/index.d.ts +72 -0
- package/dist/impl/nodejs/index.js +502 -9
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +2785 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -7
- package/dist/index.d.ts +327 -7
- package/dist/index.js +2778 -148
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -36,7 +36,11 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
36
36
|
/** Group chat: processed event IDs for deduplication */
|
|
37
37
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
38
38
|
/** Group chat: last used relay URL (stale data detection) */
|
|
39
|
-
GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
|
|
39
|
+
GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
|
|
40
|
+
/** Cached token registry JSON (fetched from remote) */
|
|
41
|
+
TOKEN_REGISTRY_CACHE: "token_registry_cache",
|
|
42
|
+
/** Timestamp of last token registry cache update (ms since epoch) */
|
|
43
|
+
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
|
|
40
44
|
};
|
|
41
45
|
var STORAGE_KEYS_ADDRESS = {
|
|
42
46
|
/** Pending transfers for this address */
|
|
@@ -112,6 +116,8 @@ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
|
112
116
|
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
113
117
|
var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
|
|
114
118
|
var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
|
|
119
|
+
var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
|
|
120
|
+
var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
|
|
115
121
|
var TEST_NOSTR_RELAYS = [
|
|
116
122
|
"wss://nostr-relay.testnet.unicity.network"
|
|
117
123
|
];
|
|
@@ -125,7 +131,8 @@ var NETWORKS = {
|
|
|
125
131
|
nostrRelays: DEFAULT_NOSTR_RELAYS,
|
|
126
132
|
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
127
133
|
electrumUrl: DEFAULT_ELECTRUM_URL,
|
|
128
|
-
groupRelays: DEFAULT_GROUP_RELAYS
|
|
134
|
+
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
135
|
+
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
129
136
|
},
|
|
130
137
|
testnet: {
|
|
131
138
|
name: "Testnet",
|
|
@@ -133,7 +140,8 @@ var NETWORKS = {
|
|
|
133
140
|
nostrRelays: TEST_NOSTR_RELAYS,
|
|
134
141
|
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
135
142
|
electrumUrl: TEST_ELECTRUM_URL,
|
|
136
|
-
groupRelays: DEFAULT_GROUP_RELAYS
|
|
143
|
+
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
144
|
+
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
137
145
|
},
|
|
138
146
|
dev: {
|
|
139
147
|
name: "Development",
|
|
@@ -141,7 +149,8 @@ var NETWORKS = {
|
|
|
141
149
|
nostrRelays: TEST_NOSTR_RELAYS,
|
|
142
150
|
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
143
151
|
electrumUrl: TEST_ELECTRUM_URL,
|
|
144
|
-
groupRelays: DEFAULT_GROUP_RELAYS
|
|
152
|
+
groupRelays: DEFAULT_GROUP_RELAYS,
|
|
153
|
+
tokenRegistryUrl: TOKEN_REGISTRY_URL
|
|
145
154
|
}
|
|
146
155
|
};
|
|
147
156
|
var TIMEOUTS = {
|
|
@@ -156,6 +165,7 @@ var TIMEOUTS = {
|
|
|
156
165
|
/** Sync interval */
|
|
157
166
|
SYNC_INTERVAL: 6e4
|
|
158
167
|
};
|
|
168
|
+
var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
|
|
159
169
|
|
|
160
170
|
// impl/browser/storage/LocalStorageProvider.ts
|
|
161
171
|
var LocalStorageProvider = class {
|
|
@@ -523,36 +533,61 @@ var IndexedDBTokenStorageProvider = class {
|
|
|
523
533
|
return meta !== null;
|
|
524
534
|
}
|
|
525
535
|
async clear() {
|
|
526
|
-
|
|
527
|
-
this.db.close();
|
|
528
|
-
this.db = null;
|
|
529
|
-
}
|
|
530
|
-
this.status = "disconnected";
|
|
531
|
-
const CLEAR_TIMEOUT = 1500;
|
|
532
|
-
const withTimeout = (promise, ms, label) => Promise.race([
|
|
533
|
-
promise,
|
|
534
|
-
new Promise(
|
|
535
|
-
(_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
|
|
536
|
-
)
|
|
537
|
-
]);
|
|
538
|
-
const deleteDb = (name) => new Promise((resolve) => {
|
|
539
|
-
const req = indexedDB.deleteDatabase(name);
|
|
540
|
-
req.onsuccess = () => resolve();
|
|
541
|
-
req.onerror = () => resolve();
|
|
542
|
-
req.onblocked = () => resolve();
|
|
543
|
-
});
|
|
536
|
+
const dbNames = [this.dbName];
|
|
544
537
|
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
545
|
if (typeof indexedDB.databases === "function") {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
546
|
+
try {
|
|
547
|
+
const dbs = await Promise.race([
|
|
548
|
+
indexedDB.databases(),
|
|
549
|
+
new Promise(
|
|
550
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)
|
|
551
|
+
)
|
|
552
|
+
]);
|
|
553
|
+
for (const dbInfo of dbs) {
|
|
554
|
+
if (dbInfo.name && dbInfo.name.startsWith(this.dbNamePrefix) && dbInfo.name !== this.dbName) {
|
|
555
|
+
dbNames.push(dbInfo.name);
|
|
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
|
+
}
|
|
556
591
|
}
|
|
557
592
|
return true;
|
|
558
593
|
} catch (err) {
|
|
@@ -1101,7 +1136,9 @@ import {
|
|
|
1101
1136
|
EventKinds,
|
|
1102
1137
|
hashNametag,
|
|
1103
1138
|
NostrClient,
|
|
1104
|
-
Filter
|
|
1139
|
+
Filter,
|
|
1140
|
+
isChatMessage,
|
|
1141
|
+
isReadReceipt
|
|
1105
1142
|
} from "@unicitylabs/nostr-js-sdk";
|
|
1106
1143
|
|
|
1107
1144
|
// core/crypto.ts
|
|
@@ -1319,6 +1356,8 @@ var NostrTransportProvider = class {
|
|
|
1319
1356
|
transferHandlers = /* @__PURE__ */ new Set();
|
|
1320
1357
|
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
1321
1358
|
paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
|
|
1359
|
+
readReceiptHandlers = /* @__PURE__ */ new Set();
|
|
1360
|
+
typingIndicatorHandlers = /* @__PURE__ */ new Set();
|
|
1322
1361
|
broadcastHandlers = /* @__PURE__ */ new Map();
|
|
1323
1362
|
eventCallbacks = /* @__PURE__ */ new Set();
|
|
1324
1363
|
constructor(config) {
|
|
@@ -1570,6 +1609,18 @@ var NostrTransportProvider = class {
|
|
|
1570
1609
|
const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
|
|
1571
1610
|
const giftWrap = NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
|
|
1572
1611
|
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
|
+
});
|
|
1573
1624
|
this.emitEvent({
|
|
1574
1625
|
type: "message:sent",
|
|
1575
1626
|
timestamp: Date.now(),
|
|
@@ -1668,6 +1719,37 @@ var NostrTransportProvider = class {
|
|
|
1668
1719
|
this.paymentRequestResponseHandlers.add(handler);
|
|
1669
1720
|
return () => this.paymentRequestResponseHandlers.delete(handler);
|
|
1670
1721
|
}
|
|
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
|
+
}
|
|
1671
1753
|
/**
|
|
1672
1754
|
* Resolve any identifier to full peer information.
|
|
1673
1755
|
* Routes to the appropriate specific resolve method based on identifier format.
|
|
@@ -2121,11 +2203,74 @@ var NostrTransportProvider = class {
|
|
|
2121
2203
|
const pm = NIP17.unwrap(event, this.keyManager);
|
|
2122
2204
|
this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
|
|
2123
2205
|
if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
|
|
2124
|
-
|
|
2206
|
+
try {
|
|
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");
|
|
2125
2232
|
return;
|
|
2126
2233
|
}
|
|
2127
|
-
if (pm
|
|
2128
|
-
this.log("
|
|
2234
|
+
if (isReadReceipt(pm)) {
|
|
2235
|
+
this.log("Read receipt from:", pm.senderPubkey?.slice(0, 16), "for:", pm.replyToEventId);
|
|
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);
|
|
2129
2274
|
return;
|
|
2130
2275
|
}
|
|
2131
2276
|
let content = pm.content;
|
|
@@ -2140,7 +2285,9 @@ var NostrTransportProvider = class {
|
|
|
2140
2285
|
}
|
|
2141
2286
|
this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
|
|
2142
2287
|
const message = {
|
|
2143
|
-
id
|
|
2288
|
+
// Use outer gift wrap event.id so it matches the sender's stored giftWrap.id.
|
|
2289
|
+
// This ensures read receipts reference an ID the sender recognizes.
|
|
2290
|
+
id: event.id,
|
|
2144
2291
|
senderTransportPubkey: pm.senderPubkey,
|
|
2145
2292
|
senderNametag,
|
|
2146
2293
|
content,
|
|
@@ -5060,6 +5207,363 @@ function createPriceProvider(config) {
|
|
|
5060
5207
|
}
|
|
5061
5208
|
}
|
|
5062
5209
|
|
|
5210
|
+
// registry/TokenRegistry.ts
|
|
5211
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
5212
|
+
var TokenRegistry = class _TokenRegistry {
|
|
5213
|
+
static instance = null;
|
|
5214
|
+
definitionsById;
|
|
5215
|
+
definitionsBySymbol;
|
|
5216
|
+
definitionsByName;
|
|
5217
|
+
// Remote refresh state
|
|
5218
|
+
remoteUrl = null;
|
|
5219
|
+
storage = null;
|
|
5220
|
+
refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
|
|
5221
|
+
refreshTimer = null;
|
|
5222
|
+
lastRefreshAt = 0;
|
|
5223
|
+
refreshPromise = null;
|
|
5224
|
+
constructor() {
|
|
5225
|
+
this.definitionsById = /* @__PURE__ */ new Map();
|
|
5226
|
+
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
5227
|
+
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
5228
|
+
}
|
|
5229
|
+
/**
|
|
5230
|
+
* Get singleton instance of TokenRegistry
|
|
5231
|
+
*/
|
|
5232
|
+
static getInstance() {
|
|
5233
|
+
if (!_TokenRegistry.instance) {
|
|
5234
|
+
_TokenRegistry.instance = new _TokenRegistry();
|
|
5235
|
+
}
|
|
5236
|
+
return _TokenRegistry.instance;
|
|
5237
|
+
}
|
|
5238
|
+
/**
|
|
5239
|
+
* Configure remote registry refresh with persistent caching.
|
|
5240
|
+
*
|
|
5241
|
+
* On first call:
|
|
5242
|
+
* 1. Loads cached data from StorageProvider (if available and fresh)
|
|
5243
|
+
* 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
|
|
5244
|
+
*
|
|
5245
|
+
* @param options - Configuration options
|
|
5246
|
+
* @param options.remoteUrl - Remote URL to fetch definitions from
|
|
5247
|
+
* @param options.storage - StorageProvider for persistent caching
|
|
5248
|
+
* @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
|
|
5249
|
+
* @param options.autoRefresh - Start auto-refresh immediately (default: true)
|
|
5250
|
+
*/
|
|
5251
|
+
static configure(options) {
|
|
5252
|
+
const instance = _TokenRegistry.getInstance();
|
|
5253
|
+
if (options.remoteUrl !== void 0) {
|
|
5254
|
+
instance.remoteUrl = options.remoteUrl;
|
|
5255
|
+
}
|
|
5256
|
+
if (options.storage !== void 0) {
|
|
5257
|
+
instance.storage = options.storage;
|
|
5258
|
+
}
|
|
5259
|
+
if (options.refreshIntervalMs !== void 0) {
|
|
5260
|
+
instance.refreshIntervalMs = options.refreshIntervalMs;
|
|
5261
|
+
}
|
|
5262
|
+
if (instance.storage) {
|
|
5263
|
+
instance.loadFromCache();
|
|
5264
|
+
}
|
|
5265
|
+
const autoRefresh = options.autoRefresh ?? true;
|
|
5266
|
+
if (autoRefresh && instance.remoteUrl) {
|
|
5267
|
+
instance.startAutoRefresh();
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
* Reset the singleton instance (useful for testing).
|
|
5272
|
+
* Stops auto-refresh if running.
|
|
5273
|
+
*/
|
|
5274
|
+
static resetInstance() {
|
|
5275
|
+
if (_TokenRegistry.instance) {
|
|
5276
|
+
_TokenRegistry.instance.stopAutoRefresh();
|
|
5277
|
+
}
|
|
5278
|
+
_TokenRegistry.instance = null;
|
|
5279
|
+
}
|
|
5280
|
+
/**
|
|
5281
|
+
* Destroy the singleton: stop auto-refresh and reset.
|
|
5282
|
+
*/
|
|
5283
|
+
static destroy() {
|
|
5284
|
+
_TokenRegistry.resetInstance();
|
|
5285
|
+
}
|
|
5286
|
+
// ===========================================================================
|
|
5287
|
+
// Cache (StorageProvider)
|
|
5288
|
+
// ===========================================================================
|
|
5289
|
+
/**
|
|
5290
|
+
* Load definitions from StorageProvider cache.
|
|
5291
|
+
* Only applies if cache exists and is fresh (within refreshIntervalMs).
|
|
5292
|
+
*/
|
|
5293
|
+
async loadFromCache() {
|
|
5294
|
+
if (!this.storage) return false;
|
|
5295
|
+
try {
|
|
5296
|
+
const [cached, cachedTs] = await Promise.all([
|
|
5297
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE),
|
|
5298
|
+
this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS)
|
|
5299
|
+
]);
|
|
5300
|
+
if (!cached || !cachedTs) return false;
|
|
5301
|
+
const ts = parseInt(cachedTs, 10);
|
|
5302
|
+
if (isNaN(ts)) return false;
|
|
5303
|
+
const age = Date.now() - ts;
|
|
5304
|
+
if (age > this.refreshIntervalMs) return false;
|
|
5305
|
+
if (this.lastRefreshAt > ts) return false;
|
|
5306
|
+
const data = JSON.parse(cached);
|
|
5307
|
+
if (!this.isValidDefinitionsArray(data)) return false;
|
|
5308
|
+
this.applyDefinitions(data);
|
|
5309
|
+
this.lastRefreshAt = ts;
|
|
5310
|
+
return true;
|
|
5311
|
+
} catch {
|
|
5312
|
+
return false;
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
/**
|
|
5316
|
+
* Save definitions to StorageProvider cache.
|
|
5317
|
+
*/
|
|
5318
|
+
async saveToCache(definitions) {
|
|
5319
|
+
if (!this.storage) return;
|
|
5320
|
+
try {
|
|
5321
|
+
await Promise.all([
|
|
5322
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE, JSON.stringify(definitions)),
|
|
5323
|
+
this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS, String(Date.now()))
|
|
5324
|
+
]);
|
|
5325
|
+
} catch {
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
// ===========================================================================
|
|
5329
|
+
// Remote Refresh
|
|
5330
|
+
// ===========================================================================
|
|
5331
|
+
/**
|
|
5332
|
+
* Apply an array of token definitions to the internal maps.
|
|
5333
|
+
* Clears existing data before applying.
|
|
5334
|
+
*/
|
|
5335
|
+
applyDefinitions(definitions) {
|
|
5336
|
+
this.definitionsById.clear();
|
|
5337
|
+
this.definitionsBySymbol.clear();
|
|
5338
|
+
this.definitionsByName.clear();
|
|
5339
|
+
for (const def of definitions) {
|
|
5340
|
+
const idLower = def.id.toLowerCase();
|
|
5341
|
+
this.definitionsById.set(idLower, def);
|
|
5342
|
+
if (def.symbol) {
|
|
5343
|
+
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
5344
|
+
}
|
|
5345
|
+
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
/**
|
|
5349
|
+
* Validate that data is an array of objects with 'id' field
|
|
5350
|
+
*/
|
|
5351
|
+
isValidDefinitionsArray(data) {
|
|
5352
|
+
return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
|
|
5353
|
+
}
|
|
5354
|
+
/**
|
|
5355
|
+
* Fetch token definitions from the remote URL and update the registry.
|
|
5356
|
+
* On success, also persists to StorageProvider cache.
|
|
5357
|
+
* Returns true on success, false on failure. On failure, existing data is preserved.
|
|
5358
|
+
* Concurrent calls are deduplicated — only one fetch runs at a time.
|
|
5359
|
+
*/
|
|
5360
|
+
async refreshFromRemote() {
|
|
5361
|
+
if (!this.remoteUrl) {
|
|
5362
|
+
return false;
|
|
5363
|
+
}
|
|
5364
|
+
if (this.refreshPromise) {
|
|
5365
|
+
return this.refreshPromise;
|
|
5366
|
+
}
|
|
5367
|
+
this.refreshPromise = this.doRefresh();
|
|
5368
|
+
try {
|
|
5369
|
+
return await this.refreshPromise;
|
|
5370
|
+
} finally {
|
|
5371
|
+
this.refreshPromise = null;
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
async doRefresh() {
|
|
5375
|
+
try {
|
|
5376
|
+
const controller = new AbortController();
|
|
5377
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
5378
|
+
let response;
|
|
5379
|
+
try {
|
|
5380
|
+
response = await fetch(this.remoteUrl, {
|
|
5381
|
+
headers: { Accept: "application/json" },
|
|
5382
|
+
signal: controller.signal
|
|
5383
|
+
});
|
|
5384
|
+
} finally {
|
|
5385
|
+
clearTimeout(timer);
|
|
5386
|
+
}
|
|
5387
|
+
if (!response.ok) {
|
|
5388
|
+
console.warn(
|
|
5389
|
+
`[TokenRegistry] Remote fetch failed: HTTP ${response.status} ${response.statusText}`
|
|
5390
|
+
);
|
|
5391
|
+
return false;
|
|
5392
|
+
}
|
|
5393
|
+
const data = await response.json();
|
|
5394
|
+
if (!this.isValidDefinitionsArray(data)) {
|
|
5395
|
+
console.warn("[TokenRegistry] Remote data is not a valid token definitions array");
|
|
5396
|
+
return false;
|
|
5397
|
+
}
|
|
5398
|
+
const definitions = data;
|
|
5399
|
+
this.applyDefinitions(definitions);
|
|
5400
|
+
this.lastRefreshAt = Date.now();
|
|
5401
|
+
this.saveToCache(definitions);
|
|
5402
|
+
return true;
|
|
5403
|
+
} catch (error) {
|
|
5404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5405
|
+
console.warn(`[TokenRegistry] Remote refresh failed: ${message}`);
|
|
5406
|
+
return false;
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
/**
|
|
5410
|
+
* Start periodic auto-refresh from the remote URL.
|
|
5411
|
+
* Does an immediate fetch, then repeats at the configured interval.
|
|
5412
|
+
*/
|
|
5413
|
+
startAutoRefresh(intervalMs) {
|
|
5414
|
+
this.stopAutoRefresh();
|
|
5415
|
+
if (intervalMs !== void 0) {
|
|
5416
|
+
this.refreshIntervalMs = intervalMs;
|
|
5417
|
+
}
|
|
5418
|
+
this.refreshFromRemote();
|
|
5419
|
+
this.refreshTimer = setInterval(() => {
|
|
5420
|
+
this.refreshFromRemote();
|
|
5421
|
+
}, this.refreshIntervalMs);
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* Stop periodic auto-refresh
|
|
5425
|
+
*/
|
|
5426
|
+
stopAutoRefresh() {
|
|
5427
|
+
if (this.refreshTimer !== null) {
|
|
5428
|
+
clearInterval(this.refreshTimer);
|
|
5429
|
+
this.refreshTimer = null;
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
/**
|
|
5433
|
+
* Timestamp of the last successful remote refresh (0 if never refreshed)
|
|
5434
|
+
*/
|
|
5435
|
+
getLastRefreshAt() {
|
|
5436
|
+
return this.lastRefreshAt;
|
|
5437
|
+
}
|
|
5438
|
+
// ===========================================================================
|
|
5439
|
+
// Lookup Methods
|
|
5440
|
+
// ===========================================================================
|
|
5441
|
+
/**
|
|
5442
|
+
* Get token definition by hex coin ID
|
|
5443
|
+
* @param coinId - 64-character hex string
|
|
5444
|
+
* @returns Token definition or undefined if not found
|
|
5445
|
+
*/
|
|
5446
|
+
getDefinition(coinId) {
|
|
5447
|
+
if (!coinId) return void 0;
|
|
5448
|
+
return this.definitionsById.get(coinId.toLowerCase());
|
|
5449
|
+
}
|
|
5450
|
+
/**
|
|
5451
|
+
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
5452
|
+
* @param symbol - Token symbol (case-insensitive)
|
|
5453
|
+
* @returns Token definition or undefined if not found
|
|
5454
|
+
*/
|
|
5455
|
+
getDefinitionBySymbol(symbol) {
|
|
5456
|
+
if (!symbol) return void 0;
|
|
5457
|
+
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
5458
|
+
}
|
|
5459
|
+
/**
|
|
5460
|
+
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
5461
|
+
* @param name - Token name (case-insensitive)
|
|
5462
|
+
* @returns Token definition or undefined if not found
|
|
5463
|
+
*/
|
|
5464
|
+
getDefinitionByName(name) {
|
|
5465
|
+
if (!name) return void 0;
|
|
5466
|
+
return this.definitionsByName.get(name.toLowerCase());
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* Get token symbol for a coin ID
|
|
5470
|
+
* @param coinId - 64-character hex string
|
|
5471
|
+
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
5472
|
+
*/
|
|
5473
|
+
getSymbol(coinId) {
|
|
5474
|
+
const def = this.getDefinition(coinId);
|
|
5475
|
+
if (def?.symbol) {
|
|
5476
|
+
return def.symbol;
|
|
5477
|
+
}
|
|
5478
|
+
return coinId.slice(0, 6).toUpperCase();
|
|
5479
|
+
}
|
|
5480
|
+
/**
|
|
5481
|
+
* Get token name for a coin ID
|
|
5482
|
+
* @param coinId - 64-character hex string
|
|
5483
|
+
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
5484
|
+
*/
|
|
5485
|
+
getName(coinId) {
|
|
5486
|
+
const def = this.getDefinition(coinId);
|
|
5487
|
+
if (def?.name) {
|
|
5488
|
+
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
5489
|
+
}
|
|
5490
|
+
return coinId;
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Get decimal places for a coin ID
|
|
5494
|
+
* @param coinId - 64-character hex string
|
|
5495
|
+
* @returns Decimals or 0 if not found
|
|
5496
|
+
*/
|
|
5497
|
+
getDecimals(coinId) {
|
|
5498
|
+
const def = this.getDefinition(coinId);
|
|
5499
|
+
return def?.decimals ?? 0;
|
|
5500
|
+
}
|
|
5501
|
+
/**
|
|
5502
|
+
* Get icon URL for a coin ID
|
|
5503
|
+
* @param coinId - 64-character hex string
|
|
5504
|
+
* @param preferPng - Prefer PNG format over SVG
|
|
5505
|
+
* @returns Icon URL or null if not found
|
|
5506
|
+
*/
|
|
5507
|
+
getIconUrl(coinId, preferPng = true) {
|
|
5508
|
+
const def = this.getDefinition(coinId);
|
|
5509
|
+
if (!def?.icons || def.icons.length === 0) {
|
|
5510
|
+
return null;
|
|
5511
|
+
}
|
|
5512
|
+
if (preferPng) {
|
|
5513
|
+
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
5514
|
+
if (pngIcon) return pngIcon.url;
|
|
5515
|
+
}
|
|
5516
|
+
return def.icons[0].url;
|
|
5517
|
+
}
|
|
5518
|
+
/**
|
|
5519
|
+
* Check if a coin ID is known in the registry
|
|
5520
|
+
* @param coinId - 64-character hex string
|
|
5521
|
+
* @returns true if the coin is in the registry
|
|
5522
|
+
*/
|
|
5523
|
+
isKnown(coinId) {
|
|
5524
|
+
return this.definitionsById.has(coinId.toLowerCase());
|
|
5525
|
+
}
|
|
5526
|
+
/**
|
|
5527
|
+
* Get all token definitions
|
|
5528
|
+
* @returns Array of all token definitions
|
|
5529
|
+
*/
|
|
5530
|
+
getAllDefinitions() {
|
|
5531
|
+
return Array.from(this.definitionsById.values());
|
|
5532
|
+
}
|
|
5533
|
+
/**
|
|
5534
|
+
* Get all fungible token definitions
|
|
5535
|
+
* @returns Array of fungible token definitions
|
|
5536
|
+
*/
|
|
5537
|
+
getFungibleTokens() {
|
|
5538
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
5539
|
+
}
|
|
5540
|
+
/**
|
|
5541
|
+
* Get all non-fungible token definitions
|
|
5542
|
+
* @returns Array of non-fungible token definitions
|
|
5543
|
+
*/
|
|
5544
|
+
getNonFungibleTokens() {
|
|
5545
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
5546
|
+
}
|
|
5547
|
+
/**
|
|
5548
|
+
* Get coin ID by symbol
|
|
5549
|
+
* @param symbol - Token symbol (e.g., "UCT")
|
|
5550
|
+
* @returns Coin ID hex string or undefined if not found
|
|
5551
|
+
*/
|
|
5552
|
+
getCoinIdBySymbol(symbol) {
|
|
5553
|
+
const def = this.getDefinitionBySymbol(symbol);
|
|
5554
|
+
return def?.id;
|
|
5555
|
+
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Get coin ID by name
|
|
5558
|
+
* @param name - Token name (e.g., "bitcoin")
|
|
5559
|
+
* @returns Coin ID hex string or undefined if not found
|
|
5560
|
+
*/
|
|
5561
|
+
getCoinIdByName(name) {
|
|
5562
|
+
const def = this.getDefinitionByName(name);
|
|
5563
|
+
return def?.id;
|
|
5564
|
+
}
|
|
5565
|
+
};
|
|
5566
|
+
|
|
5063
5567
|
// impl/shared/resolvers.ts
|
|
5064
5568
|
function getNetworkConfig(network = "mainnet") {
|
|
5065
5569
|
return NETWORKS[network];
|
|
@@ -5145,6 +5649,16 @@ function resolveGroupChatConfig(network, config) {
|
|
|
5145
5649
|
relays: config.relays ?? [...netConfig.groupRelays]
|
|
5146
5650
|
};
|
|
5147
5651
|
}
|
|
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
|
+
}
|
|
5148
5662
|
|
|
5149
5663
|
// impl/browser/index.ts
|
|
5150
5664
|
if (typeof globalThis.Buffer === "undefined") {
|
|
@@ -5211,9 +5725,13 @@ function createBrowserProviders(config) {
|
|
|
5211
5725
|
// reuse debug-like flag
|
|
5212
5726
|
}) : void 0;
|
|
5213
5727
|
const groupChat = resolveGroupChatConfig(network, config?.groupChat);
|
|
5728
|
+
const networkConfig = getNetworkConfig(network);
|
|
5729
|
+
TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
|
|
5730
|
+
const market = resolveMarketConfig(config?.market);
|
|
5214
5731
|
return {
|
|
5215
5732
|
storage,
|
|
5216
5733
|
groupChat,
|
|
5734
|
+
market,
|
|
5217
5735
|
transport: createNostrTransportProvider({
|
|
5218
5736
|
relays: transportConfig.relays,
|
|
5219
5737
|
timeout: transportConfig.timeout,
|