@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.
@@ -37,7 +37,11 @@ var STORAGE_KEYS_GLOBAL = {
37
37
  /** Group chat: processed event IDs for deduplication */
38
38
  GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
39
39
  /** Group chat: last used relay URL (stale data detection) */
40
- GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
40
+ GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
41
+ /** Cached token registry JSON (fetched from remote) */
42
+ TOKEN_REGISTRY_CACHE: "token_registry_cache",
43
+ /** Timestamp of last token registry cache update (ms since epoch) */
44
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
41
45
  };
42
46
  var STORAGE_KEYS_ADDRESS = {
43
47
  /** Pending transfers for this address */
@@ -113,6 +117,8 @@ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
113
117
  var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
114
118
  var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
115
119
  var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
120
+ var TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
121
+ var TOKEN_REGISTRY_REFRESH_INTERVAL = 36e5;
116
122
  var TEST_NOSTR_RELAYS = [
117
123
  "wss://nostr-relay.testnet.unicity.network"
118
124
  ];
@@ -126,7 +132,8 @@ var NETWORKS = {
126
132
  nostrRelays: DEFAULT_NOSTR_RELAYS,
127
133
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
128
134
  electrumUrl: DEFAULT_ELECTRUM_URL,
129
- groupRelays: DEFAULT_GROUP_RELAYS
135
+ groupRelays: DEFAULT_GROUP_RELAYS,
136
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
130
137
  },
131
138
  testnet: {
132
139
  name: "Testnet",
@@ -134,7 +141,8 @@ var NETWORKS = {
134
141
  nostrRelays: TEST_NOSTR_RELAYS,
135
142
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
136
143
  electrumUrl: TEST_ELECTRUM_URL,
137
- groupRelays: DEFAULT_GROUP_RELAYS
144
+ groupRelays: DEFAULT_GROUP_RELAYS,
145
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
138
146
  },
139
147
  dev: {
140
148
  name: "Development",
@@ -142,7 +150,8 @@ var NETWORKS = {
142
150
  nostrRelays: TEST_NOSTR_RELAYS,
143
151
  ipfsGateways: DEFAULT_IPFS_GATEWAYS,
144
152
  electrumUrl: TEST_ELECTRUM_URL,
145
- groupRelays: DEFAULT_GROUP_RELAYS
153
+ groupRelays: DEFAULT_GROUP_RELAYS,
154
+ tokenRegistryUrl: TOKEN_REGISTRY_URL
146
155
  }
147
156
  };
148
157
  var TIMEOUTS = {
@@ -157,6 +166,7 @@ var TIMEOUTS = {
157
166
  /** Sync interval */
158
167
  SYNC_INTERVAL: 6e4
159
168
  };
169
+ var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
160
170
 
161
171
  // impl/nodejs/storage/FileStorageProvider.ts
162
172
  var FileStorageProvider = class {
@@ -928,7 +938,9 @@ import {
928
938
  EventKinds,
929
939
  hashNametag,
930
940
  NostrClient,
931
- Filter
941
+ Filter,
942
+ isChatMessage,
943
+ isReadReceipt
932
944
  } from "@unicitylabs/nostr-js-sdk";
933
945
 
934
946
  // core/crypto.ts
@@ -1146,6 +1158,8 @@ var NostrTransportProvider = class {
1146
1158
  transferHandlers = /* @__PURE__ */ new Set();
1147
1159
  paymentRequestHandlers = /* @__PURE__ */ new Set();
1148
1160
  paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1161
+ readReceiptHandlers = /* @__PURE__ */ new Set();
1162
+ typingIndicatorHandlers = /* @__PURE__ */ new Set();
1149
1163
  broadcastHandlers = /* @__PURE__ */ new Map();
1150
1164
  eventCallbacks = /* @__PURE__ */ new Set();
1151
1165
  constructor(config) {
@@ -1397,6 +1411,18 @@ var NostrTransportProvider = class {
1397
1411
  const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1398
1412
  const giftWrap = NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
1399
1413
  await this.publishEvent(giftWrap);
1414
+ const selfWrapContent = JSON.stringify({
1415
+ selfWrap: true,
1416
+ originalId: giftWrap.id,
1417
+ recipientPubkey,
1418
+ senderNametag,
1419
+ text: content
1420
+ });
1421
+ const selfPubkey = this.keyManager.getPublicKeyHex();
1422
+ const selfGiftWrap = NIP17.createGiftWrap(this.keyManager, selfPubkey, selfWrapContent);
1423
+ this.publishEvent(selfGiftWrap).catch((err) => {
1424
+ this.log("Self-wrap publish failed:", err);
1425
+ });
1400
1426
  this.emitEvent({
1401
1427
  type: "message:sent",
1402
1428
  timestamp: Date.now(),
@@ -1495,6 +1521,37 @@ var NostrTransportProvider = class {
1495
1521
  this.paymentRequestResponseHandlers.add(handler);
1496
1522
  return () => this.paymentRequestResponseHandlers.delete(handler);
1497
1523
  }
1524
+ // ===========================================================================
1525
+ // Read Receipts
1526
+ // ===========================================================================
1527
+ async sendReadReceipt(recipientTransportPubkey, messageEventId) {
1528
+ if (!this.keyManager) throw new Error("Not initialized");
1529
+ const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1530
+ const event = NIP17.createReadReceipt(this.keyManager, nostrRecipient, messageEventId);
1531
+ await this.publishEvent(event);
1532
+ this.log("Sent read receipt for:", messageEventId, "to:", nostrRecipient.slice(0, 16));
1533
+ }
1534
+ onReadReceipt(handler) {
1535
+ this.readReceiptHandlers.add(handler);
1536
+ return () => this.readReceiptHandlers.delete(handler);
1537
+ }
1538
+ // ===========================================================================
1539
+ // Typing Indicators
1540
+ // ===========================================================================
1541
+ async sendTypingIndicator(recipientTransportPubkey) {
1542
+ if (!this.keyManager) throw new Error("Not initialized");
1543
+ const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1544
+ const content = JSON.stringify({
1545
+ type: "typing",
1546
+ senderNametag: this.identity?.nametag
1547
+ });
1548
+ const event = NIP17.createGiftWrap(this.keyManager, nostrRecipient, content);
1549
+ await this.publishEvent(event);
1550
+ }
1551
+ onTypingIndicator(handler) {
1552
+ this.typingIndicatorHandlers.add(handler);
1553
+ return () => this.typingIndicatorHandlers.delete(handler);
1554
+ }
1498
1555
  /**
1499
1556
  * Resolve any identifier to full peer information.
1500
1557
  * Routes to the appropriate specific resolve method based on identifier format.
@@ -1948,11 +2005,74 @@ var NostrTransportProvider = class {
1948
2005
  const pm = NIP17.unwrap(event, this.keyManager);
1949
2006
  this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
1950
2007
  if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
1951
- this.log("Skipping own message");
2008
+ try {
2009
+ const parsed = JSON.parse(pm.content);
2010
+ if (parsed?.selfWrap && parsed.recipientPubkey) {
2011
+ this.log("Self-wrap replay for recipient:", parsed.recipientPubkey?.slice(0, 16));
2012
+ const message2 = {
2013
+ id: parsed.originalId || pm.eventId,
2014
+ senderTransportPubkey: pm.senderPubkey,
2015
+ senderNametag: parsed.senderNametag,
2016
+ recipientTransportPubkey: parsed.recipientPubkey,
2017
+ content: parsed.text ?? "",
2018
+ timestamp: pm.timestamp * 1e3,
2019
+ encrypted: true,
2020
+ isSelfWrap: true
2021
+ };
2022
+ for (const handler of this.messageHandlers) {
2023
+ try {
2024
+ handler(message2);
2025
+ } catch (e) {
2026
+ this.log("Self-wrap handler error:", e);
2027
+ }
2028
+ }
2029
+ return;
2030
+ }
2031
+ } catch {
2032
+ }
2033
+ this.log("Skipping own non-self-wrap message");
2034
+ return;
2035
+ }
2036
+ if (isReadReceipt(pm)) {
2037
+ this.log("Read receipt from:", pm.senderPubkey?.slice(0, 16), "for:", pm.replyToEventId);
2038
+ if (pm.replyToEventId) {
2039
+ const receipt = {
2040
+ senderTransportPubkey: pm.senderPubkey,
2041
+ messageEventId: pm.replyToEventId,
2042
+ timestamp: pm.timestamp * 1e3
2043
+ };
2044
+ for (const handler of this.readReceiptHandlers) {
2045
+ try {
2046
+ handler(receipt);
2047
+ } catch (e) {
2048
+ this.log("Read receipt handler error:", e);
2049
+ }
2050
+ }
2051
+ }
1952
2052
  return;
1953
2053
  }
1954
- if (pm.kind !== EventKinds.CHAT_MESSAGE) {
1955
- this.log("Skipping non-chat message, kind:", pm.kind);
2054
+ try {
2055
+ const parsed = JSON.parse(pm.content);
2056
+ if (parsed?.type === "typing") {
2057
+ this.log("Typing indicator from:", pm.senderPubkey?.slice(0, 16));
2058
+ const indicator = {
2059
+ senderTransportPubkey: pm.senderPubkey,
2060
+ senderNametag: parsed.senderNametag,
2061
+ timestamp: pm.timestamp * 1e3
2062
+ };
2063
+ for (const handler of this.typingIndicatorHandlers) {
2064
+ try {
2065
+ handler(indicator);
2066
+ } catch (e) {
2067
+ this.log("Typing handler error:", e);
2068
+ }
2069
+ }
2070
+ return;
2071
+ }
2072
+ } catch {
2073
+ }
2074
+ if (!isChatMessage(pm)) {
2075
+ this.log("Skipping unknown message kind:", pm.kind);
1956
2076
  return;
1957
2077
  }
1958
2078
  let content = pm.content;
@@ -1967,7 +2087,9 @@ var NostrTransportProvider = class {
1967
2087
  }
1968
2088
  this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
1969
2089
  const message = {
1970
- id: pm.eventId,
2090
+ // Use outer gift wrap event.id so it matches the sender's stored giftWrap.id.
2091
+ // This ensures read receipts reference an ID the sender recognizes.
2092
+ id: event.id,
1971
2093
  senderTransportPubkey: pm.senderPubkey,
1972
2094
  senderNametag,
1973
2095
  content,
@@ -4831,6 +4953,363 @@ function createPriceProvider(config) {
4831
4953
  }
4832
4954
  }
4833
4955
 
4956
+ // registry/TokenRegistry.ts
4957
+ var FETCH_TIMEOUT_MS = 1e4;
4958
+ var TokenRegistry = class _TokenRegistry {
4959
+ static instance = null;
4960
+ definitionsById;
4961
+ definitionsBySymbol;
4962
+ definitionsByName;
4963
+ // Remote refresh state
4964
+ remoteUrl = null;
4965
+ storage = null;
4966
+ refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
4967
+ refreshTimer = null;
4968
+ lastRefreshAt = 0;
4969
+ refreshPromise = null;
4970
+ constructor() {
4971
+ this.definitionsById = /* @__PURE__ */ new Map();
4972
+ this.definitionsBySymbol = /* @__PURE__ */ new Map();
4973
+ this.definitionsByName = /* @__PURE__ */ new Map();
4974
+ }
4975
+ /**
4976
+ * Get singleton instance of TokenRegistry
4977
+ */
4978
+ static getInstance() {
4979
+ if (!_TokenRegistry.instance) {
4980
+ _TokenRegistry.instance = new _TokenRegistry();
4981
+ }
4982
+ return _TokenRegistry.instance;
4983
+ }
4984
+ /**
4985
+ * Configure remote registry refresh with persistent caching.
4986
+ *
4987
+ * On first call:
4988
+ * 1. Loads cached data from StorageProvider (if available and fresh)
4989
+ * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
4990
+ *
4991
+ * @param options - Configuration options
4992
+ * @param options.remoteUrl - Remote URL to fetch definitions from
4993
+ * @param options.storage - StorageProvider for persistent caching
4994
+ * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
4995
+ * @param options.autoRefresh - Start auto-refresh immediately (default: true)
4996
+ */
4997
+ static configure(options) {
4998
+ const instance = _TokenRegistry.getInstance();
4999
+ if (options.remoteUrl !== void 0) {
5000
+ instance.remoteUrl = options.remoteUrl;
5001
+ }
5002
+ if (options.storage !== void 0) {
5003
+ instance.storage = options.storage;
5004
+ }
5005
+ if (options.refreshIntervalMs !== void 0) {
5006
+ instance.refreshIntervalMs = options.refreshIntervalMs;
5007
+ }
5008
+ if (instance.storage) {
5009
+ instance.loadFromCache();
5010
+ }
5011
+ const autoRefresh = options.autoRefresh ?? true;
5012
+ if (autoRefresh && instance.remoteUrl) {
5013
+ instance.startAutoRefresh();
5014
+ }
5015
+ }
5016
+ /**
5017
+ * Reset the singleton instance (useful for testing).
5018
+ * Stops auto-refresh if running.
5019
+ */
5020
+ static resetInstance() {
5021
+ if (_TokenRegistry.instance) {
5022
+ _TokenRegistry.instance.stopAutoRefresh();
5023
+ }
5024
+ _TokenRegistry.instance = null;
5025
+ }
5026
+ /**
5027
+ * Destroy the singleton: stop auto-refresh and reset.
5028
+ */
5029
+ static destroy() {
5030
+ _TokenRegistry.resetInstance();
5031
+ }
5032
+ // ===========================================================================
5033
+ // Cache (StorageProvider)
5034
+ // ===========================================================================
5035
+ /**
5036
+ * Load definitions from StorageProvider cache.
5037
+ * Only applies if cache exists and is fresh (within refreshIntervalMs).
5038
+ */
5039
+ async loadFromCache() {
5040
+ if (!this.storage) return false;
5041
+ try {
5042
+ const [cached, cachedTs] = await Promise.all([
5043
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE),
5044
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS)
5045
+ ]);
5046
+ if (!cached || !cachedTs) return false;
5047
+ const ts = parseInt(cachedTs, 10);
5048
+ if (isNaN(ts)) return false;
5049
+ const age = Date.now() - ts;
5050
+ if (age > this.refreshIntervalMs) return false;
5051
+ if (this.lastRefreshAt > ts) return false;
5052
+ const data = JSON.parse(cached);
5053
+ if (!this.isValidDefinitionsArray(data)) return false;
5054
+ this.applyDefinitions(data);
5055
+ this.lastRefreshAt = ts;
5056
+ return true;
5057
+ } catch {
5058
+ return false;
5059
+ }
5060
+ }
5061
+ /**
5062
+ * Save definitions to StorageProvider cache.
5063
+ */
5064
+ async saveToCache(definitions) {
5065
+ if (!this.storage) return;
5066
+ try {
5067
+ await Promise.all([
5068
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE, JSON.stringify(definitions)),
5069
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS, String(Date.now()))
5070
+ ]);
5071
+ } catch {
5072
+ }
5073
+ }
5074
+ // ===========================================================================
5075
+ // Remote Refresh
5076
+ // ===========================================================================
5077
+ /**
5078
+ * Apply an array of token definitions to the internal maps.
5079
+ * Clears existing data before applying.
5080
+ */
5081
+ applyDefinitions(definitions) {
5082
+ this.definitionsById.clear();
5083
+ this.definitionsBySymbol.clear();
5084
+ this.definitionsByName.clear();
5085
+ for (const def of definitions) {
5086
+ const idLower = def.id.toLowerCase();
5087
+ this.definitionsById.set(idLower, def);
5088
+ if (def.symbol) {
5089
+ this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
5090
+ }
5091
+ this.definitionsByName.set(def.name.toLowerCase(), def);
5092
+ }
5093
+ }
5094
+ /**
5095
+ * Validate that data is an array of objects with 'id' field
5096
+ */
5097
+ isValidDefinitionsArray(data) {
5098
+ return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
5099
+ }
5100
+ /**
5101
+ * Fetch token definitions from the remote URL and update the registry.
5102
+ * On success, also persists to StorageProvider cache.
5103
+ * Returns true on success, false on failure. On failure, existing data is preserved.
5104
+ * Concurrent calls are deduplicated — only one fetch runs at a time.
5105
+ */
5106
+ async refreshFromRemote() {
5107
+ if (!this.remoteUrl) {
5108
+ return false;
5109
+ }
5110
+ if (this.refreshPromise) {
5111
+ return this.refreshPromise;
5112
+ }
5113
+ this.refreshPromise = this.doRefresh();
5114
+ try {
5115
+ return await this.refreshPromise;
5116
+ } finally {
5117
+ this.refreshPromise = null;
5118
+ }
5119
+ }
5120
+ async doRefresh() {
5121
+ try {
5122
+ const controller = new AbortController();
5123
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
5124
+ let response;
5125
+ try {
5126
+ response = await fetch(this.remoteUrl, {
5127
+ headers: { Accept: "application/json" },
5128
+ signal: controller.signal
5129
+ });
5130
+ } finally {
5131
+ clearTimeout(timer);
5132
+ }
5133
+ if (!response.ok) {
5134
+ console.warn(
5135
+ `[TokenRegistry] Remote fetch failed: HTTP ${response.status} ${response.statusText}`
5136
+ );
5137
+ return false;
5138
+ }
5139
+ const data = await response.json();
5140
+ if (!this.isValidDefinitionsArray(data)) {
5141
+ console.warn("[TokenRegistry] Remote data is not a valid token definitions array");
5142
+ return false;
5143
+ }
5144
+ const definitions = data;
5145
+ this.applyDefinitions(definitions);
5146
+ this.lastRefreshAt = Date.now();
5147
+ this.saveToCache(definitions);
5148
+ return true;
5149
+ } catch (error) {
5150
+ const message = error instanceof Error ? error.message : String(error);
5151
+ console.warn(`[TokenRegistry] Remote refresh failed: ${message}`);
5152
+ return false;
5153
+ }
5154
+ }
5155
+ /**
5156
+ * Start periodic auto-refresh from the remote URL.
5157
+ * Does an immediate fetch, then repeats at the configured interval.
5158
+ */
5159
+ startAutoRefresh(intervalMs) {
5160
+ this.stopAutoRefresh();
5161
+ if (intervalMs !== void 0) {
5162
+ this.refreshIntervalMs = intervalMs;
5163
+ }
5164
+ this.refreshFromRemote();
5165
+ this.refreshTimer = setInterval(() => {
5166
+ this.refreshFromRemote();
5167
+ }, this.refreshIntervalMs);
5168
+ }
5169
+ /**
5170
+ * Stop periodic auto-refresh
5171
+ */
5172
+ stopAutoRefresh() {
5173
+ if (this.refreshTimer !== null) {
5174
+ clearInterval(this.refreshTimer);
5175
+ this.refreshTimer = null;
5176
+ }
5177
+ }
5178
+ /**
5179
+ * Timestamp of the last successful remote refresh (0 if never refreshed)
5180
+ */
5181
+ getLastRefreshAt() {
5182
+ return this.lastRefreshAt;
5183
+ }
5184
+ // ===========================================================================
5185
+ // Lookup Methods
5186
+ // ===========================================================================
5187
+ /**
5188
+ * Get token definition by hex coin ID
5189
+ * @param coinId - 64-character hex string
5190
+ * @returns Token definition or undefined if not found
5191
+ */
5192
+ getDefinition(coinId) {
5193
+ if (!coinId) return void 0;
5194
+ return this.definitionsById.get(coinId.toLowerCase());
5195
+ }
5196
+ /**
5197
+ * Get token definition by symbol (e.g., "UCT", "BTC")
5198
+ * @param symbol - Token symbol (case-insensitive)
5199
+ * @returns Token definition or undefined if not found
5200
+ */
5201
+ getDefinitionBySymbol(symbol) {
5202
+ if (!symbol) return void 0;
5203
+ return this.definitionsBySymbol.get(symbol.toUpperCase());
5204
+ }
5205
+ /**
5206
+ * Get token definition by name (e.g., "bitcoin", "ethereum")
5207
+ * @param name - Token name (case-insensitive)
5208
+ * @returns Token definition or undefined if not found
5209
+ */
5210
+ getDefinitionByName(name) {
5211
+ if (!name) return void 0;
5212
+ return this.definitionsByName.get(name.toLowerCase());
5213
+ }
5214
+ /**
5215
+ * Get token symbol for a coin ID
5216
+ * @param coinId - 64-character hex string
5217
+ * @returns Symbol (e.g., "UCT") or truncated ID if not found
5218
+ */
5219
+ getSymbol(coinId) {
5220
+ const def = this.getDefinition(coinId);
5221
+ if (def?.symbol) {
5222
+ return def.symbol;
5223
+ }
5224
+ return coinId.slice(0, 6).toUpperCase();
5225
+ }
5226
+ /**
5227
+ * Get token name for a coin ID
5228
+ * @param coinId - 64-character hex string
5229
+ * @returns Name (e.g., "Bitcoin") or coin ID if not found
5230
+ */
5231
+ getName(coinId) {
5232
+ const def = this.getDefinition(coinId);
5233
+ if (def?.name) {
5234
+ return def.name.charAt(0).toUpperCase() + def.name.slice(1);
5235
+ }
5236
+ return coinId;
5237
+ }
5238
+ /**
5239
+ * Get decimal places for a coin ID
5240
+ * @param coinId - 64-character hex string
5241
+ * @returns Decimals or 0 if not found
5242
+ */
5243
+ getDecimals(coinId) {
5244
+ const def = this.getDefinition(coinId);
5245
+ return def?.decimals ?? 0;
5246
+ }
5247
+ /**
5248
+ * Get icon URL for a coin ID
5249
+ * @param coinId - 64-character hex string
5250
+ * @param preferPng - Prefer PNG format over SVG
5251
+ * @returns Icon URL or null if not found
5252
+ */
5253
+ getIconUrl(coinId, preferPng = true) {
5254
+ const def = this.getDefinition(coinId);
5255
+ if (!def?.icons || def.icons.length === 0) {
5256
+ return null;
5257
+ }
5258
+ if (preferPng) {
5259
+ const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
5260
+ if (pngIcon) return pngIcon.url;
5261
+ }
5262
+ return def.icons[0].url;
5263
+ }
5264
+ /**
5265
+ * Check if a coin ID is known in the registry
5266
+ * @param coinId - 64-character hex string
5267
+ * @returns true if the coin is in the registry
5268
+ */
5269
+ isKnown(coinId) {
5270
+ return this.definitionsById.has(coinId.toLowerCase());
5271
+ }
5272
+ /**
5273
+ * Get all token definitions
5274
+ * @returns Array of all token definitions
5275
+ */
5276
+ getAllDefinitions() {
5277
+ return Array.from(this.definitionsById.values());
5278
+ }
5279
+ /**
5280
+ * Get all fungible token definitions
5281
+ * @returns Array of fungible token definitions
5282
+ */
5283
+ getFungibleTokens() {
5284
+ return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
5285
+ }
5286
+ /**
5287
+ * Get all non-fungible token definitions
5288
+ * @returns Array of non-fungible token definitions
5289
+ */
5290
+ getNonFungibleTokens() {
5291
+ return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
5292
+ }
5293
+ /**
5294
+ * Get coin ID by symbol
5295
+ * @param symbol - Token symbol (e.g., "UCT")
5296
+ * @returns Coin ID hex string or undefined if not found
5297
+ */
5298
+ getCoinIdBySymbol(symbol) {
5299
+ const def = this.getDefinitionBySymbol(symbol);
5300
+ return def?.id;
5301
+ }
5302
+ /**
5303
+ * Get coin ID by name
5304
+ * @param name - Token name (e.g., "bitcoin")
5305
+ * @returns Coin ID hex string or undefined if not found
5306
+ */
5307
+ getCoinIdByName(name) {
5308
+ const def = this.getDefinitionByName(name);
5309
+ return def?.id;
5310
+ }
5311
+ };
5312
+
4834
5313
  // impl/shared/resolvers.ts
4835
5314
  function getNetworkConfig(network = "mainnet") {
4836
5315
  return NETWORKS[network];
@@ -4906,6 +5385,16 @@ function resolveGroupChatConfig(network, config) {
4906
5385
  relays: config.relays ?? [...netConfig.groupRelays]
4907
5386
  };
4908
5387
  }
5388
+ function resolveMarketConfig(config) {
5389
+ if (!config) return void 0;
5390
+ if (config === true) {
5391
+ return { apiUrl: DEFAULT_MARKET_API_URL };
5392
+ }
5393
+ return {
5394
+ apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5395
+ timeout: config.timeout
5396
+ };
5397
+ }
4909
5398
 
4910
5399
  // impl/nodejs/index.ts
4911
5400
  function createNodeProviders(config) {
@@ -4921,9 +5410,13 @@ function createNodeProviders(config) {
4921
5410
  const ipfsSync = config?.tokenSync?.ipfs;
4922
5411
  const ipfsTokenStorage = ipfsSync?.enabled ? createNodeIpfsStorageProvider(ipfsSync.config, storage) : void 0;
4923
5412
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5413
+ const networkConfig = getNetworkConfig(network);
5414
+ TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5415
+ const market = resolveMarketConfig(config?.market);
4924
5416
  return {
4925
5417
  storage,
4926
5418
  groupChat,
5419
+ market,
4927
5420
  tokenStorage: createFileTokenStorageProvider({
4928
5421
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
4929
5422
  }),