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