@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.
@@ -98,7 +98,11 @@ var STORAGE_KEYS_GLOBAL = {
98
98
  /** Cached token registry JSON (fetched from remote) */
99
99
  TOKEN_REGISTRY_CACHE: "token_registry_cache",
100
100
  /** Timestamp of last token registry cache update (ms since epoch) */
101
- TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts"
101
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
102
+ /** Cached price data JSON (from CoinGecko or other provider) */
103
+ PRICE_CACHE: "price_cache",
104
+ /** Timestamp of last price cache update (ms since epoch) */
105
+ PRICE_CACHE_TS: "price_cache_ts"
102
106
  };
103
107
  var STORAGE_KEYS_ADDRESS = {
104
108
  /** Pending transfers for this address */
@@ -223,7 +227,6 @@ var TIMEOUTS = {
223
227
  /** Sync interval */
224
228
  SYNC_INTERVAL: 6e4
225
229
  };
226
- var DEFAULT_MARKET_API_URL = "https://market-api.unicity.network";
227
230
 
228
231
  // impl/browser/storage/LocalStorageProvider.ts
229
232
  var LocalStorageProvider = class {
@@ -3277,6 +3280,7 @@ async function loadIpnsModule() {
3277
3280
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3278
3281
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3279
3282
  const record = await createIPNSRecord(
3283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3280
3284
  keyPair,
3281
3285
  `/ipfs/${cid}`,
3282
3286
  sequenceNumber,
@@ -5021,26 +5025,37 @@ var CoinGeckoPriceProvider = class {
5021
5025
  timeout;
5022
5026
  debug;
5023
5027
  baseUrl;
5028
+ storage;
5029
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
5030
+ fetchPromise = null;
5031
+ /** Token names being fetched in the current in-flight request */
5032
+ fetchNames = null;
5033
+ /** Whether persistent cache has been loaded into memory */
5034
+ persistentCacheLoaded = false;
5035
+ /** Promise for loading persistent cache (deduplication) */
5036
+ loadCachePromise = null;
5024
5037
  constructor(config) {
5025
5038
  this.apiKey = config?.apiKey;
5026
5039
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
5027
5040
  this.timeout = config?.timeout ?? 1e4;
5028
5041
  this.debug = config?.debug ?? false;
5042
+ this.storage = config?.storage ?? null;
5029
5043
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
5030
5044
  }
5031
5045
  async getPrices(tokenNames) {
5032
5046
  if (tokenNames.length === 0) {
5033
5047
  return /* @__PURE__ */ new Map();
5034
5048
  }
5049
+ if (!this.persistentCacheLoaded && this.storage) {
5050
+ await this.loadFromStorage();
5051
+ }
5035
5052
  const now = Date.now();
5036
5053
  const result = /* @__PURE__ */ new Map();
5037
5054
  const uncachedNames = [];
5038
5055
  for (const name of tokenNames) {
5039
5056
  const cached = this.cache.get(name);
5040
5057
  if (cached && cached.expiresAt > now) {
5041
- if (cached.price !== null) {
5042
- result.set(name, cached.price);
5043
- }
5058
+ result.set(name, cached.price);
5044
5059
  } else {
5045
5060
  uncachedNames.push(name);
5046
5061
  }
@@ -5048,6 +5063,41 @@ var CoinGeckoPriceProvider = class {
5048
5063
  if (uncachedNames.length === 0) {
5049
5064
  return result;
5050
5065
  }
5066
+ if (this.fetchPromise && this.fetchNames) {
5067
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
5068
+ if (allCovered) {
5069
+ if (this.debug) {
5070
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
5071
+ }
5072
+ const fetched = await this.fetchPromise;
5073
+ for (const name of uncachedNames) {
5074
+ const price = fetched.get(name);
5075
+ if (price) {
5076
+ result.set(name, price);
5077
+ }
5078
+ }
5079
+ return result;
5080
+ }
5081
+ }
5082
+ const fetchPromise = this.doFetch(uncachedNames);
5083
+ this.fetchPromise = fetchPromise;
5084
+ this.fetchNames = new Set(uncachedNames);
5085
+ try {
5086
+ const fetched = await fetchPromise;
5087
+ for (const [name, price] of fetched) {
5088
+ result.set(name, price);
5089
+ }
5090
+ } finally {
5091
+ if (this.fetchPromise === fetchPromise) {
5092
+ this.fetchPromise = null;
5093
+ this.fetchNames = null;
5094
+ }
5095
+ }
5096
+ return result;
5097
+ }
5098
+ async doFetch(uncachedNames) {
5099
+ const result = /* @__PURE__ */ new Map();
5100
+ const now = Date.now();
5051
5101
  try {
5052
5102
  const ids = uncachedNames.join(",");
5053
5103
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -5063,6 +5113,9 @@ var CoinGeckoPriceProvider = class {
5063
5113
  signal: AbortSignal.timeout(this.timeout)
5064
5114
  });
5065
5115
  if (!response.ok) {
5116
+ if (response.status === 429) {
5117
+ this.extendCacheOnRateLimit(uncachedNames);
5118
+ }
5066
5119
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
5067
5120
  }
5068
5121
  const data = await response.json();
@@ -5081,25 +5134,113 @@ var CoinGeckoPriceProvider = class {
5081
5134
  }
5082
5135
  for (const name of uncachedNames) {
5083
5136
  if (!result.has(name)) {
5084
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
5137
+ const zeroPrice = {
5138
+ tokenName: name,
5139
+ priceUsd: 0,
5140
+ priceEur: 0,
5141
+ change24h: 0,
5142
+ timestamp: now
5143
+ };
5144
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
5145
+ result.set(name, zeroPrice);
5085
5146
  }
5086
5147
  }
5087
5148
  if (this.debug) {
5088
5149
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
5089
5150
  }
5151
+ this.saveToStorage();
5090
5152
  } catch (error) {
5091
5153
  if (this.debug) {
5092
5154
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
5093
5155
  }
5094
5156
  for (const name of uncachedNames) {
5095
5157
  const stale = this.cache.get(name);
5096
- if (stale?.price) {
5158
+ if (stale) {
5097
5159
  result.set(name, stale.price);
5098
5160
  }
5099
5161
  }
5100
5162
  }
5101
5163
  return result;
5102
5164
  }
5165
+ // ===========================================================================
5166
+ // Persistent Storage
5167
+ // ===========================================================================
5168
+ /**
5169
+ * Load cached prices from StorageProvider into in-memory cache.
5170
+ * Only loads entries that are still within cacheTtlMs.
5171
+ */
5172
+ async loadFromStorage() {
5173
+ if (this.loadCachePromise) {
5174
+ return this.loadCachePromise;
5175
+ }
5176
+ this.loadCachePromise = this.doLoadFromStorage();
5177
+ try {
5178
+ await this.loadCachePromise;
5179
+ } finally {
5180
+ this.loadCachePromise = null;
5181
+ }
5182
+ }
5183
+ async doLoadFromStorage() {
5184
+ this.persistentCacheLoaded = true;
5185
+ if (!this.storage) return;
5186
+ try {
5187
+ const [cached, cachedTs] = await Promise.all([
5188
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
5189
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
5190
+ ]);
5191
+ if (!cached || !cachedTs) return;
5192
+ const ts = parseInt(cachedTs, 10);
5193
+ if (isNaN(ts)) return;
5194
+ const age = Date.now() - ts;
5195
+ if (age > this.cacheTtlMs) return;
5196
+ const data = JSON.parse(cached);
5197
+ const expiresAt = ts + this.cacheTtlMs;
5198
+ for (const [name, price] of Object.entries(data)) {
5199
+ if (!this.cache.has(name)) {
5200
+ this.cache.set(name, { price, expiresAt });
5201
+ }
5202
+ }
5203
+ if (this.debug) {
5204
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
5205
+ }
5206
+ } catch {
5207
+ }
5208
+ }
5209
+ /**
5210
+ * Save current prices to StorageProvider (fire-and-forget).
5211
+ */
5212
+ saveToStorage() {
5213
+ if (!this.storage) return;
5214
+ const data = {};
5215
+ for (const [name, entry] of this.cache) {
5216
+ data[name] = entry.price;
5217
+ }
5218
+ Promise.all([
5219
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
5220
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
5221
+ ]).catch(() => {
5222
+ });
5223
+ }
5224
+ // ===========================================================================
5225
+ // Rate-limit handling
5226
+ // ===========================================================================
5227
+ /**
5228
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
5229
+ * don't immediately retry and hammer the API.
5230
+ */
5231
+ extendCacheOnRateLimit(names) {
5232
+ const backoffMs = 6e4;
5233
+ const extendedExpiry = Date.now() + backoffMs;
5234
+ for (const name of names) {
5235
+ const existing = this.cache.get(name);
5236
+ if (existing) {
5237
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
5238
+ }
5239
+ }
5240
+ if (this.debug) {
5241
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
5242
+ }
5243
+ }
5103
5244
  async getPrice(tokenName) {
5104
5245
  const prices = await this.getPrices([tokenName]);
5105
5246
  return prices.get(tokenName) ?? null;
@@ -5133,6 +5274,7 @@ var TokenRegistry = class _TokenRegistry {
5133
5274
  refreshTimer = null;
5134
5275
  lastRefreshAt = 0;
5135
5276
  refreshPromise = null;
5277
+ initialLoadPromise = null;
5136
5278
  constructor() {
5137
5279
  this.definitionsById = /* @__PURE__ */ new Map();
5138
5280
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -5171,13 +5313,8 @@ var TokenRegistry = class _TokenRegistry {
5171
5313
  if (options.refreshIntervalMs !== void 0) {
5172
5314
  instance.refreshIntervalMs = options.refreshIntervalMs;
5173
5315
  }
5174
- if (instance.storage) {
5175
- instance.loadFromCache();
5176
- }
5177
5316
  const autoRefresh = options.autoRefresh ?? true;
5178
- if (autoRefresh && instance.remoteUrl) {
5179
- instance.startAutoRefresh();
5180
- }
5317
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
5181
5318
  }
5182
5319
  /**
5183
5320
  * Reset the singleton instance (useful for testing).
@@ -5195,6 +5332,53 @@ var TokenRegistry = class _TokenRegistry {
5195
5332
  static destroy() {
5196
5333
  _TokenRegistry.resetInstance();
5197
5334
  }
5335
+ /**
5336
+ * Wait for the initial data load (cache or remote) to complete.
5337
+ * Returns true if data was loaded, false if not (timeout or no data source).
5338
+ *
5339
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5340
+ */
5341
+ static async waitForReady(timeoutMs = 1e4) {
5342
+ const instance = _TokenRegistry.getInstance();
5343
+ if (!instance.initialLoadPromise) {
5344
+ return instance.definitionsById.size > 0;
5345
+ }
5346
+ if (timeoutMs <= 0) {
5347
+ return instance.initialLoadPromise;
5348
+ }
5349
+ return Promise.race([
5350
+ instance.initialLoadPromise,
5351
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5352
+ ]);
5353
+ }
5354
+ // ===========================================================================
5355
+ // Initial Load
5356
+ // ===========================================================================
5357
+ /**
5358
+ * Perform initial data load: try cache first, fall back to remote fetch.
5359
+ * After initial data is available, start periodic auto-refresh if configured.
5360
+ */
5361
+ async performInitialLoad(autoRefresh) {
5362
+ let loaded = false;
5363
+ if (this.storage) {
5364
+ loaded = await this.loadFromCache();
5365
+ }
5366
+ if (loaded) {
5367
+ if (autoRefresh && this.remoteUrl) {
5368
+ this.startAutoRefresh();
5369
+ }
5370
+ return true;
5371
+ }
5372
+ if (autoRefresh && this.remoteUrl) {
5373
+ loaded = await this.refreshFromRemote();
5374
+ this.stopAutoRefresh();
5375
+ this.refreshTimer = setInterval(() => {
5376
+ this.refreshFromRemote();
5377
+ }, this.refreshIntervalMs);
5378
+ return loaded;
5379
+ }
5380
+ return false;
5381
+ }
5198
5382
  // ===========================================================================
5199
5383
  // Cache (StorageProvider)
5200
5384
  // ===========================================================================
@@ -5524,7 +5708,7 @@ function resolveL1Config(network, config) {
5524
5708
  enableVesting: config.enableVesting
5525
5709
  };
5526
5710
  }
5527
- function resolvePriceConfig(config) {
5711
+ function resolvePriceConfig(config, storage) {
5528
5712
  if (config === void 0) {
5529
5713
  return void 0;
5530
5714
  }
@@ -5534,7 +5718,8 @@ function resolvePriceConfig(config) {
5534
5718
  baseUrl: config.baseUrl,
5535
5719
  cacheTtlMs: config.cacheTtlMs,
5536
5720
  timeout: config.timeout,
5537
- debug: config.debug
5721
+ debug: config.debug,
5722
+ storage
5538
5723
  };
5539
5724
  }
5540
5725
  function resolveArrayConfig(defaults, replace, additional) {
@@ -5561,16 +5746,6 @@ function resolveGroupChatConfig(network, config) {
5561
5746
  relays: config.relays ?? [...netConfig.groupRelays]
5562
5747
  };
5563
5748
  }
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
- }
5574
5749
 
5575
5750
  // impl/browser/index.ts
5576
5751
  if (typeof globalThis.Buffer === "undefined") {
@@ -5628,8 +5803,8 @@ function createBrowserProviders(config) {
5628
5803
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5629
5804
  const l1Config = resolveL1Config(network, config?.l1);
5630
5805
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
5631
- const priceConfig = resolvePriceConfig(config?.price);
5632
5806
  const storage = createLocalStorageProvider(config?.storage);
5807
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5633
5808
  const ipfsConfig = tokenSyncConfig?.ipfs;
5634
5809
  const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
5635
5810
  gateways: ipfsConfig.gateways,
@@ -5639,11 +5814,9 @@ function createBrowserProviders(config) {
5639
5814
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5640
5815
  const networkConfig = getNetworkConfig(network);
5641
5816
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5642
- const market = resolveMarketConfig(config?.market);
5643
5817
  return {
5644
5818
  storage,
5645
5819
  groupChat,
5646
- market,
5647
5820
  transport: createNostrTransportProvider({
5648
5821
  relays: transportConfig.relays,
5649
5822
  timeout: transportConfig.timeout,