@unicitylabs/sphere-sdk 0.3.6 → 0.3.8

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.
@@ -1190,6 +1190,7 @@ declare const createUnicityOracleProvider: typeof createUnicityAggregatorProvide
1190
1190
  * Platform-independent abstraction for fetching token market prices.
1191
1191
  * Does not extend BaseProvider — stateless HTTP client with internal caching.
1192
1192
  */
1193
+
1193
1194
  /**
1194
1195
  * Supported price provider platforms
1195
1196
  */
@@ -1234,7 +1235,7 @@ interface PriceProvider {
1234
1235
  /**
1235
1236
  * Get price for a single token
1236
1237
  * @param tokenName - Token name (e.g., 'bitcoin')
1237
- * @returns Token price or null if not available
1238
+ * @returns Token price (zero-price entry for tokens not listed on the platform), or null on network error with no cache
1238
1239
  */
1239
1240
  getPrice(tokenName: string): Promise<TokenPrice | null>;
1240
1241
  /**
@@ -1315,15 +1316,6 @@ interface BasePriceConfig {
1315
1316
  /** Enable debug logging */
1316
1317
  debug?: boolean;
1317
1318
  }
1318
- /**
1319
- * Base market module configuration
1320
- */
1321
- interface BaseMarketConfig {
1322
- /** Market API base URL (default: https://market-api.unicity.network) */
1323
- apiUrl?: string;
1324
- /** Request timeout in ms (default: 30000) */
1325
- timeout?: number;
1326
- }
1327
1319
  /**
1328
1320
  * Base providers result
1329
1321
  * Common structure for all platforms
@@ -1339,13 +1331,6 @@ interface BaseProviders {
1339
1331
  price?: PriceProvider;
1340
1332
  }
1341
1333
 
1342
- interface MarketModuleConfig {
1343
- /** Market API base URL (default: https://market-api.unicity.network) */
1344
- apiUrl?: string;
1345
- /** Request timeout in ms (default: 30000) */
1346
- timeout?: number;
1347
- }
1348
-
1349
1334
  /** IPFS storage provider configuration */
1350
1335
  interface IpfsStorageConfig {
1351
1336
  /** Gateway URLs for HTTP API (defaults to Unicity dedicated nodes) */
@@ -1438,8 +1423,6 @@ interface NodeProvidersConfig {
1438
1423
  enabled?: boolean;
1439
1424
  relays?: string[];
1440
1425
  } | boolean;
1441
- /** Market module configuration. true = enable with defaults, object = custom config */
1442
- market?: BaseMarketConfig | boolean;
1443
1426
  }
1444
1427
  interface NodeProviders {
1445
1428
  storage: StorageProvider;
@@ -1454,8 +1437,6 @@ interface NodeProviders {
1454
1437
  ipfsTokenStorage?: TokenStorageProvider<TxfStorageDataBase>;
1455
1438
  /** Group chat config (resolved, for passing to Sphere.init) */
1456
1439
  groupChat?: GroupChatModuleConfig | boolean;
1457
- /** Market module config (resolved, for passing to Sphere.init) */
1458
- market?: MarketModuleConfig | boolean;
1459
1440
  }
1460
1441
  /**
1461
1442
  * Create all Node.js providers with default configuration
@@ -1190,6 +1190,7 @@ declare const createUnicityOracleProvider: typeof createUnicityAggregatorProvide
1190
1190
  * Platform-independent abstraction for fetching token market prices.
1191
1191
  * Does not extend BaseProvider — stateless HTTP client with internal caching.
1192
1192
  */
1193
+
1193
1194
  /**
1194
1195
  * Supported price provider platforms
1195
1196
  */
@@ -1234,7 +1235,7 @@ interface PriceProvider {
1234
1235
  /**
1235
1236
  * Get price for a single token
1236
1237
  * @param tokenName - Token name (e.g., 'bitcoin')
1237
- * @returns Token price or null if not available
1238
+ * @returns Token price (zero-price entry for tokens not listed on the platform), or null on network error with no cache
1238
1239
  */
1239
1240
  getPrice(tokenName: string): Promise<TokenPrice | null>;
1240
1241
  /**
@@ -1315,15 +1316,6 @@ interface BasePriceConfig {
1315
1316
  /** Enable debug logging */
1316
1317
  debug?: boolean;
1317
1318
  }
1318
- /**
1319
- * Base market module configuration
1320
- */
1321
- interface BaseMarketConfig {
1322
- /** Market API base URL (default: https://market-api.unicity.network) */
1323
- apiUrl?: string;
1324
- /** Request timeout in ms (default: 30000) */
1325
- timeout?: number;
1326
- }
1327
1319
  /**
1328
1320
  * Base providers result
1329
1321
  * Common structure for all platforms
@@ -1339,13 +1331,6 @@ interface BaseProviders {
1339
1331
  price?: PriceProvider;
1340
1332
  }
1341
1333
 
1342
- interface MarketModuleConfig {
1343
- /** Market API base URL (default: https://market-api.unicity.network) */
1344
- apiUrl?: string;
1345
- /** Request timeout in ms (default: 30000) */
1346
- timeout?: number;
1347
- }
1348
-
1349
1334
  /** IPFS storage provider configuration */
1350
1335
  interface IpfsStorageConfig {
1351
1336
  /** Gateway URLs for HTTP API (defaults to Unicity dedicated nodes) */
@@ -1438,8 +1423,6 @@ interface NodeProvidersConfig {
1438
1423
  enabled?: boolean;
1439
1424
  relays?: string[];
1440
1425
  } | boolean;
1441
- /** Market module configuration. true = enable with defaults, object = custom config */
1442
- market?: BaseMarketConfig | boolean;
1443
1426
  }
1444
1427
  interface NodeProviders {
1445
1428
  storage: StorageProvider;
@@ -1454,8 +1437,6 @@ interface NodeProviders {
1454
1437
  ipfsTokenStorage?: TokenStorageProvider<TxfStorageDataBase>;
1455
1438
  /** Group chat config (resolved, for passing to Sphere.init) */
1456
1439
  groupChat?: GroupChatModuleConfig | boolean;
1457
- /** Market module config (resolved, for passing to Sphere.init) */
1458
- market?: MarketModuleConfig | boolean;
1459
1440
  }
1460
1441
  /**
1461
1442
  * Create all Node.js providers with default configuration
@@ -41,7 +41,11 @@ var STORAGE_KEYS_GLOBAL = {
41
41
  /** Cached token registry JSON (fetched from remote) */
42
42
  TOKEN_REGISTRY_CACHE: "token_registry_cache",
43
43
  /** Timestamp of last token registry cache update (ms since epoch) */
44
- TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
44
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
45
+ /** Cached price data JSON (from CoinGecko or other provider) */
46
+ PRICE_CACHE: "price_cache",
47
+ /** Timestamp of last price cache update (ms since epoch) */
48
+ PRICE_CACHE_TS: "price_cache_ts"
45
49
  };
46
50
  var STORAGE_KEYS_ADDRESS = {
47
51
  /** Pending transfers for this address */
@@ -166,7 +170,6 @@ var TIMEOUTS = {
166
170
  /** Sync interval */
167
171
  SYNC_INTERVAL: 6e4
168
172
  };
169
- var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
170
173
 
171
174
  // impl/nodejs/storage/FileStorageProvider.ts
172
175
  var FileStorageProvider = class {
@@ -3002,6 +3005,7 @@ async function loadIpnsModule() {
3002
3005
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3003
3006
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3004
3007
  const record = await createIPNSRecord(
3008
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3005
3009
  keyPair,
3006
3010
  `/ipfs/${cid}`,
3007
3011
  sequenceNumber,
@@ -4743,26 +4747,37 @@ var CoinGeckoPriceProvider = class {
4743
4747
  timeout;
4744
4748
  debug;
4745
4749
  baseUrl;
4750
+ storage;
4751
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
4752
+ fetchPromise = null;
4753
+ /** Token names being fetched in the current in-flight request */
4754
+ fetchNames = null;
4755
+ /** Whether persistent cache has been loaded into memory */
4756
+ persistentCacheLoaded = false;
4757
+ /** Promise for loading persistent cache (deduplication) */
4758
+ loadCachePromise = null;
4746
4759
  constructor(config) {
4747
4760
  this.apiKey = config?.apiKey;
4748
4761
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
4749
4762
  this.timeout = config?.timeout ?? 1e4;
4750
4763
  this.debug = config?.debug ?? false;
4764
+ this.storage = config?.storage ?? null;
4751
4765
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
4752
4766
  }
4753
4767
  async getPrices(tokenNames) {
4754
4768
  if (tokenNames.length === 0) {
4755
4769
  return /* @__PURE__ */ new Map();
4756
4770
  }
4771
+ if (!this.persistentCacheLoaded && this.storage) {
4772
+ await this.loadFromStorage();
4773
+ }
4757
4774
  const now = Date.now();
4758
4775
  const result = /* @__PURE__ */ new Map();
4759
4776
  const uncachedNames = [];
4760
4777
  for (const name of tokenNames) {
4761
4778
  const cached = this.cache.get(name);
4762
4779
  if (cached && cached.expiresAt > now) {
4763
- if (cached.price !== null) {
4764
- result.set(name, cached.price);
4765
- }
4780
+ result.set(name, cached.price);
4766
4781
  } else {
4767
4782
  uncachedNames.push(name);
4768
4783
  }
@@ -4770,6 +4785,41 @@ var CoinGeckoPriceProvider = class {
4770
4785
  if (uncachedNames.length === 0) {
4771
4786
  return result;
4772
4787
  }
4788
+ if (this.fetchPromise && this.fetchNames) {
4789
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
4790
+ if (allCovered) {
4791
+ if (this.debug) {
4792
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
4793
+ }
4794
+ const fetched = await this.fetchPromise;
4795
+ for (const name of uncachedNames) {
4796
+ const price = fetched.get(name);
4797
+ if (price) {
4798
+ result.set(name, price);
4799
+ }
4800
+ }
4801
+ return result;
4802
+ }
4803
+ }
4804
+ const fetchPromise = this.doFetch(uncachedNames);
4805
+ this.fetchPromise = fetchPromise;
4806
+ this.fetchNames = new Set(uncachedNames);
4807
+ try {
4808
+ const fetched = await fetchPromise;
4809
+ for (const [name, price] of fetched) {
4810
+ result.set(name, price);
4811
+ }
4812
+ } finally {
4813
+ if (this.fetchPromise === fetchPromise) {
4814
+ this.fetchPromise = null;
4815
+ this.fetchNames = null;
4816
+ }
4817
+ }
4818
+ return result;
4819
+ }
4820
+ async doFetch(uncachedNames) {
4821
+ const result = /* @__PURE__ */ new Map();
4822
+ const now = Date.now();
4773
4823
  try {
4774
4824
  const ids = uncachedNames.join(",");
4775
4825
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -4785,6 +4835,9 @@ var CoinGeckoPriceProvider = class {
4785
4835
  signal: AbortSignal.timeout(this.timeout)
4786
4836
  });
4787
4837
  if (!response.ok) {
4838
+ if (response.status === 429) {
4839
+ this.extendCacheOnRateLimit(uncachedNames);
4840
+ }
4788
4841
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
4789
4842
  }
4790
4843
  const data = await response.json();
@@ -4803,25 +4856,113 @@ var CoinGeckoPriceProvider = class {
4803
4856
  }
4804
4857
  for (const name of uncachedNames) {
4805
4858
  if (!result.has(name)) {
4806
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
4859
+ const zeroPrice = {
4860
+ tokenName: name,
4861
+ priceUsd: 0,
4862
+ priceEur: 0,
4863
+ change24h: 0,
4864
+ timestamp: now
4865
+ };
4866
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
4867
+ result.set(name, zeroPrice);
4807
4868
  }
4808
4869
  }
4809
4870
  if (this.debug) {
4810
4871
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
4811
4872
  }
4873
+ this.saveToStorage();
4812
4874
  } catch (error) {
4813
4875
  if (this.debug) {
4814
4876
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
4815
4877
  }
4816
4878
  for (const name of uncachedNames) {
4817
4879
  const stale = this.cache.get(name);
4818
- if (stale?.price) {
4880
+ if (stale) {
4819
4881
  result.set(name, stale.price);
4820
4882
  }
4821
4883
  }
4822
4884
  }
4823
4885
  return result;
4824
4886
  }
4887
+ // ===========================================================================
4888
+ // Persistent Storage
4889
+ // ===========================================================================
4890
+ /**
4891
+ * Load cached prices from StorageProvider into in-memory cache.
4892
+ * Only loads entries that are still within cacheTtlMs.
4893
+ */
4894
+ async loadFromStorage() {
4895
+ if (this.loadCachePromise) {
4896
+ return this.loadCachePromise;
4897
+ }
4898
+ this.loadCachePromise = this.doLoadFromStorage();
4899
+ try {
4900
+ await this.loadCachePromise;
4901
+ } finally {
4902
+ this.loadCachePromise = null;
4903
+ }
4904
+ }
4905
+ async doLoadFromStorage() {
4906
+ this.persistentCacheLoaded = true;
4907
+ if (!this.storage) return;
4908
+ try {
4909
+ const [cached, cachedTs] = await Promise.all([
4910
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
4911
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
4912
+ ]);
4913
+ if (!cached || !cachedTs) return;
4914
+ const ts = parseInt(cachedTs, 10);
4915
+ if (isNaN(ts)) return;
4916
+ const age = Date.now() - ts;
4917
+ if (age > this.cacheTtlMs) return;
4918
+ const data = JSON.parse(cached);
4919
+ const expiresAt = ts + this.cacheTtlMs;
4920
+ for (const [name, price] of Object.entries(data)) {
4921
+ if (!this.cache.has(name)) {
4922
+ this.cache.set(name, { price, expiresAt });
4923
+ }
4924
+ }
4925
+ if (this.debug) {
4926
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
4927
+ }
4928
+ } catch {
4929
+ }
4930
+ }
4931
+ /**
4932
+ * Save current prices to StorageProvider (fire-and-forget).
4933
+ */
4934
+ saveToStorage() {
4935
+ if (!this.storage) return;
4936
+ const data = {};
4937
+ for (const [name, entry] of this.cache) {
4938
+ data[name] = entry.price;
4939
+ }
4940
+ Promise.all([
4941
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
4942
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
4943
+ ]).catch(() => {
4944
+ });
4945
+ }
4946
+ // ===========================================================================
4947
+ // Rate-limit handling
4948
+ // ===========================================================================
4949
+ /**
4950
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
4951
+ * don't immediately retry and hammer the API.
4952
+ */
4953
+ extendCacheOnRateLimit(names) {
4954
+ const backoffMs = 6e4;
4955
+ const extendedExpiry = Date.now() + backoffMs;
4956
+ for (const name of names) {
4957
+ const existing = this.cache.get(name);
4958
+ if (existing) {
4959
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
4960
+ }
4961
+ }
4962
+ if (this.debug) {
4963
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
4964
+ }
4965
+ }
4825
4966
  async getPrice(tokenName) {
4826
4967
  const prices = await this.getPrices([tokenName]);
4827
4968
  return prices.get(tokenName) ?? null;
@@ -4855,6 +4996,7 @@ var TokenRegistry = class _TokenRegistry {
4855
4996
  refreshTimer = null;
4856
4997
  lastRefreshAt = 0;
4857
4998
  refreshPromise = null;
4999
+ initialLoadPromise = null;
4858
5000
  constructor() {
4859
5001
  this.definitionsById = /* @__PURE__ */ new Map();
4860
5002
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -4893,13 +5035,8 @@ var TokenRegistry = class _TokenRegistry {
4893
5035
  if (options.refreshIntervalMs !== void 0) {
4894
5036
  instance.refreshIntervalMs = options.refreshIntervalMs;
4895
5037
  }
4896
- if (instance.storage) {
4897
- instance.loadFromCache();
4898
- }
4899
5038
  const autoRefresh = options.autoRefresh ?? true;
4900
- if (autoRefresh && instance.remoteUrl) {
4901
- instance.startAutoRefresh();
4902
- }
5039
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
4903
5040
  }
4904
5041
  /**
4905
5042
  * Reset the singleton instance (useful for testing).
@@ -4917,6 +5054,53 @@ var TokenRegistry = class _TokenRegistry {
4917
5054
  static destroy() {
4918
5055
  _TokenRegistry.resetInstance();
4919
5056
  }
5057
+ /**
5058
+ * Wait for the initial data load (cache or remote) to complete.
5059
+ * Returns true if data was loaded, false if not (timeout or no data source).
5060
+ *
5061
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5062
+ */
5063
+ static async waitForReady(timeoutMs = 1e4) {
5064
+ const instance = _TokenRegistry.getInstance();
5065
+ if (!instance.initialLoadPromise) {
5066
+ return instance.definitionsById.size > 0;
5067
+ }
5068
+ if (timeoutMs <= 0) {
5069
+ return instance.initialLoadPromise;
5070
+ }
5071
+ return Promise.race([
5072
+ instance.initialLoadPromise,
5073
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5074
+ ]);
5075
+ }
5076
+ // ===========================================================================
5077
+ // Initial Load
5078
+ // ===========================================================================
5079
+ /**
5080
+ * Perform initial data load: try cache first, fall back to remote fetch.
5081
+ * After initial data is available, start periodic auto-refresh if configured.
5082
+ */
5083
+ async performInitialLoad(autoRefresh) {
5084
+ let loaded = false;
5085
+ if (this.storage) {
5086
+ loaded = await this.loadFromCache();
5087
+ }
5088
+ if (loaded) {
5089
+ if (autoRefresh && this.remoteUrl) {
5090
+ this.startAutoRefresh();
5091
+ }
5092
+ return true;
5093
+ }
5094
+ if (autoRefresh && this.remoteUrl) {
5095
+ loaded = await this.refreshFromRemote();
5096
+ this.stopAutoRefresh();
5097
+ this.refreshTimer = setInterval(() => {
5098
+ this.refreshFromRemote();
5099
+ }, this.refreshIntervalMs);
5100
+ return loaded;
5101
+ }
5102
+ return false;
5103
+ }
4920
5104
  // ===========================================================================
4921
5105
  // Cache (StorageProvider)
4922
5106
  // ===========================================================================
@@ -5246,7 +5430,7 @@ function resolveL1Config(network, config) {
5246
5430
  enableVesting: config.enableVesting
5247
5431
  };
5248
5432
  }
5249
- function resolvePriceConfig(config) {
5433
+ function resolvePriceConfig(config, storage) {
5250
5434
  if (config === void 0) {
5251
5435
  return void 0;
5252
5436
  }
@@ -5256,7 +5440,8 @@ function resolvePriceConfig(config) {
5256
5440
  baseUrl: config.baseUrl,
5257
5441
  cacheTtlMs: config.cacheTtlMs,
5258
5442
  timeout: config.timeout,
5259
- debug: config.debug
5443
+ debug: config.debug,
5444
+ storage
5260
5445
  };
5261
5446
  }
5262
5447
  function resolveGroupChatConfig(network, config) {
@@ -5273,16 +5458,6 @@ function resolveGroupChatConfig(network, config) {
5273
5458
  relays: config.relays ?? [...netConfig.groupRelays]
5274
5459
  };
5275
5460
  }
5276
- function resolveMarketConfig(config) {
5277
- if (!config) return void 0;
5278
- if (config === true) {
5279
- return { apiUrl: DEFAULT_MARKET_API_URL };
5280
- }
5281
- return {
5282
- apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5283
- timeout: config.timeout
5284
- };
5285
- }
5286
5461
 
5287
5462
  // impl/nodejs/index.ts
5288
5463
  function createNodeProviders(config) {
@@ -5290,21 +5465,19 @@ function createNodeProviders(config) {
5290
5465
  const transportConfig = resolveTransportConfig(network, config?.transport);
5291
5466
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5292
5467
  const l1Config = resolveL1Config(network, config?.l1);
5293
- const priceConfig = resolvePriceConfig(config?.price);
5294
5468
  const storage = createFileStorageProvider({
5295
5469
  dataDir: config?.dataDir ?? "./sphere-data",
5296
5470
  ...config?.walletFileName ? { fileName: config.walletFileName } : {}
5297
5471
  });
5472
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5298
5473
  const ipfsSync = config?.tokenSync?.ipfs;
5299
5474
  const ipfsTokenStorage = ipfsSync?.enabled ? createNodeIpfsStorageProvider(ipfsSync.config, storage) : void 0;
5300
5475
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5301
5476
  const networkConfig = getNetworkConfig(network);
5302
5477
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5303
- const market = resolveMarketConfig(config?.market);
5304
5478
  return {
5305
5479
  storage,
5306
5480
  groupChat,
5307
- market,
5308
5481
  tokenStorage: createFileTokenStorageProvider({
5309
5482
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
5310
5483
  }),