@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.
@@ -90,7 +90,11 @@ var STORAGE_KEYS_GLOBAL = {
90
90
  /** Cached token registry JSON (fetched from remote) */
91
91
  TOKEN_REGISTRY_CACHE: "token_registry_cache",
92
92
  /** Timestamp of last token registry cache update (ms since epoch) */
93
- TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
93
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
94
+ /** Cached price data JSON (from CoinGecko or other provider) */
95
+ PRICE_CACHE: "price_cache",
96
+ /** Timestamp of last price cache update (ms since epoch) */
97
+ PRICE_CACHE_TS: "price_cache_ts"
94
98
  };
95
99
  var STORAGE_KEYS_ADDRESS = {
96
100
  /** Pending transfers for this address */
@@ -215,7 +219,6 @@ var TIMEOUTS = {
215
219
  /** Sync interval */
216
220
  SYNC_INTERVAL: 6e4
217
221
  };
218
- var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
219
222
 
220
223
  // impl/nodejs/storage/FileStorageProvider.ts
221
224
  var FileStorageProvider = class {
@@ -3042,6 +3045,7 @@ async function loadIpnsModule() {
3042
3045
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3043
3046
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3044
3047
  const record = await createIPNSRecord(
3048
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3045
3049
  keyPair,
3046
3050
  `/ipfs/${cid}`,
3047
3051
  sequenceNumber,
@@ -4783,26 +4787,37 @@ var CoinGeckoPriceProvider = class {
4783
4787
  timeout;
4784
4788
  debug;
4785
4789
  baseUrl;
4790
+ storage;
4791
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
4792
+ fetchPromise = null;
4793
+ /** Token names being fetched in the current in-flight request */
4794
+ fetchNames = null;
4795
+ /** Whether persistent cache has been loaded into memory */
4796
+ persistentCacheLoaded = false;
4797
+ /** Promise for loading persistent cache (deduplication) */
4798
+ loadCachePromise = null;
4786
4799
  constructor(config) {
4787
4800
  this.apiKey = config?.apiKey;
4788
4801
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
4789
4802
  this.timeout = config?.timeout ?? 1e4;
4790
4803
  this.debug = config?.debug ?? false;
4804
+ this.storage = config?.storage ?? null;
4791
4805
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
4792
4806
  }
4793
4807
  async getPrices(tokenNames) {
4794
4808
  if (tokenNames.length === 0) {
4795
4809
  return /* @__PURE__ */ new Map();
4796
4810
  }
4811
+ if (!this.persistentCacheLoaded && this.storage) {
4812
+ await this.loadFromStorage();
4813
+ }
4797
4814
  const now = Date.now();
4798
4815
  const result = /* @__PURE__ */ new Map();
4799
4816
  const uncachedNames = [];
4800
4817
  for (const name of tokenNames) {
4801
4818
  const cached = this.cache.get(name);
4802
4819
  if (cached && cached.expiresAt > now) {
4803
- if (cached.price !== null) {
4804
- result.set(name, cached.price);
4805
- }
4820
+ result.set(name, cached.price);
4806
4821
  } else {
4807
4822
  uncachedNames.push(name);
4808
4823
  }
@@ -4810,6 +4825,41 @@ var CoinGeckoPriceProvider = class {
4810
4825
  if (uncachedNames.length === 0) {
4811
4826
  return result;
4812
4827
  }
4828
+ if (this.fetchPromise && this.fetchNames) {
4829
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
4830
+ if (allCovered) {
4831
+ if (this.debug) {
4832
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
4833
+ }
4834
+ const fetched = await this.fetchPromise;
4835
+ for (const name of uncachedNames) {
4836
+ const price = fetched.get(name);
4837
+ if (price) {
4838
+ result.set(name, price);
4839
+ }
4840
+ }
4841
+ return result;
4842
+ }
4843
+ }
4844
+ const fetchPromise = this.doFetch(uncachedNames);
4845
+ this.fetchPromise = fetchPromise;
4846
+ this.fetchNames = new Set(uncachedNames);
4847
+ try {
4848
+ const fetched = await fetchPromise;
4849
+ for (const [name, price] of fetched) {
4850
+ result.set(name, price);
4851
+ }
4852
+ } finally {
4853
+ if (this.fetchPromise === fetchPromise) {
4854
+ this.fetchPromise = null;
4855
+ this.fetchNames = null;
4856
+ }
4857
+ }
4858
+ return result;
4859
+ }
4860
+ async doFetch(uncachedNames) {
4861
+ const result = /* @__PURE__ */ new Map();
4862
+ const now = Date.now();
4813
4863
  try {
4814
4864
  const ids = uncachedNames.join(",");
4815
4865
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -4825,6 +4875,9 @@ var CoinGeckoPriceProvider = class {
4825
4875
  signal: AbortSignal.timeout(this.timeout)
4826
4876
  });
4827
4877
  if (!response.ok) {
4878
+ if (response.status === 429) {
4879
+ this.extendCacheOnRateLimit(uncachedNames);
4880
+ }
4828
4881
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
4829
4882
  }
4830
4883
  const data = await response.json();
@@ -4843,25 +4896,113 @@ var CoinGeckoPriceProvider = class {
4843
4896
  }
4844
4897
  for (const name of uncachedNames) {
4845
4898
  if (!result.has(name)) {
4846
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
4899
+ const zeroPrice = {
4900
+ tokenName: name,
4901
+ priceUsd: 0,
4902
+ priceEur: 0,
4903
+ change24h: 0,
4904
+ timestamp: now
4905
+ };
4906
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
4907
+ result.set(name, zeroPrice);
4847
4908
  }
4848
4909
  }
4849
4910
  if (this.debug) {
4850
4911
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
4851
4912
  }
4913
+ this.saveToStorage();
4852
4914
  } catch (error) {
4853
4915
  if (this.debug) {
4854
4916
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
4855
4917
  }
4856
4918
  for (const name of uncachedNames) {
4857
4919
  const stale = this.cache.get(name);
4858
- if (stale?.price) {
4920
+ if (stale) {
4859
4921
  result.set(name, stale.price);
4860
4922
  }
4861
4923
  }
4862
4924
  }
4863
4925
  return result;
4864
4926
  }
4927
+ // ===========================================================================
4928
+ // Persistent Storage
4929
+ // ===========================================================================
4930
+ /**
4931
+ * Load cached prices from StorageProvider into in-memory cache.
4932
+ * Only loads entries that are still within cacheTtlMs.
4933
+ */
4934
+ async loadFromStorage() {
4935
+ if (this.loadCachePromise) {
4936
+ return this.loadCachePromise;
4937
+ }
4938
+ this.loadCachePromise = this.doLoadFromStorage();
4939
+ try {
4940
+ await this.loadCachePromise;
4941
+ } finally {
4942
+ this.loadCachePromise = null;
4943
+ }
4944
+ }
4945
+ async doLoadFromStorage() {
4946
+ this.persistentCacheLoaded = true;
4947
+ if (!this.storage) return;
4948
+ try {
4949
+ const [cached, cachedTs] = await Promise.all([
4950
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
4951
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
4952
+ ]);
4953
+ if (!cached || !cachedTs) return;
4954
+ const ts = parseInt(cachedTs, 10);
4955
+ if (isNaN(ts)) return;
4956
+ const age = Date.now() - ts;
4957
+ if (age > this.cacheTtlMs) return;
4958
+ const data = JSON.parse(cached);
4959
+ const expiresAt = ts + this.cacheTtlMs;
4960
+ for (const [name, price] of Object.entries(data)) {
4961
+ if (!this.cache.has(name)) {
4962
+ this.cache.set(name, { price, expiresAt });
4963
+ }
4964
+ }
4965
+ if (this.debug) {
4966
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
4967
+ }
4968
+ } catch {
4969
+ }
4970
+ }
4971
+ /**
4972
+ * Save current prices to StorageProvider (fire-and-forget).
4973
+ */
4974
+ saveToStorage() {
4975
+ if (!this.storage) return;
4976
+ const data = {};
4977
+ for (const [name, entry] of this.cache) {
4978
+ data[name] = entry.price;
4979
+ }
4980
+ Promise.all([
4981
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
4982
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
4983
+ ]).catch(() => {
4984
+ });
4985
+ }
4986
+ // ===========================================================================
4987
+ // Rate-limit handling
4988
+ // ===========================================================================
4989
+ /**
4990
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
4991
+ * don't immediately retry and hammer the API.
4992
+ */
4993
+ extendCacheOnRateLimit(names) {
4994
+ const backoffMs = 6e4;
4995
+ const extendedExpiry = Date.now() + backoffMs;
4996
+ for (const name of names) {
4997
+ const existing = this.cache.get(name);
4998
+ if (existing) {
4999
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
5000
+ }
5001
+ }
5002
+ if (this.debug) {
5003
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
5004
+ }
5005
+ }
4865
5006
  async getPrice(tokenName) {
4866
5007
  const prices = await this.getPrices([tokenName]);
4867
5008
  return prices.get(tokenName) ?? null;
@@ -4895,6 +5036,7 @@ var TokenRegistry = class _TokenRegistry {
4895
5036
  refreshTimer = null;
4896
5037
  lastRefreshAt = 0;
4897
5038
  refreshPromise = null;
5039
+ initialLoadPromise = null;
4898
5040
  constructor() {
4899
5041
  this.definitionsById = /* @__PURE__ */ new Map();
4900
5042
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -4933,13 +5075,8 @@ var TokenRegistry = class _TokenRegistry {
4933
5075
  if (options.refreshIntervalMs !== void 0) {
4934
5076
  instance.refreshIntervalMs = options.refreshIntervalMs;
4935
5077
  }
4936
- if (instance.storage) {
4937
- instance.loadFromCache();
4938
- }
4939
5078
  const autoRefresh = options.autoRefresh ?? true;
4940
- if (autoRefresh && instance.remoteUrl) {
4941
- instance.startAutoRefresh();
4942
- }
5079
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
4943
5080
  }
4944
5081
  /**
4945
5082
  * Reset the singleton instance (useful for testing).
@@ -4957,6 +5094,53 @@ var TokenRegistry = class _TokenRegistry {
4957
5094
  static destroy() {
4958
5095
  _TokenRegistry.resetInstance();
4959
5096
  }
5097
+ /**
5098
+ * Wait for the initial data load (cache or remote) to complete.
5099
+ * Returns true if data was loaded, false if not (timeout or no data source).
5100
+ *
5101
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5102
+ */
5103
+ static async waitForReady(timeoutMs = 1e4) {
5104
+ const instance = _TokenRegistry.getInstance();
5105
+ if (!instance.initialLoadPromise) {
5106
+ return instance.definitionsById.size > 0;
5107
+ }
5108
+ if (timeoutMs <= 0) {
5109
+ return instance.initialLoadPromise;
5110
+ }
5111
+ return Promise.race([
5112
+ instance.initialLoadPromise,
5113
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5114
+ ]);
5115
+ }
5116
+ // ===========================================================================
5117
+ // Initial Load
5118
+ // ===========================================================================
5119
+ /**
5120
+ * Perform initial data load: try cache first, fall back to remote fetch.
5121
+ * After initial data is available, start periodic auto-refresh if configured.
5122
+ */
5123
+ async performInitialLoad(autoRefresh) {
5124
+ let loaded = false;
5125
+ if (this.storage) {
5126
+ loaded = await this.loadFromCache();
5127
+ }
5128
+ if (loaded) {
5129
+ if (autoRefresh && this.remoteUrl) {
5130
+ this.startAutoRefresh();
5131
+ }
5132
+ return true;
5133
+ }
5134
+ if (autoRefresh && this.remoteUrl) {
5135
+ loaded = await this.refreshFromRemote();
5136
+ this.stopAutoRefresh();
5137
+ this.refreshTimer = setInterval(() => {
5138
+ this.refreshFromRemote();
5139
+ }, this.refreshIntervalMs);
5140
+ return loaded;
5141
+ }
5142
+ return false;
5143
+ }
4960
5144
  // ===========================================================================
4961
5145
  // Cache (StorageProvider)
4962
5146
  // ===========================================================================
@@ -5286,7 +5470,7 @@ function resolveL1Config(network, config) {
5286
5470
  enableVesting: config.enableVesting
5287
5471
  };
5288
5472
  }
5289
- function resolvePriceConfig(config) {
5473
+ function resolvePriceConfig(config, storage) {
5290
5474
  if (config === void 0) {
5291
5475
  return void 0;
5292
5476
  }
@@ -5296,7 +5480,8 @@ function resolvePriceConfig(config) {
5296
5480
  baseUrl: config.baseUrl,
5297
5481
  cacheTtlMs: config.cacheTtlMs,
5298
5482
  timeout: config.timeout,
5299
- debug: config.debug
5483
+ debug: config.debug,
5484
+ storage
5300
5485
  };
5301
5486
  }
5302
5487
  function resolveGroupChatConfig(network, config) {
@@ -5313,16 +5498,6 @@ function resolveGroupChatConfig(network, config) {
5313
5498
  relays: config.relays ?? [...netConfig.groupRelays]
5314
5499
  };
5315
5500
  }
5316
- function resolveMarketConfig(config) {
5317
- if (!config) return void 0;
5318
- if (config === true) {
5319
- return { apiUrl: DEFAULT_MARKET_API_URL };
5320
- }
5321
- return {
5322
- apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5323
- timeout: config.timeout
5324
- };
5325
- }
5326
5501
 
5327
5502
  // impl/nodejs/index.ts
5328
5503
  function createNodeProviders(config) {
@@ -5330,21 +5505,19 @@ function createNodeProviders(config) {
5330
5505
  const transportConfig = resolveTransportConfig(network, config?.transport);
5331
5506
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5332
5507
  const l1Config = resolveL1Config(network, config?.l1);
5333
- const priceConfig = resolvePriceConfig(config?.price);
5334
5508
  const storage = createFileStorageProvider({
5335
5509
  dataDir: config?.dataDir ?? "./sphere-data",
5336
5510
  ...config?.walletFileName ? { fileName: config.walletFileName } : {}
5337
5511
  });
5512
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5338
5513
  const ipfsSync = config?.tokenSync?.ipfs;
5339
5514
  const ipfsTokenStorage = ipfsSync?.enabled ? createNodeIpfsStorageProvider(ipfsSync.config, storage) : void 0;
5340
5515
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5341
5516
  const networkConfig = getNetworkConfig(network);
5342
5517
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5343
- const market = resolveMarketConfig(config?.market);
5344
5518
  return {
5345
5519
  storage,
5346
5520
  groupChat,
5347
- market,
5348
5521
  tokenStorage: createFileTokenStorageProvider({
5349
5522
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
5350
5523
  }),