@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.
@@ -86,7 +86,11 @@ var STORAGE_KEYS_GLOBAL = {
86
86
  /** Group chat: processed event IDs for deduplication */
87
87
  GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
88
88
  /** Group chat: last used relay URL (stale data detection) */
89
- GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
89
+ GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
90
+ /** Cached token registry JSON (fetched from remote) */
91
+ TOKEN_REGISTRY_CACHE: "token_registry_cache",
92
+ /** Timestamp of last token registry cache update (ms since epoch) */
93
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
90
94
  };
91
95
  var STORAGE_KEYS_ADDRESS = {
92
96
  /** Pending transfers for this address */
@@ -162,6 +166,8 @@ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
162
166
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
163
167
  var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
164
168
  var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
169
+ var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
170
+ var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
165
171
  var TEST_NOSTR_RELAYS = [
166
172
  "wss://nostr-relay.testnet.unicity.network"
167
173
  ];
@@ -175,7 +181,8 @@ var NETWORKS = {
175
181
  nostrRelays: DEFAULT_NOSTR_RELAYS,
176
182
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
177
183
  electrumUrl: DEFAULT_ELECTRUM_URL,
178
- groupRelays: DEFAULT_GROUP_RELAYS
184
+ groupRelays: DEFAULT_GROUP_RELAYS,
185
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
179
186
  },
180
187
  testnet: {
181
188
  name: "Testnet",
@@ -183,7 +190,8 @@ var NETWORKS = {
183
190
  nostrRelays: TEST_NOSTR_RELAYS,
184
191
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
185
192
  electrumUrl: TEST_ELECTRUM_URL,
186
- groupRelays: DEFAULT_GROUP_RELAYS
193
+ groupRelays: DEFAULT_GROUP_RELAYS,
194
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
187
195
  },
188
196
  dev: {
189
197
  name: "Development",
@@ -191,7 +199,8 @@ var NETWORKS = {
191
199
  nostrRelays: TEST_NOSTR_RELAYS,
192
200
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
193
201
  electrumUrl: TEST_ELECTRUM_URL,
194
- groupRelays: DEFAULT_GROUP_RELAYS
202
+ groupRelays: DEFAULT_GROUP_RELAYS,
203
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
195
204
  }
196
205
  };
197
206
  var TIMEOUTS = {
@@ -206,6 +215,7 @@ var TIMEOUTS = {
206
215
  /** Sync interval */
207
216
  SYNC_INTERVAL: 6e4
208
217
  };
218
+ var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
209
219
 
210
220
  // impl/nodejs/storage/FileStorageProvider.ts
211
221
  var FileStorageProvider = class {
@@ -1186,6 +1196,8 @@ var NostrTransportProvider = class {
1186
1196
  transferHandlers = /* @__PURE__ */ new Set();
1187
1197
  paymentRequestHandlers = /* @__PURE__ */ new Set();
1188
1198
  paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1199
+ readReceiptHandlers = /* @__PURE__ */ new Set();
1200
+ typingIndicatorHandlers = /* @__PURE__ */ new Set();
1189
1201
  broadcastHandlers = /* @__PURE__ */ new Map();
1190
1202
  eventCallbacks = /* @__PURE__ */ new Set();
1191
1203
  constructor(config) {
@@ -1437,6 +1449,18 @@ var NostrTransportProvider = class {
1437
1449
  const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1438
1450
  const giftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
1439
1451
  await this.publishEvent(giftWrap);
1452
+ const selfWrapContent = JSON.stringify({
1453
+ selfWrap: true,
1454
+ originalId: giftWrap.id,
1455
+ recipientPubkey,
1456
+ senderNametag,
1457
+ text: content
1458
+ });
1459
+ const selfPubkey = this.keyManager.getPublicKeyHex();
1460
+ const selfGiftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, selfPubkey, selfWrapContent);
1461
+ this.publishEvent(selfGiftWrap).catch((err) => {
1462
+ this.log("Self-wrap publish failed:", err);
1463
+ });
1440
1464
  this.emitEvent({
1441
1465
  type: "message:sent",
1442
1466
  timestamp: Date.now(),
@@ -1535,6 +1559,37 @@ var NostrTransportProvider = class {
1535
1559
  this.paymentRequestResponseHandlers.add(handler);
1536
1560
  return () => this.paymentRequestResponseHandlers.delete(handler);
1537
1561
  }
1562
+ // ===========================================================================
1563
+ // Read Receipts
1564
+ // ===========================================================================
1565
+ async sendReadReceipt(recipientTransportPubkey, messageEventId) {
1566
+ if (!this.keyManager) throw new Error("Not initialized");
1567
+ const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1568
+ const event = import_nostr_js_sdk.NIP17.createReadReceipt(this.keyManager, nostrRecipient, messageEventId);
1569
+ await this.publishEvent(event);
1570
+ this.log("Sent read receipt for:", messageEventId, "to:", nostrRecipient.slice(0, 16));
1571
+ }
1572
+ onReadReceipt(handler) {
1573
+ this.readReceiptHandlers.add(handler);
1574
+ return () => this.readReceiptHandlers.delete(handler);
1575
+ }
1576
+ // ===========================================================================
1577
+ // Typing Indicators
1578
+ // ===========================================================================
1579
+ async sendTypingIndicator(recipientTransportPubkey) {
1580
+ if (!this.keyManager) throw new Error("Not initialized");
1581
+ const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1582
+ const content = JSON.stringify({
1583
+ type: "typing",
1584
+ senderNametag: this.identity?.nametag
1585
+ });
1586
+ const event = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, content);
1587
+ await this.publishEvent(event);
1588
+ }
1589
+ onTypingIndicator(handler) {
1590
+ this.typingIndicatorHandlers.add(handler);
1591
+ return () => this.typingIndicatorHandlers.delete(handler);
1592
+ }
1538
1593
  /**
1539
1594
  * Resolve any identifier to full peer information.
1540
1595
  * Routes to the appropriate specific resolve method based on identifier format.
@@ -1988,11 +2043,74 @@ var NostrTransportProvider = class {
1988
2043
  const pm = import_nostr_js_sdk.NIP17.unwrap(event, this.keyManager);
1989
2044
  this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
1990
2045
  if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
1991
- this.log("Skipping own message");
2046
+ try {
2047
+ const parsed = JSON.parse(pm.content);
2048
+ if (parsed?.selfWrap && parsed.recipientPubkey) {
2049
+ this.log("Self-wrap replay for recipient:", parsed.recipientPubkey?.slice(0, 16));
2050
+ const message2 = {
2051
+ id: parsed.originalId || pm.eventId,
2052
+ senderTransportPubkey: pm.senderPubkey,
2053
+ senderNametag: parsed.senderNametag,
2054
+ recipientTransportPubkey: parsed.recipientPubkey,
2055
+ content: parsed.text ?? "",
2056
+ timestamp: pm.timestamp * 1e3,
2057
+ encrypted: true,
2058
+ isSelfWrap: true
2059
+ };
2060
+ for (const handler of this.messageHandlers) {
2061
+ try {
2062
+ handler(message2);
2063
+ } catch (e) {
2064
+ this.log("Self-wrap handler error:", e);
2065
+ }
2066
+ }
2067
+ return;
2068
+ }
2069
+ } catch {
2070
+ }
2071
+ this.log("Skipping own non-self-wrap message");
2072
+ return;
2073
+ }
2074
+ if ((0, import_nostr_js_sdk.isReadReceipt)(pm)) {
2075
+ this.log("Read receipt from:", pm.senderPubkey?.slice(0, 16), "for:", pm.replyToEventId);
2076
+ if (pm.replyToEventId) {
2077
+ const receipt = {
2078
+ senderTransportPubkey: pm.senderPubkey,
2079
+ messageEventId: pm.replyToEventId,
2080
+ timestamp: pm.timestamp * 1e3
2081
+ };
2082
+ for (const handler of this.readReceiptHandlers) {
2083
+ try {
2084
+ handler(receipt);
2085
+ } catch (e) {
2086
+ this.log("Read receipt handler error:", e);
2087
+ }
2088
+ }
2089
+ }
1992
2090
  return;
1993
2091
  }
1994
- if (pm.kind !== import_nostr_js_sdk.EventKinds.CHAT_MESSAGE) {
1995
- this.log("Skipping non-chat message, kind:", pm.kind);
2092
+ try {
2093
+ const parsed = JSON.parse(pm.content);
2094
+ if (parsed?.type === "typing") {
2095
+ this.log("Typing indicator from:", pm.senderPubkey?.slice(0, 16));
2096
+ const indicator = {
2097
+ senderTransportPubkey: pm.senderPubkey,
2098
+ senderNametag: parsed.senderNametag,
2099
+ timestamp: pm.timestamp * 1e3
2100
+ };
2101
+ for (const handler of this.typingIndicatorHandlers) {
2102
+ try {
2103
+ handler(indicator);
2104
+ } catch (e) {
2105
+ this.log("Typing handler error:", e);
2106
+ }
2107
+ }
2108
+ return;
2109
+ }
2110
+ } catch {
2111
+ }
2112
+ if (!(0, import_nostr_js_sdk.isChatMessage)(pm)) {
2113
+ this.log("Skipping unknown message kind:", pm.kind);
1996
2114
  return;
1997
2115
  }
1998
2116
  let content = pm.content;
@@ -2007,7 +2125,9 @@ var NostrTransportProvider = class {
2007
2125
  }
2008
2126
  this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
2009
2127
  const message = {
2010
- id: pm.eventId,
2128
+ // Use outer gift wrap event.id so it matches the sender's stored giftWrap.id.
2129
+ // This ensures read receipts reference an ID the sender recognizes.
2130
+ id: event.id,
2011
2131
  senderTransportPubkey: pm.senderPubkey,
2012
2132
  senderNametag,
2013
2133
  content,
@@ -4871,6 +4991,363 @@ function createPriceProvider(config) {
4871
4991
  }
4872
4992
  }
4873
4993
 
4994
+ // registry/TokenRegistry.ts
4995
+ var FETCH_TIMEOUT_MS = 1e4;
4996
+ var TokenRegistry = class _TokenRegistry {
4997
+ static instance = null;
4998
+ definitionsById;
4999
+ definitionsBySymbol;
5000
+ definitionsByName;
5001
+ // Remote refresh state
5002
+ remoteUrl = null;
5003
+ storage = null;
5004
+ refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
5005
+ refreshTimer = null;
5006
+ lastRefreshAt = 0;
5007
+ refreshPromise = null;
5008
+ constructor() {
5009
+ this.definitionsById = /* @__PURE__ */ new Map();
5010
+ this.definitionsBySymbol = /* @__PURE__ */ new Map();
5011
+ this.definitionsByName = /* @__PURE__ */ new Map();
5012
+ }
5013
+ /**
5014
+ * Get singleton instance of TokenRegistry
5015
+ */
5016
+ static getInstance() {
5017
+ if (!_TokenRegistry.instance) {
5018
+ _TokenRegistry.instance = new _TokenRegistry();
5019
+ }
5020
+ return _TokenRegistry.instance;
5021
+ }
5022
+ /**
5023
+ * Configure remote registry refresh with persistent caching.
5024
+ *
5025
+ * On first call:
5026
+ * 1. Loads cached data from StorageProvider (if available and fresh)
5027
+ * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
5028
+ *
5029
+ * @param options - Configuration options
5030
+ * @param options.remoteUrl - Remote URL to fetch definitions from
5031
+ * @param options.storage - StorageProvider for persistent caching
5032
+ * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
5033
+ * @param options.autoRefresh - Start auto-refresh immediately (default: true)
5034
+ */
5035
+ static configure(options) {
5036
+ const instance = _TokenRegistry.getInstance();
5037
+ if (options.remoteUrl !== void 0) {
5038
+ instance.remoteUrl = options.remoteUrl;
5039
+ }
5040
+ if (options.storage !== void 0) {
5041
+ instance.storage = options.storage;
5042
+ }
5043
+ if (options.refreshIntervalMs !== void 0) {
5044
+ instance.refreshIntervalMs = options.refreshIntervalMs;
5045
+ }
5046
+ if (instance.storage) {
5047
+ instance.loadFromCache();
5048
+ }
5049
+ const autoRefresh = options.autoRefresh ?? true;
5050
+ if (autoRefresh && instance.remoteUrl) {
5051
+ instance.startAutoRefresh();
5052
+ }
5053
+ }
5054
+ /**
5055
+ * Reset the singleton instance (useful for testing).
5056
+ * Stops auto-refresh if running.
5057
+ */
5058
+ static resetInstance() {
5059
+ if (_TokenRegistry.instance) {
5060
+ _TokenRegistry.instance.stopAutoRefresh();
5061
+ }
5062
+ _TokenRegistry.instance = null;
5063
+ }
5064
+ /**
5065
+ * Destroy the singleton: stop auto-refresh and reset.
5066
+ */
5067
+ static destroy() {
5068
+ _TokenRegistry.resetInstance();
5069
+ }
5070
+ // ===========================================================================
5071
+ // Cache (StorageProvider)
5072
+ // ===========================================================================
5073
+ /**
5074
+ * Load definitions from StorageProvider cache.
5075
+ * Only applies if cache exists and is fresh (within refreshIntervalMs).
5076
+ */
5077
+ async loadFromCache() {
5078
+ if (!this.storage) return false;
5079
+ try {
5080
+ const [cached, cachedTs] = await Promise.all([
5081
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE),
5082
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS)
5083
+ ]);
5084
+ if (!cached || !cachedTs) return false;
5085
+ const ts = parseInt(cachedTs, 10);
5086
+ if (isNaN(ts)) return false;
5087
+ const age = Date.now() - ts;
5088
+ if (age > this.refreshIntervalMs) return false;
5089
+ if (this.lastRefreshAt > ts) return false;
5090
+ const data = JSON.parse(cached);
5091
+ if (!this.isValidDefinitionsArray(data)) return false;
5092
+ this.applyDefinitions(data);
5093
+ this.lastRefreshAt = ts;
5094
+ return true;
5095
+ } catch {
5096
+ return false;
5097
+ }
5098
+ }
5099
+ /**
5100
+ * Save definitions to StorageProvider cache.
5101
+ */
5102
+ async saveToCache(definitions) {
5103
+ if (!this.storage) return;
5104
+ try {
5105
+ await Promise.all([
5106
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE, JSON.stringify(definitions)),
5107
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS, String(Date.now()))
5108
+ ]);
5109
+ } catch {
5110
+ }
5111
+ }
5112
+ // ===========================================================================
5113
+ // Remote Refresh
5114
+ // ===========================================================================
5115
+ /**
5116
+ * Apply an array of token definitions to the internal maps.
5117
+ * Clears existing data before applying.
5118
+ */
5119
+ applyDefinitions(definitions) {
5120
+ this.definitionsById.clear();
5121
+ this.definitionsBySymbol.clear();
5122
+ this.definitionsByName.clear();
5123
+ for (const def of definitions) {
5124
+ const idLower = def.id.toLowerCase();
5125
+ this.definitionsById.set(idLower, def);
5126
+ if (def.symbol) {
5127
+ this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
5128
+ }
5129
+ this.definitionsByName.set(def.name.toLowerCase(), def);
5130
+ }
5131
+ }
5132
+ /**
5133
+ * Validate that data is an array of objects with 'id' field
5134
+ */
5135
+ isValidDefinitionsArray(data) {
5136
+ return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
5137
+ }
5138
+ /**
5139
+ * Fetch token definitions from the remote URL and update the registry.
5140
+ * On success, also persists to StorageProvider cache.
5141
+ * Returns true on success, false on failure. On failure, existing data is preserved.
5142
+ * Concurrent calls are deduplicated — only one fetch runs at a time.
5143
+ */
5144
+ async refreshFromRemote() {
5145
+ if (!this.remoteUrl) {
5146
+ return false;
5147
+ }
5148
+ if (this.refreshPromise) {
5149
+ return this.refreshPromise;
5150
+ }
5151
+ this.refreshPromise = this.doRefresh();
5152
+ try {
5153
+ return await this.refreshPromise;
5154
+ } finally {
5155
+ this.refreshPromise = null;
5156
+ }
5157
+ }
5158
+ async doRefresh() {
5159
+ try {
5160
+ const controller = new AbortController();
5161
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
5162
+ let response;
5163
+ try {
5164
+ response = await fetch(this.remoteUrl, {
5165
+ headers: { Accept: "application/json" },
5166
+ signal: controller.signal
5167
+ });
5168
+ } finally {
5169
+ clearTimeout(timer);
5170
+ }
5171
+ if (!response.ok) {
5172
+ console.warn(
5173
+ `[TokenRegistry] Remote fetch failed: HTTP ${response.status} ${response.statusText}`
5174
+ );
5175
+ return false;
5176
+ }
5177
+ const data = await response.json();
5178
+ if (!this.isValidDefinitionsArray(data)) {
5179
+ console.warn("[TokenRegistry] Remote data is not a valid token definitions array");
5180
+ return false;
5181
+ }
5182
+ const definitions = data;
5183
+ this.applyDefinitions(definitions);
5184
+ this.lastRefreshAt = Date.now();
5185
+ this.saveToCache(definitions);
5186
+ return true;
5187
+ } catch (error) {
5188
+ const message = error instanceof Error ? error.message : String(error);
5189
+ console.warn(`[TokenRegistry] Remote refresh failed: ${message}`);
5190
+ return false;
5191
+ }
5192
+ }
5193
+ /**
5194
+ * Start periodic auto-refresh from the remote URL.
5195
+ * Does an immediate fetch, then repeats at the configured interval.
5196
+ */
5197
+ startAutoRefresh(intervalMs) {
5198
+ this.stopAutoRefresh();
5199
+ if (intervalMs !== void 0) {
5200
+ this.refreshIntervalMs = intervalMs;
5201
+ }
5202
+ this.refreshFromRemote();
5203
+ this.refreshTimer = setInterval(() => {
5204
+ this.refreshFromRemote();
5205
+ }, this.refreshIntervalMs);
5206
+ }
5207
+ /**
5208
+ * Stop periodic auto-refresh
5209
+ */
5210
+ stopAutoRefresh() {
5211
+ if (this.refreshTimer !== null) {
5212
+ clearInterval(this.refreshTimer);
5213
+ this.refreshTimer = null;
5214
+ }
5215
+ }
5216
+ /**
5217
+ * Timestamp of the last successful remote refresh (0 if never refreshed)
5218
+ */
5219
+ getLastRefreshAt() {
5220
+ return this.lastRefreshAt;
5221
+ }
5222
+ // ===========================================================================
5223
+ // Lookup Methods
5224
+ // ===========================================================================
5225
+ /**
5226
+ * Get token definition by hex coin ID
5227
+ * @param coinId - 64-character hex string
5228
+ * @returns Token definition or undefined if not found
5229
+ */
5230
+ getDefinition(coinId) {
5231
+ if (!coinId) return void 0;
5232
+ return this.definitionsById.get(coinId.toLowerCase());
5233
+ }
5234
+ /**
5235
+ * Get token definition by symbol (e.g., "UCT", "BTC")
5236
+ * @param symbol - Token symbol (case-insensitive)
5237
+ * @returns Token definition or undefined if not found
5238
+ */
5239
+ getDefinitionBySymbol(symbol) {
5240
+ if (!symbol) return void 0;
5241
+ return this.definitionsBySymbol.get(symbol.toUpperCase());
5242
+ }
5243
+ /**
5244
+ * Get token definition by name (e.g., "bitcoin", "ethereum")
5245
+ * @param name - Token name (case-insensitive)
5246
+ * @returns Token definition or undefined if not found
5247
+ */
5248
+ getDefinitionByName(name) {
5249
+ if (!name) return void 0;
5250
+ return this.definitionsByName.get(name.toLowerCase());
5251
+ }
5252
+ /**
5253
+ * Get token symbol for a coin ID
5254
+ * @param coinId - 64-character hex string
5255
+ * @returns Symbol (e.g., "UCT") or truncated ID if not found
5256
+ */
5257
+ getSymbol(coinId) {
5258
+ const def = this.getDefinition(coinId);
5259
+ if (def?.symbol) {
5260
+ return def.symbol;
5261
+ }
5262
+ return coinId.slice(0, 6).toUpperCase();
5263
+ }
5264
+ /**
5265
+ * Get token name for a coin ID
5266
+ * @param coinId - 64-character hex string
5267
+ * @returns Name (e.g., "Bitcoin") or coin ID if not found
5268
+ */
5269
+ getName(coinId) {
5270
+ const def = this.getDefinition(coinId);
5271
+ if (def?.name) {
5272
+ return def.name.charAt(0).toUpperCase() + def.name.slice(1);
5273
+ }
5274
+ return coinId;
5275
+ }
5276
+ /**
5277
+ * Get decimal places for a coin ID
5278
+ * @param coinId - 64-character hex string
5279
+ * @returns Decimals or 0 if not found
5280
+ */
5281
+ getDecimals(coinId) {
5282
+ const def = this.getDefinition(coinId);
5283
+ return def?.decimals ?? 0;
5284
+ }
5285
+ /**
5286
+ * Get icon URL for a coin ID
5287
+ * @param coinId - 64-character hex string
5288
+ * @param preferPng - Prefer PNG format over SVG
5289
+ * @returns Icon URL or null if not found
5290
+ */
5291
+ getIconUrl(coinId, preferPng = true) {
5292
+ const def = this.getDefinition(coinId);
5293
+ if (!def?.icons || def.icons.length === 0) {
5294
+ return null;
5295
+ }
5296
+ if (preferPng) {
5297
+ const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
5298
+ if (pngIcon) return pngIcon.url;
5299
+ }
5300
+ return def.icons[0].url;
5301
+ }
5302
+ /**
5303
+ * Check if a coin ID is known in the registry
5304
+ * @param coinId - 64-character hex string
5305
+ * @returns true if the coin is in the registry
5306
+ */
5307
+ isKnown(coinId) {
5308
+ return this.definitionsById.has(coinId.toLowerCase());
5309
+ }
5310
+ /**
5311
+ * Get all token definitions
5312
+ * @returns Array of all token definitions
5313
+ */
5314
+ getAllDefinitions() {
5315
+ return Array.from(this.definitionsById.values());
5316
+ }
5317
+ /**
5318
+ * Get all fungible token definitions
5319
+ * @returns Array of fungible token definitions
5320
+ */
5321
+ getFungibleTokens() {
5322
+ return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
5323
+ }
5324
+ /**
5325
+ * Get all non-fungible token definitions
5326
+ * @returns Array of non-fungible token definitions
5327
+ */
5328
+ getNonFungibleTokens() {
5329
+ return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
5330
+ }
5331
+ /**
5332
+ * Get coin ID by symbol
5333
+ * @param symbol - Token symbol (e.g., "UCT")
5334
+ * @returns Coin ID hex string or undefined if not found
5335
+ */
5336
+ getCoinIdBySymbol(symbol) {
5337
+ const def = this.getDefinitionBySymbol(symbol);
5338
+ return def?.id;
5339
+ }
5340
+ /**
5341
+ * Get coin ID by name
5342
+ * @param name - Token name (e.g., "bitcoin")
5343
+ * @returns Coin ID hex string or undefined if not found
5344
+ */
5345
+ getCoinIdByName(name) {
5346
+ const def = this.getDefinitionByName(name);
5347
+ return def?.id;
5348
+ }
5349
+ };
5350
+
4874
5351
  // impl/shared/resolvers.ts
4875
5352
  function getNetworkConfig(network = "mainnet") {
4876
5353
  return NETWORKS[network];
@@ -4946,6 +5423,16 @@ function resolveGroupChatConfig(network, config) {
4946
5423
  relays: config.relays ?? [...netConfig.groupRelays]
4947
5424
  };
4948
5425
  }
5426
+ function resolveMarketConfig(config) {
5427
+ if (!config) return void 0;
5428
+ if (config === true) {
5429
+ return { apiUrl: DEFAULT_MARKET_API_URL };
5430
+ }
5431
+ return {
5432
+ apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5433
+ timeout: config.timeout
5434
+ };
5435
+ }
4949
5436
 
4950
5437
  // impl/nodejs/index.ts
4951
5438
  function createNodeProviders(config) {
@@ -4961,9 +5448,13 @@ function createNodeProviders(config) {
4961
5448
  const ipfsSync = config?.tokenSync?.ipfs;
4962
5449
  const ipfsTokenStorage = ipfsSync?.enabled ? createNodeIpfsStorageProvider(ipfsSync.config, storage) : void 0;
4963
5450
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5451
+ const networkConfig = getNetworkConfig(network);
5452
+ TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5453
+ const market = resolveMarketConfig(config?.market);
4964
5454
  return {
4965
5455
  storage,
4966
5456
  groupChat,
5457
+ market,
4967
5458
  tokenStorage: createFileTokenStorageProvider({
4968
5459
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
4969
5460
  }),