@unicitylabs/sphere-sdk 0.3.4 → 0.3.6

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.
@@ -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 {
@@ -5060,6 +5070,363 @@ function createPriceProvider(config) {
5060
5070
  }
5061
5071
  }
5062
5072
 
5073
+ // registry/TokenRegistry.ts
5074
+ var FETCH_TIMEOUT_MS = 1e4;
5075
+ var TokenRegistry = class _TokenRegistry {
5076
+ static instance = null;
5077
+ definitionsById;
5078
+ definitionsBySymbol;
5079
+ definitionsByName;
5080
+ // Remote refresh state
5081
+ remoteUrl = null;
5082
+ storage = null;
5083
+ refreshIntervalMs = TOKEN_REGISTRY_REFRESH_INTERVAL;
5084
+ refreshTimer = null;
5085
+ lastRefreshAt = 0;
5086
+ refreshPromise = null;
5087
+ constructor() {
5088
+ this.definitionsById = /* @__PURE__ */ new Map();
5089
+ this.definitionsBySymbol = /* @__PURE__ */ new Map();
5090
+ this.definitionsByName = /* @__PURE__ */ new Map();
5091
+ }
5092
+ /**
5093
+ * Get singleton instance of TokenRegistry
5094
+ */
5095
+ static getInstance() {
5096
+ if (!_TokenRegistry.instance) {
5097
+ _TokenRegistry.instance = new _TokenRegistry();
5098
+ }
5099
+ return _TokenRegistry.instance;
5100
+ }
5101
+ /**
5102
+ * Configure remote registry refresh with persistent caching.
5103
+ *
5104
+ * On first call:
5105
+ * 1. Loads cached data from StorageProvider (if available and fresh)
5106
+ * 2. Starts periodic remote fetch (if autoRefresh is true, which is default)
5107
+ *
5108
+ * @param options - Configuration options
5109
+ * @param options.remoteUrl - Remote URL to fetch definitions from
5110
+ * @param options.storage - StorageProvider for persistent caching
5111
+ * @param options.refreshIntervalMs - Refresh interval in ms (default: 1 hour)
5112
+ * @param options.autoRefresh - Start auto-refresh immediately (default: true)
5113
+ */
5114
+ static configure(options) {
5115
+ const instance = _TokenRegistry.getInstance();
5116
+ if (options.remoteUrl !== void 0) {
5117
+ instance.remoteUrl = options.remoteUrl;
5118
+ }
5119
+ if (options.storage !== void 0) {
5120
+ instance.storage = options.storage;
5121
+ }
5122
+ if (options.refreshIntervalMs !== void 0) {
5123
+ instance.refreshIntervalMs = options.refreshIntervalMs;
5124
+ }
5125
+ if (instance.storage) {
5126
+ instance.loadFromCache();
5127
+ }
5128
+ const autoRefresh = options.autoRefresh ?? true;
5129
+ if (autoRefresh && instance.remoteUrl) {
5130
+ instance.startAutoRefresh();
5131
+ }
5132
+ }
5133
+ /**
5134
+ * Reset the singleton instance (useful for testing).
5135
+ * Stops auto-refresh if running.
5136
+ */
5137
+ static resetInstance() {
5138
+ if (_TokenRegistry.instance) {
5139
+ _TokenRegistry.instance.stopAutoRefresh();
5140
+ }
5141
+ _TokenRegistry.instance = null;
5142
+ }
5143
+ /**
5144
+ * Destroy the singleton: stop auto-refresh and reset.
5145
+ */
5146
+ static destroy() {
5147
+ _TokenRegistry.resetInstance();
5148
+ }
5149
+ // ===========================================================================
5150
+ // Cache (StorageProvider)
5151
+ // ===========================================================================
5152
+ /**
5153
+ * Load definitions from StorageProvider cache.
5154
+ * Only applies if cache exists and is fresh (within refreshIntervalMs).
5155
+ */
5156
+ async loadFromCache() {
5157
+ if (!this.storage) return false;
5158
+ try {
5159
+ const [cached, cachedTs] = await Promise.all([
5160
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE),
5161
+ this.storage.get(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS)
5162
+ ]);
5163
+ if (!cached || !cachedTs) return false;
5164
+ const ts = parseInt(cachedTs, 10);
5165
+ if (isNaN(ts)) return false;
5166
+ const age = Date.now() - ts;
5167
+ if (age > this.refreshIntervalMs) return false;
5168
+ if (this.lastRefreshAt > ts) return false;
5169
+ const data = JSON.parse(cached);
5170
+ if (!this.isValidDefinitionsArray(data)) return false;
5171
+ this.applyDefinitions(data);
5172
+ this.lastRefreshAt = ts;
5173
+ return true;
5174
+ } catch {
5175
+ return false;
5176
+ }
5177
+ }
5178
+ /**
5179
+ * Save definitions to StorageProvider cache.
5180
+ */
5181
+ async saveToCache(definitions) {
5182
+ if (!this.storage) return;
5183
+ try {
5184
+ await Promise.all([
5185
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE, JSON.stringify(definitions)),
5186
+ this.storage.set(STORAGE_KEYS_GLOBAL.TOKEN_REGISTRY_CACHE_TS, String(Date.now()))
5187
+ ]);
5188
+ } catch {
5189
+ }
5190
+ }
5191
+ // ===========================================================================
5192
+ // Remote Refresh
5193
+ // ===========================================================================
5194
+ /**
5195
+ * Apply an array of token definitions to the internal maps.
5196
+ * Clears existing data before applying.
5197
+ */
5198
+ applyDefinitions(definitions) {
5199
+ this.definitionsById.clear();
5200
+ this.definitionsBySymbol.clear();
5201
+ this.definitionsByName.clear();
5202
+ for (const def of definitions) {
5203
+ const idLower = def.id.toLowerCase();
5204
+ this.definitionsById.set(idLower, def);
5205
+ if (def.symbol) {
5206
+ this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
5207
+ }
5208
+ this.definitionsByName.set(def.name.toLowerCase(), def);
5209
+ }
5210
+ }
5211
+ /**
5212
+ * Validate that data is an array of objects with 'id' field
5213
+ */
5214
+ isValidDefinitionsArray(data) {
5215
+ return Array.isArray(data) && data.every((item) => item && typeof item === "object" && "id" in item);
5216
+ }
5217
+ /**
5218
+ * Fetch token definitions from the remote URL and update the registry.
5219
+ * On success, also persists to StorageProvider cache.
5220
+ * Returns true on success, false on failure. On failure, existing data is preserved.
5221
+ * Concurrent calls are deduplicated — only one fetch runs at a time.
5222
+ */
5223
+ async refreshFromRemote() {
5224
+ if (!this.remoteUrl) {
5225
+ return false;
5226
+ }
5227
+ if (this.refreshPromise) {
5228
+ return this.refreshPromise;
5229
+ }
5230
+ this.refreshPromise = this.doRefresh();
5231
+ try {
5232
+ return await this.refreshPromise;
5233
+ } finally {
5234
+ this.refreshPromise = null;
5235
+ }
5236
+ }
5237
+ async doRefresh() {
5238
+ try {
5239
+ const controller = new AbortController();
5240
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
5241
+ let response;
5242
+ try {
5243
+ response = await fetch(this.remoteUrl, {
5244
+ headers: { Accept: "application/json" },
5245
+ signal: controller.signal
5246
+ });
5247
+ } finally {
5248
+ clearTimeout(timer);
5249
+ }
5250
+ if (!response.ok) {
5251
+ console.warn(
5252
+ `[TokenRegistry] Remote fetch failed: HTTP ${response.status} ${response.statusText}`
5253
+ );
5254
+ return false;
5255
+ }
5256
+ const data = await response.json();
5257
+ if (!this.isValidDefinitionsArray(data)) {
5258
+ console.warn("[TokenRegistry] Remote data is not a valid token definitions array");
5259
+ return false;
5260
+ }
5261
+ const definitions = data;
5262
+ this.applyDefinitions(definitions);
5263
+ this.lastRefreshAt = Date.now();
5264
+ this.saveToCache(definitions);
5265
+ return true;
5266
+ } catch (error) {
5267
+ const message = error instanceof Error ? error.message : String(error);
5268
+ console.warn(`[TokenRegistry] Remote refresh failed: ${message}`);
5269
+ return false;
5270
+ }
5271
+ }
5272
+ /**
5273
+ * Start periodic auto-refresh from the remote URL.
5274
+ * Does an immediate fetch, then repeats at the configured interval.
5275
+ */
5276
+ startAutoRefresh(intervalMs) {
5277
+ this.stopAutoRefresh();
5278
+ if (intervalMs !== void 0) {
5279
+ this.refreshIntervalMs = intervalMs;
5280
+ }
5281
+ this.refreshFromRemote();
5282
+ this.refreshTimer = setInterval(() => {
5283
+ this.refreshFromRemote();
5284
+ }, this.refreshIntervalMs);
5285
+ }
5286
+ /**
5287
+ * Stop periodic auto-refresh
5288
+ */
5289
+ stopAutoRefresh() {
5290
+ if (this.refreshTimer !== null) {
5291
+ clearInterval(this.refreshTimer);
5292
+ this.refreshTimer = null;
5293
+ }
5294
+ }
5295
+ /**
5296
+ * Timestamp of the last successful remote refresh (0 if never refreshed)
5297
+ */
5298
+ getLastRefreshAt() {
5299
+ return this.lastRefreshAt;
5300
+ }
5301
+ // ===========================================================================
5302
+ // Lookup Methods
5303
+ // ===========================================================================
5304
+ /**
5305
+ * Get token definition by hex coin ID
5306
+ * @param coinId - 64-character hex string
5307
+ * @returns Token definition or undefined if not found
5308
+ */
5309
+ getDefinition(coinId) {
5310
+ if (!coinId) return void 0;
5311
+ return this.definitionsById.get(coinId.toLowerCase());
5312
+ }
5313
+ /**
5314
+ * Get token definition by symbol (e.g., "UCT", "BTC")
5315
+ * @param symbol - Token symbol (case-insensitive)
5316
+ * @returns Token definition or undefined if not found
5317
+ */
5318
+ getDefinitionBySymbol(symbol) {
5319
+ if (!symbol) return void 0;
5320
+ return this.definitionsBySymbol.get(symbol.toUpperCase());
5321
+ }
5322
+ /**
5323
+ * Get token definition by name (e.g., "bitcoin", "ethereum")
5324
+ * @param name - Token name (case-insensitive)
5325
+ * @returns Token definition or undefined if not found
5326
+ */
5327
+ getDefinitionByName(name) {
5328
+ if (!name) return void 0;
5329
+ return this.definitionsByName.get(name.toLowerCase());
5330
+ }
5331
+ /**
5332
+ * Get token symbol for a coin ID
5333
+ * @param coinId - 64-character hex string
5334
+ * @returns Symbol (e.g., "UCT") or truncated ID if not found
5335
+ */
5336
+ getSymbol(coinId) {
5337
+ const def = this.getDefinition(coinId);
5338
+ if (def?.symbol) {
5339
+ return def.symbol;
5340
+ }
5341
+ return coinId.slice(0, 6).toUpperCase();
5342
+ }
5343
+ /**
5344
+ * Get token name for a coin ID
5345
+ * @param coinId - 64-character hex string
5346
+ * @returns Name (e.g., "Bitcoin") or coin ID if not found
5347
+ */
5348
+ getName(coinId) {
5349
+ const def = this.getDefinition(coinId);
5350
+ if (def?.name) {
5351
+ return def.name.charAt(0).toUpperCase() + def.name.slice(1);
5352
+ }
5353
+ return coinId;
5354
+ }
5355
+ /**
5356
+ * Get decimal places for a coin ID
5357
+ * @param coinId - 64-character hex string
5358
+ * @returns Decimals or 0 if not found
5359
+ */
5360
+ getDecimals(coinId) {
5361
+ const def = this.getDefinition(coinId);
5362
+ return def?.decimals ?? 0;
5363
+ }
5364
+ /**
5365
+ * Get icon URL for a coin ID
5366
+ * @param coinId - 64-character hex string
5367
+ * @param preferPng - Prefer PNG format over SVG
5368
+ * @returns Icon URL or null if not found
5369
+ */
5370
+ getIconUrl(coinId, preferPng = true) {
5371
+ const def = this.getDefinition(coinId);
5372
+ if (!def?.icons || def.icons.length === 0) {
5373
+ return null;
5374
+ }
5375
+ if (preferPng) {
5376
+ const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
5377
+ if (pngIcon) return pngIcon.url;
5378
+ }
5379
+ return def.icons[0].url;
5380
+ }
5381
+ /**
5382
+ * Check if a coin ID is known in the registry
5383
+ * @param coinId - 64-character hex string
5384
+ * @returns true if the coin is in the registry
5385
+ */
5386
+ isKnown(coinId) {
5387
+ return this.definitionsById.has(coinId.toLowerCase());
5388
+ }
5389
+ /**
5390
+ * Get all token definitions
5391
+ * @returns Array of all token definitions
5392
+ */
5393
+ getAllDefinitions() {
5394
+ return Array.from(this.definitionsById.values());
5395
+ }
5396
+ /**
5397
+ * Get all fungible token definitions
5398
+ * @returns Array of fungible token definitions
5399
+ */
5400
+ getFungibleTokens() {
5401
+ return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
5402
+ }
5403
+ /**
5404
+ * Get all non-fungible token definitions
5405
+ * @returns Array of non-fungible token definitions
5406
+ */
5407
+ getNonFungibleTokens() {
5408
+ return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
5409
+ }
5410
+ /**
5411
+ * Get coin ID by symbol
5412
+ * @param symbol - Token symbol (e.g., "UCT")
5413
+ * @returns Coin ID hex string or undefined if not found
5414
+ */
5415
+ getCoinIdBySymbol(symbol) {
5416
+ const def = this.getDefinitionBySymbol(symbol);
5417
+ return def?.id;
5418
+ }
5419
+ /**
5420
+ * Get coin ID by name
5421
+ * @param name - Token name (e.g., "bitcoin")
5422
+ * @returns Coin ID hex string or undefined if not found
5423
+ */
5424
+ getCoinIdByName(name) {
5425
+ const def = this.getDefinitionByName(name);
5426
+ return def?.id;
5427
+ }
5428
+ };
5429
+
5063
5430
  // impl/shared/resolvers.ts
5064
5431
  function getNetworkConfig(network = "mainnet") {
5065
5432
  return NETWORKS[network];
@@ -5145,6 +5512,16 @@ function resolveGroupChatConfig(network, config) {
5145
5512
  relays: config.relays ?? [...netConfig.groupRelays]
5146
5513
  };
5147
5514
  }
5515
+ function resolveMarketConfig(config) {
5516
+ if (!config) return void 0;
5517
+ if (config === true) {
5518
+ return { apiUrl: DEFAULT_MARKET_API_URL };
5519
+ }
5520
+ return {
5521
+ apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5522
+ timeout: config.timeout
5523
+ };
5524
+ }
5148
5525
 
5149
5526
  // impl/browser/index.ts
5150
5527
  if (typeof globalThis.Buffer === "undefined") {
@@ -5211,9 +5588,13 @@ function createBrowserProviders(config) {
5211
5588
  // reuse debug-like flag
5212
5589
  }) : void 0;
5213
5590
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5591
+ const networkConfig = getNetworkConfig(network);
5592
+ TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5593
+ const market = resolveMarketConfig(config?.market);
5214
5594
  return {
5215
5595
  storage,
5216
5596
  groupChat,
5597
+ market,
5217
5598
  transport: createNostrTransportProvider({
5218
5599
  relays: transportConfig.relays,
5219
5600
  timeout: transportConfig.timeout,