@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.
@@ -40,7 +40,11 @@ var STORAGE_KEYS_GLOBAL = {
40
40
  /** Cached token registry JSON (fetched from remote) */
41
41
  TOKEN_REGISTRY_CACHE: "token_registry_cache",
42
42
  /** Timestamp of last token registry cache update (ms since epoch) */
43
- TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
43
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
44
+ /** Cached price data JSON (from CoinGecko or other provider) */
45
+ PRICE_CACHE: "price_cache",
46
+ /** Timestamp of last price cache update (ms since epoch) */
47
+ PRICE_CACHE_TS: "price_cache_ts"
44
48
  };
45
49
  var STORAGE_KEYS_ADDRESS = {
46
50
  /** Pending transfers for this address */
@@ -165,7 +169,6 @@ var TIMEOUTS = {
165
169
  /** Sync interval */
166
170
  SYNC_INTERVAL: 6e4
167
171
  };
168
- var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
169
172
 
170
173
  // impl/browser/storage/LocalStorageProvider.ts
171
174
  var LocalStorageProvider = class {
@@ -3228,6 +3231,7 @@ async function loadIpnsModule() {
3228
3231
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3229
3232
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3230
3233
  const record = await createIPNSRecord(
3234
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3231
3235
  keyPair,
3232
3236
  `/ipfs/${cid}`,
3233
3237
  sequenceNumber,
@@ -4972,26 +4976,37 @@ var CoinGeckoPriceProvider = class {
4972
4976
  timeout;
4973
4977
  debug;
4974
4978
  baseUrl;
4979
+ storage;
4980
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
4981
+ fetchPromise = null;
4982
+ /** Token names being fetched in the current in-flight request */
4983
+ fetchNames = null;
4984
+ /** Whether persistent cache has been loaded into memory */
4985
+ persistentCacheLoaded = false;
4986
+ /** Promise for loading persistent cache (deduplication) */
4987
+ loadCachePromise = null;
4975
4988
  constructor(config) {
4976
4989
  this.apiKey = config?.apiKey;
4977
4990
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
4978
4991
  this.timeout = config?.timeout ?? 1e4;
4979
4992
  this.debug = config?.debug ?? false;
4993
+ this.storage = config?.storage ?? null;
4980
4994
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
4981
4995
  }
4982
4996
  async getPrices(tokenNames) {
4983
4997
  if (tokenNames.length === 0) {
4984
4998
  return /* @__PURE__ */ new Map();
4985
4999
  }
5000
+ if (!this.persistentCacheLoaded && this.storage) {
5001
+ await this.loadFromStorage();
5002
+ }
4986
5003
  const now = Date.now();
4987
5004
  const result = /* @__PURE__ */ new Map();
4988
5005
  const uncachedNames = [];
4989
5006
  for (const name of tokenNames) {
4990
5007
  const cached = this.cache.get(name);
4991
5008
  if (cached && cached.expiresAt > now) {
4992
- if (cached.price !== null) {
4993
- result.set(name, cached.price);
4994
- }
5009
+ result.set(name, cached.price);
4995
5010
  } else {
4996
5011
  uncachedNames.push(name);
4997
5012
  }
@@ -4999,6 +5014,41 @@ var CoinGeckoPriceProvider = class {
4999
5014
  if (uncachedNames.length === 0) {
5000
5015
  return result;
5001
5016
  }
5017
+ if (this.fetchPromise && this.fetchNames) {
5018
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
5019
+ if (allCovered) {
5020
+ if (this.debug) {
5021
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
5022
+ }
5023
+ const fetched = await this.fetchPromise;
5024
+ for (const name of uncachedNames) {
5025
+ const price = fetched.get(name);
5026
+ if (price) {
5027
+ result.set(name, price);
5028
+ }
5029
+ }
5030
+ return result;
5031
+ }
5032
+ }
5033
+ const fetchPromise = this.doFetch(uncachedNames);
5034
+ this.fetchPromise = fetchPromise;
5035
+ this.fetchNames = new Set(uncachedNames);
5036
+ try {
5037
+ const fetched = await fetchPromise;
5038
+ for (const [name, price] of fetched) {
5039
+ result.set(name, price);
5040
+ }
5041
+ } finally {
5042
+ if (this.fetchPromise === fetchPromise) {
5043
+ this.fetchPromise = null;
5044
+ this.fetchNames = null;
5045
+ }
5046
+ }
5047
+ return result;
5048
+ }
5049
+ async doFetch(uncachedNames) {
5050
+ const result = /* @__PURE__ */ new Map();
5051
+ const now = Date.now();
5002
5052
  try {
5003
5053
  const ids = uncachedNames.join(",");
5004
5054
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -5014,6 +5064,9 @@ var CoinGeckoPriceProvider = class {
5014
5064
  signal: AbortSignal.timeout(this.timeout)
5015
5065
  });
5016
5066
  if (!response.ok) {
5067
+ if (response.status === 429) {
5068
+ this.extendCacheOnRateLimit(uncachedNames);
5069
+ }
5017
5070
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
5018
5071
  }
5019
5072
  const data = await response.json();
@@ -5032,25 +5085,113 @@ var CoinGeckoPriceProvider = class {
5032
5085
  }
5033
5086
  for (const name of uncachedNames) {
5034
5087
  if (!result.has(name)) {
5035
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
5088
+ const zeroPrice = {
5089
+ tokenName: name,
5090
+ priceUsd: 0,
5091
+ priceEur: 0,
5092
+ change24h: 0,
5093
+ timestamp: now
5094
+ };
5095
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
5096
+ result.set(name, zeroPrice);
5036
5097
  }
5037
5098
  }
5038
5099
  if (this.debug) {
5039
5100
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
5040
5101
  }
5102
+ this.saveToStorage();
5041
5103
  } catch (error) {
5042
5104
  if (this.debug) {
5043
5105
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
5044
5106
  }
5045
5107
  for (const name of uncachedNames) {
5046
5108
  const stale = this.cache.get(name);
5047
- if (stale?.price) {
5109
+ if (stale) {
5048
5110
  result.set(name, stale.price);
5049
5111
  }
5050
5112
  }
5051
5113
  }
5052
5114
  return result;
5053
5115
  }
5116
+ // ===========================================================================
5117
+ // Persistent Storage
5118
+ // ===========================================================================
5119
+ /**
5120
+ * Load cached prices from StorageProvider into in-memory cache.
5121
+ * Only loads entries that are still within cacheTtlMs.
5122
+ */
5123
+ async loadFromStorage() {
5124
+ if (this.loadCachePromise) {
5125
+ return this.loadCachePromise;
5126
+ }
5127
+ this.loadCachePromise = this.doLoadFromStorage();
5128
+ try {
5129
+ await this.loadCachePromise;
5130
+ } finally {
5131
+ this.loadCachePromise = null;
5132
+ }
5133
+ }
5134
+ async doLoadFromStorage() {
5135
+ this.persistentCacheLoaded = true;
5136
+ if (!this.storage) return;
5137
+ try {
5138
+ const [cached, cachedTs] = await Promise.all([
5139
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
5140
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
5141
+ ]);
5142
+ if (!cached || !cachedTs) return;
5143
+ const ts = parseInt(cachedTs, 10);
5144
+ if (isNaN(ts)) return;
5145
+ const age = Date.now() - ts;
5146
+ if (age > this.cacheTtlMs) return;
5147
+ const data = JSON.parse(cached);
5148
+ const expiresAt = ts + this.cacheTtlMs;
5149
+ for (const [name, price] of Object.entries(data)) {
5150
+ if (!this.cache.has(name)) {
5151
+ this.cache.set(name, { price, expiresAt });
5152
+ }
5153
+ }
5154
+ if (this.debug) {
5155
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
5156
+ }
5157
+ } catch {
5158
+ }
5159
+ }
5160
+ /**
5161
+ * Save current prices to StorageProvider (fire-and-forget).
5162
+ */
5163
+ saveToStorage() {
5164
+ if (!this.storage) return;
5165
+ const data = {};
5166
+ for (const [name, entry] of this.cache) {
5167
+ data[name] = entry.price;
5168
+ }
5169
+ Promise.all([
5170
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
5171
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
5172
+ ]).catch(() => {
5173
+ });
5174
+ }
5175
+ // ===========================================================================
5176
+ // Rate-limit handling
5177
+ // ===========================================================================
5178
+ /**
5179
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
5180
+ * don't immediately retry and hammer the API.
5181
+ */
5182
+ extendCacheOnRateLimit(names) {
5183
+ const backoffMs = 6e4;
5184
+ const extendedExpiry = Date.now() + backoffMs;
5185
+ for (const name of names) {
5186
+ const existing = this.cache.get(name);
5187
+ if (existing) {
5188
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
5189
+ }
5190
+ }
5191
+ if (this.debug) {
5192
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
5193
+ }
5194
+ }
5054
5195
  async getPrice(tokenName) {
5055
5196
  const prices = await this.getPrices([tokenName]);
5056
5197
  return prices.get(tokenName) ?? null;
@@ -5084,6 +5225,7 @@ var TokenRegistry = class _TokenRegistry {
5084
5225
  refreshTimer = null;
5085
5226
  lastRefreshAt = 0;
5086
5227
  refreshPromise = null;
5228
+ initialLoadPromise = null;
5087
5229
  constructor() {
5088
5230
  this.definitionsById = /* @__PURE__ */ new Map();
5089
5231
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -5122,13 +5264,8 @@ var TokenRegistry = class _TokenRegistry {
5122
5264
  if (options.refreshIntervalMs !== void 0) {
5123
5265
  instance.refreshIntervalMs = options.refreshIntervalMs;
5124
5266
  }
5125
- if (instance.storage) {
5126
- instance.loadFromCache();
5127
- }
5128
5267
  const autoRefresh = options.autoRefresh ?? true;
5129
- if (autoRefresh && instance.remoteUrl) {
5130
- instance.startAutoRefresh();
5131
- }
5268
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
5132
5269
  }
5133
5270
  /**
5134
5271
  * Reset the singleton instance (useful for testing).
@@ -5146,6 +5283,53 @@ var TokenRegistry = class _TokenRegistry {
5146
5283
  static destroy() {
5147
5284
  _TokenRegistry.resetInstance();
5148
5285
  }
5286
+ /**
5287
+ * Wait for the initial data load (cache or remote) to complete.
5288
+ * Returns true if data was loaded, false if not (timeout or no data source).
5289
+ *
5290
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5291
+ */
5292
+ static async waitForReady(timeoutMs = 1e4) {
5293
+ const instance = _TokenRegistry.getInstance();
5294
+ if (!instance.initialLoadPromise) {
5295
+ return instance.definitionsById.size > 0;
5296
+ }
5297
+ if (timeoutMs <= 0) {
5298
+ return instance.initialLoadPromise;
5299
+ }
5300
+ return Promise.race([
5301
+ instance.initialLoadPromise,
5302
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5303
+ ]);
5304
+ }
5305
+ // ===========================================================================
5306
+ // Initial Load
5307
+ // ===========================================================================
5308
+ /**
5309
+ * Perform initial data load: try cache first, fall back to remote fetch.
5310
+ * After initial data is available, start periodic auto-refresh if configured.
5311
+ */
5312
+ async performInitialLoad(autoRefresh) {
5313
+ let loaded = false;
5314
+ if (this.storage) {
5315
+ loaded = await this.loadFromCache();
5316
+ }
5317
+ if (loaded) {
5318
+ if (autoRefresh && this.remoteUrl) {
5319
+ this.startAutoRefresh();
5320
+ }
5321
+ return true;
5322
+ }
5323
+ if (autoRefresh && this.remoteUrl) {
5324
+ loaded = await this.refreshFromRemote();
5325
+ this.stopAutoRefresh();
5326
+ this.refreshTimer = setInterval(() => {
5327
+ this.refreshFromRemote();
5328
+ }, this.refreshIntervalMs);
5329
+ return loaded;
5330
+ }
5331
+ return false;
5332
+ }
5149
5333
  // ===========================================================================
5150
5334
  // Cache (StorageProvider)
5151
5335
  // ===========================================================================
@@ -5475,7 +5659,7 @@ function resolveL1Config(network, config) {
5475
5659
  enableVesting: config.enableVesting
5476
5660
  };
5477
5661
  }
5478
- function resolvePriceConfig(config) {
5662
+ function resolvePriceConfig(config, storage) {
5479
5663
  if (config === void 0) {
5480
5664
  return void 0;
5481
5665
  }
@@ -5485,7 +5669,8 @@ function resolvePriceConfig(config) {
5485
5669
  baseUrl: config.baseUrl,
5486
5670
  cacheTtlMs: config.cacheTtlMs,
5487
5671
  timeout: config.timeout,
5488
- debug: config.debug
5672
+ debug: config.debug,
5673
+ storage
5489
5674
  };
5490
5675
  }
5491
5676
  function resolveArrayConfig(defaults, replace, additional) {
@@ -5512,16 +5697,6 @@ function resolveGroupChatConfig(network, config) {
5512
5697
  relays: config.relays ?? [...netConfig.groupRelays]
5513
5698
  };
5514
5699
  }
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
- }
5525
5700
 
5526
5701
  // impl/browser/index.ts
5527
5702
  if (typeof globalThis.Buffer === "undefined") {
@@ -5579,8 +5754,8 @@ function createBrowserProviders(config) {
5579
5754
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5580
5755
  const l1Config = resolveL1Config(network, config?.l1);
5581
5756
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
5582
- const priceConfig = resolvePriceConfig(config?.price);
5583
5757
  const storage = createLocalStorageProvider(config?.storage);
5758
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5584
5759
  const ipfsConfig = tokenSyncConfig?.ipfs;
5585
5760
  const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
5586
5761
  gateways: ipfsConfig.gateways,
@@ -5590,11 +5765,9 @@ function createBrowserProviders(config) {
5590
5765
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5591
5766
  const networkConfig = getNetworkConfig(network);
5592
5767
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5593
- const market = resolveMarketConfig(config?.market);
5594
5768
  return {
5595
5769
  storage,
5596
5770
  groupChat,
5597
- market,
5598
5771
  transport: createNostrTransportProvider({
5599
5772
  relays: transportConfig.relays,
5600
5773
  timeout: transportConfig.timeout,