@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.
@@ -94,7 +94,11 @@ var STORAGE_KEYS_GLOBAL = {
94
94
  /** Group chat: processed event IDs for deduplication */
95
95
  GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
96
96
  /** Group chat: last used relay URL (stale data detection) */
97
- GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
97
+ GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
98
+ /** Cached token registry JSON (fetched from remote) */
99
+ TOKEN_REGISTRY_CACHE: "token_registry_cache",
100
+ /** Timestamp of last token registry cache update (ms since epoch) */
101
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
98
102
  };
99
103
  var STORAGE_KEYS_ADDRESS = {
100
104
  /** Pending transfers for this address */
@@ -170,6 +174,8 @@ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
170
174
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
171
175
  var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
172
176
  var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
177
+ var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
178
+ var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
173
179
  var TEST_NOSTR_RELAYS = [
174
180
  "wss://nostr-relay.testnet.unicity.network"
175
181
  ];
@@ -183,7 +189,8 @@ var NETWORKS = {
183
189
  nostrRelays: DEFAULT_NOSTR_RELAYS,
184
190
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
185
191
  electrumUrl: DEFAULT_ELECTRUM_URL,
186
- groupRelays: DEFAULT_GROUP_RELAYS
192
+ groupRelays: DEFAULT_GROUP_RELAYS,
193
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
187
194
  },
188
195
  testnet: {
189
196
  name: "Testnet",
@@ -191,7 +198,8 @@ var NETWORKS = {
191
198
  nostrRelays: TEST_NOSTR_RELAYS,
192
199
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
193
200
  electrumUrl: TEST_ELECTRUM_URL,
194
- groupRelays: DEFAULT_GROUP_RELAYS
201
+ groupRelays: DEFAULT_GROUP_RELAYS,
202
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
195
203
  },
196
204
  dev: {
197
205
  name: "Development",
@@ -199,7 +207,8 @@ var NETWORKS = {
199
207
  nostrRelays: TEST_NOSTR_RELAYS,
200
208
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
201
209
  electrumUrl: TEST_ELECTRUM_URL,
202
- groupRelays: DEFAULT_GROUP_RELAYS
210
+ groupRelays: DEFAULT_GROUP_RELAYS,
211
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
203
212
  }
204
213
  };
205
214
  var TIMEOUTS = {
@@ -214,6 +223,7 @@ var TIMEOUTS = {
214
223
  /** Sync interval */
215
224
  SYNC_INTERVAL: 6e4
216
225
  };
226
+ var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
217
227
 
218
228
  // impl/browser/storage/LocalStorageProvider.ts
219
229
  var LocalStorageProvider = class {
@@ -581,36 +591,61 @@ var IndexedDBTokenStorageProvider = class {
581
591
  return meta !== null;
582
592
  }
583
593
  async clear() {
584
- if (this.db) {
585
- this.db.close();
586
- this.db = null;
587
- }
588
- this.status = "disconnected";
589
- const CLEAR_TIMEOUT = 1500;
590
- const withTimeout = (promise, ms, label) => Promise.race([
591
- promise,
592
- new Promise(
593
- (_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
594
- )
595
- ]);
596
- const deleteDb = (name) => new Promise((resolve) => {
597
- const req = indexedDB.deleteDatabase(name);
598
- req.onsuccess = () => resolve();
599
- req.onerror = () => resolve();
600
- req.onblocked = () => resolve();
601
- });
594
+ const dbNames = [this.dbName];
602
595
  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
603
  if (typeof indexedDB.databases === "function") {
604
- const dbs = await withTimeout(
605
- indexedDB.databases(),
606
- CLEAR_TIMEOUT,
607
- "indexedDB.databases()"
608
- );
609
- await Promise.all(
610
- dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
611
- );
612
- } else {
613
- await deleteDb(this.dbName);
604
+ try {
605
+ const dbs = await Promise.race([
606
+ indexedDB.databases(),
607
+ new Promise(
608
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)
609
+ )
610
+ ]);
611
+ for (const dbInfo of dbs) {
612
+ if (dbInfo.name && dbInfo.name.startsWith(this.dbNamePrefix) && dbInfo.name !== this.dbName) {
613
+ dbNames.push(dbInfo.name);
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
+ }
614
649
  }
615
650
  return true;
616
651
  } catch (err) {
@@ -1368,6 +1403,8 @@ var NostrTransportProvider = class {
1368
1403
  transferHandlers = /* @__PURE__ */ new Set();
1369
1404
  paymentRequestHandlers = /* @__PURE__ */ new Set();
1370
1405
  paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1406
+ readReceiptHandlers = /* @__PURE__ */ new Set();
1407
+ typingIndicatorHandlers = /* @__PURE__ */ new Set();
1371
1408
  broadcastHandlers = /* @__PURE__ */ new Map();
1372
1409
  eventCallbacks = /* @__PURE__ */ new Set();
1373
1410
  constructor(config) {
@@ -1619,6 +1656,18 @@ var NostrTransportProvider = class {
1619
1656
  const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1620
1657
  const giftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
1621
1658
  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
+ });
1622
1671
  this.emitEvent({
1623
1672
  type: "message:sent",
1624
1673
  timestamp: Date.now(),
@@ -1717,6 +1766,37 @@ var NostrTransportProvider = class {
1717
1766
  this.paymentRequestResponseHandlers.add(handler);
1718
1767
  return () => this.paymentRequestResponseHandlers.delete(handler);
1719
1768
  }
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
+ }
1720
1800
  /**
1721
1801
  * Resolve any identifier to full peer information.
1722
1802
  * Routes to the appropriate specific resolve method based on identifier format.
@@ -2170,11 +2250,74 @@ var NostrTransportProvider = class {
2170
2250
  const pm = import_nostr_js_sdk.NIP17.unwrap(event, this.keyManager);
2171
2251
  this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
2172
2252
  if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
2173
- this.log("Skipping own message");
2253
+ try {
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");
2174
2279
  return;
2175
2280
  }
2176
- if (pm.kind !== import_nostr_js_sdk.EventKinds.CHAT_MESSAGE) {
2177
- this.log("Skipping non-chat message, kind:", pm.kind);
2281
+ if ((0, import_nostr_js_sdk.isReadReceipt)(pm)) {
2282
+ this.log("Read receipt from:", pm.senderPubkey?.slice(0, 16), "for:", pm.replyToEventId);
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);
2178
2321
  return;
2179
2322
  }
2180
2323
  let content = pm.content;
@@ -2189,7 +2332,9 @@ var NostrTransportProvider = class {
2189
2332
  }
2190
2333
  this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
2191
2334
  const message = {
2192
- id: pm.eventId,
2335
+ // Use outer gift wrap event.id so it matches the sender's stored giftWrap.id.
2336
+ // This ensures read receipts reference an ID the sender recognizes.
2337
+ id: event.id,
2193
2338
  senderTransportPubkey: pm.senderPubkey,
2194
2339
  senderNametag,
2195
2340
  content,
@@ -5109,6 +5254,363 @@ function createPriceProvider(config) {
5109
5254
  }
5110
5255
  }
5111
5256
 
5257
+ // registry/TokenRegistry.ts
5258
+ var FETCH_TIMEOUT_MS = 1e4;
5259
+ var TokenRegistry = class _TokenRegistry {
5260
+ static instance = null;
5261
+ definitionsById;
5262
+ definitionsBySymbol;
5263
+ definitionsByName;
5264
+ // Remote refresh state
5265
+ remoteUrl = null;
5266
+ storage = null;
5267
+ refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
5268
+ refreshTimer = null;
5269
+ lastRefreshAt = 0;
5270
+ refreshPromise = null;
5271
+ constructor() {
5272
+ this.definitionsById = /* @__PURE__ */ new Map();
5273
+ this.definitionsBySymbol = /* @__PURE__ */ new Map();
5274
+ this.definitionsByName = /* @__PURE__ */ new Map();
5275
+ }
5276
+ /**
5277
+ * Get singleton instance of TokenRegistry
5278
+ */
5279
+ static getInstance() {
5280
+ if (!_TokenRegistry.instance) {
5281
+ _TokenRegistry.instance = new _TokenRegistry();
5282
+ }
5283
+ return _TokenRegistry.instance;
5284
+ }
5285
+ /**
5286
+ * Configure remote registry refresh with persistent caching.
5287
+ *
5288
+ * On first call:
5289
+ * 1. Loads cached data from StorageProvider (if available and fresh)
5290
+ * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
5291
+ *
5292
+ * @param options - Configuration options
5293
+ * @param options.remoteUrl - Remote URL to fetch definitions from
5294
+ * @param options.storage - StorageProvider for persistent caching
5295
+ * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
5296
+ * @param options.autoRefresh - Start auto-refresh immediately (default: true)
5297
+ */
5298
+ static configure(options) {
5299
+ const instance = _TokenRegistry.getInstance();
5300
+ if (options.remoteUrl !== void 0) {
5301
+ instance.remoteUrl = options.remoteUrl;
5302
+ }
5303
+ if (options.storage !== void 0) {
5304
+ instance.storage = options.storage;
5305
+ }
5306
+ if (options.refreshIntervalMs !== void 0) {
5307
+ instance.refreshIntervalMs = options.refreshIntervalMs;
5308
+ }
5309
+ if (instance.storage) {
5310
+ instance.loadFromCache();
5311
+ }
5312
+ const autoRefresh = options.autoRefresh ?? true;
5313
+ if (autoRefresh && instance.remoteUrl) {
5314
+ instance.startAutoRefresh();
5315
+ }
5316
+ }
5317
+ /**
5318
+ * Reset the singleton instance (useful for testing).
5319
+ * Stops auto-refresh if running.
5320
+ */
5321
+ static resetInstance() {
5322
+ if (_TokenRegistry.instance) {
5323
+ _TokenRegistry.instance.stopAutoRefresh();
5324
+ }
5325
+ _TokenRegistry.instance = null;
5326
+ }
5327
+ /**
5328
+ * Destroy the singleton: stop auto-refresh and reset.
5329
+ */
5330
+ static destroy() {
5331
+ _TokenRegistry.resetInstance();
5332
+ }
5333
+ // ===========================================================================
5334
+ // Cache (StorageProvider)
5335
+ // ===========================================================================
5336
+ /**
5337
+ * Load definitions from StorageProvider cache.
5338
+ * Only applies if cache exists and is fresh (within refreshIntervalMs).
5339
+ */
5340
+ async loadFromCache() {
5341
+ if (!this.storage) return false;
5342
+ try {
5343
+ const [cached, cachedTs] = await Promise.all([
5344
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE),
5345
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS)
5346
+ ]);
5347
+ if (!cached || !cachedTs) return false;
5348
+ const ts = parseInt(cachedTs, 10);
5349
+ if (isNaN(ts)) return false;
5350
+ const age = Date.now() - ts;
5351
+ if (age > this.refreshIntervalMs) return false;
5352
+ if (this.lastRefreshAt > ts) return false;
5353
+ const data = JSON.parse(cached);
5354
+ if (!this.isValidDefinitionsArray(data)) return false;
5355
+ this.applyDefinitions(data);
5356
+ this.lastRefreshAt = ts;
5357
+ return true;
5358
+ } catch {
5359
+ return false;
5360
+ }
5361
+ }
5362
+ /**
5363
+ * Save definitions to StorageProvider cache.
5364
+ */
5365
+ async saveToCache(definitions) {
5366
+ if (!this.storage) return;
5367
+ try {
5368
+ await Promise.all([
5369
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE, JSON.stringify(definitions)),
5370
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS, String(Date.now()))
5371
+ ]);
5372
+ } catch {
5373
+ }
5374
+ }
5375
+ // ===========================================================================
5376
+ // Remote Refresh
5377
+ // ===========================================================================
5378
+ /**
5379
+ * Apply an array of token definitions to the internal maps.
5380
+ * Clears existing data before applying.
5381
+ */
5382
+ applyDefinitions(definitions) {
5383
+ this.definitionsById.clear();
5384
+ this.definitionsBySymbol.clear();
5385
+ this.definitionsByName.clear();
5386
+ for (const def of definitions) {
5387
+ const idLower = def.id.toLowerCase();
5388
+ this.definitionsById.set(idLower, def);
5389
+ if (def.symbol) {
5390
+ this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
5391
+ }
5392
+ this.definitionsByName.set(def.name.toLowerCase(), def);
5393
+ }
5394
+ }
5395
+ /**
5396
+ * Validate that data is an array of objects with 'id' field
5397
+ */
5398
+ isValidDefinitionsArray(data) {
5399
+ return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
5400
+ }
5401
+ /**
5402
+ * Fetch token definitions from the remote URL and update the registry.
5403
+ * On success, also persists to StorageProvider cache.
5404
+ * Returns true on success, false on failure. On failure, existing data is preserved.
5405
+ * Concurrent calls are deduplicated — only one fetch runs at a time.
5406
+ */
5407
+ async refreshFromRemote() {
5408
+ if (!this.remoteUrl) {
5409
+ return false;
5410
+ }
5411
+ if (this.refreshPromise) {
5412
+ return this.refreshPromise;
5413
+ }
5414
+ this.refreshPromise = this.doRefresh();
5415
+ try {
5416
+ return await this.refreshPromise;
5417
+ } finally {
5418
+ this.refreshPromise = null;
5419
+ }
5420
+ }
5421
+ async doRefresh() {
5422
+ try {
5423
+ const controller = new AbortController();
5424
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
5425
+ let response;
5426
+ try {
5427
+ response = await fetch(this.remoteUrl, {
5428
+ headers: { Accept: "application/json" },
5429
+ signal: controller.signal
5430
+ });
5431
+ } finally {
5432
+ clearTimeout(timer);
5433
+ }
5434
+ if (!response.ok) {
5435
+ console.warn(
5436
+ `[TokenRegistry] Remote fetch failed: HTTP ${response.status} ${response.statusText}`
5437
+ );
5438
+ return false;
5439
+ }
5440
+ const data = await response.json();
5441
+ if (!this.isValidDefinitionsArray(data)) {
5442
+ console.warn("[TokenRegistry] Remote data is not a valid token definitions array");
5443
+ return false;
5444
+ }
5445
+ const definitions = data;
5446
+ this.applyDefinitions(definitions);
5447
+ this.lastRefreshAt = Date.now();
5448
+ this.saveToCache(definitions);
5449
+ return true;
5450
+ } catch (error) {
5451
+ const message = error instanceof Error ? error.message : String(error);
5452
+ console.warn(`[TokenRegistry] Remote refresh failed: ${message}`);
5453
+ return false;
5454
+ }
5455
+ }
5456
+ /**
5457
+ * Start periodic auto-refresh from the remote URL.
5458
+ * Does an immediate fetch, then repeats at the configured interval.
5459
+ */
5460
+ startAutoRefresh(intervalMs) {
5461
+ this.stopAutoRefresh();
5462
+ if (intervalMs !== void 0) {
5463
+ this.refreshIntervalMs = intervalMs;
5464
+ }
5465
+ this.refreshFromRemote();
5466
+ this.refreshTimer = setInterval(() => {
5467
+ this.refreshFromRemote();
5468
+ }, this.refreshIntervalMs);
5469
+ }
5470
+ /**
5471
+ * Stop periodic auto-refresh
5472
+ */
5473
+ stopAutoRefresh() {
5474
+ if (this.refreshTimer !== null) {
5475
+ clearInterval(this.refreshTimer);
5476
+ this.refreshTimer = null;
5477
+ }
5478
+ }
5479
+ /**
5480
+ * Timestamp of the last successful remote refresh (0 if never refreshed)
5481
+ */
5482
+ getLastRefreshAt() {
5483
+ return this.lastRefreshAt;
5484
+ }
5485
+ // ===========================================================================
5486
+ // Lookup Methods
5487
+ // ===========================================================================
5488
+ /**
5489
+ * Get token definition by hex coin ID
5490
+ * @param coinId - 64-character hex string
5491
+ * @returns Token definition or undefined if not found
5492
+ */
5493
+ getDefinition(coinId) {
5494
+ if (!coinId) return void 0;
5495
+ return this.definitionsById.get(coinId.toLowerCase());
5496
+ }
5497
+ /**
5498
+ * Get token definition by symbol (e.g., "UCT", "BTC")
5499
+ * @param symbol - Token symbol (case-insensitive)
5500
+ * @returns Token definition or undefined if not found
5501
+ */
5502
+ getDefinitionBySymbol(symbol) {
5503
+ if (!symbol) return void 0;
5504
+ return this.definitionsBySymbol.get(symbol.toUpperCase());
5505
+ }
5506
+ /**
5507
+ * Get token definition by name (e.g., "bitcoin", "ethereum")
5508
+ * @param name - Token name (case-insensitive)
5509
+ * @returns Token definition or undefined if not found
5510
+ */
5511
+ getDefinitionByName(name) {
5512
+ if (!name) return void 0;
5513
+ return this.definitionsByName.get(name.toLowerCase());
5514
+ }
5515
+ /**
5516
+ * Get token symbol for a coin ID
5517
+ * @param coinId - 64-character hex string
5518
+ * @returns Symbol (e.g., "UCT") or truncated ID if not found
5519
+ */
5520
+ getSymbol(coinId) {
5521
+ const def = this.getDefinition(coinId);
5522
+ if (def?.symbol) {
5523
+ return def.symbol;
5524
+ }
5525
+ return coinId.slice(0, 6).toUpperCase();
5526
+ }
5527
+ /**
5528
+ * Get token name for a coin ID
5529
+ * @param coinId - 64-character hex string
5530
+ * @returns Name (e.g., "Bitcoin") or coin ID if not found
5531
+ */
5532
+ getName(coinId) {
5533
+ const def = this.getDefinition(coinId);
5534
+ if (def?.name) {
5535
+ return def.name.charAt(0).toUpperCase() + def.name.slice(1);
5536
+ }
5537
+ return coinId;
5538
+ }
5539
+ /**
5540
+ * Get decimal places for a coin ID
5541
+ * @param coinId - 64-character hex string
5542
+ * @returns Decimals or 0 if not found
5543
+ */
5544
+ getDecimals(coinId) {
5545
+ const def = this.getDefinition(coinId);
5546
+ return def?.decimals ?? 0;
5547
+ }
5548
+ /**
5549
+ * Get icon URL for a coin ID
5550
+ * @param coinId - 64-character hex string
5551
+ * @param preferPng - Prefer PNG format over SVG
5552
+ * @returns Icon URL or null if not found
5553
+ */
5554
+ getIconUrl(coinId, preferPng = true) {
5555
+ const def = this.getDefinition(coinId);
5556
+ if (!def?.icons || def.icons.length === 0) {
5557
+ return null;
5558
+ }
5559
+ if (preferPng) {
5560
+ const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
5561
+ if (pngIcon) return pngIcon.url;
5562
+ }
5563
+ return def.icons[0].url;
5564
+ }
5565
+ /**
5566
+ * Check if a coin ID is known in the registry
5567
+ * @param coinId - 64-character hex string
5568
+ * @returns true if the coin is in the registry
5569
+ */
5570
+ isKnown(coinId) {
5571
+ return this.definitionsById.has(coinId.toLowerCase());
5572
+ }
5573
+ /**
5574
+ * Get all token definitions
5575
+ * @returns Array of all token definitions
5576
+ */
5577
+ getAllDefinitions() {
5578
+ return Array.from(this.definitionsById.values());
5579
+ }
5580
+ /**
5581
+ * Get all fungible token definitions
5582
+ * @returns Array of fungible token definitions
5583
+ */
5584
+ getFungibleTokens() {
5585
+ return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
5586
+ }
5587
+ /**
5588
+ * Get all non-fungible token definitions
5589
+ * @returns Array of non-fungible token definitions
5590
+ */
5591
+ getNonFungibleTokens() {
5592
+ return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
5593
+ }
5594
+ /**
5595
+ * Get coin ID by symbol
5596
+ * @param symbol - Token symbol (e.g., "UCT")
5597
+ * @returns Coin ID hex string or undefined if not found
5598
+ */
5599
+ getCoinIdBySymbol(symbol) {
5600
+ const def = this.getDefinitionBySymbol(symbol);
5601
+ return def?.id;
5602
+ }
5603
+ /**
5604
+ * Get coin ID by name
5605
+ * @param name - Token name (e.g., "bitcoin")
5606
+ * @returns Coin ID hex string or undefined if not found
5607
+ */
5608
+ getCoinIdByName(name) {
5609
+ const def = this.getDefinitionByName(name);
5610
+ return def?.id;
5611
+ }
5612
+ };
5613
+
5112
5614
  // impl/shared/resolvers.ts
5113
5615
  function getNetworkConfig(network = "mainnet") {
5114
5616
  return NETWORKS[network];
@@ -5194,6 +5696,16 @@ function resolveGroupChatConfig(network, config) {
5194
5696
  relays: config.relays ?? [...netConfig.groupRelays]
5195
5697
  };
5196
5698
  }
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
+ }
5197
5709
 
5198
5710
  // impl/browser/index.ts
5199
5711
  if (typeof globalThis.Buffer === "undefined") {
@@ -5260,9 +5772,13 @@ function createBrowserProviders(config) {
5260
5772
  // reuse debug-like flag
5261
5773
  }) : void 0;
5262
5774
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5775
+ const networkConfig = getNetworkConfig(network);
5776
+ TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5777
+ const market = resolveMarketConfig(config?.market);
5263
5778
  return {
5264
5779
  storage,
5265
5780
  groupChat,
5781
+ market,
5266
5782
  transport: createNostrTransportProvider({
5267
5783
  relays: transportConfig.relays,
5268
5784
  timeout: transportConfig.timeout,