@unicitylabs/sphere-sdk 0.3.7 → 0.3.9

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.
Files changed (45) hide show
  1. package/dist/connect/index.cjs +770 -0
  2. package/dist/connect/index.cjs.map +1 -0
  3. package/dist/connect/index.d.cts +312 -0
  4. package/dist/connect/index.d.ts +312 -0
  5. package/dist/connect/index.js +747 -0
  6. package/dist/connect/index.js.map +1 -0
  7. package/dist/core/index.cjs +90 -2502
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +10 -165
  10. package/dist/core/index.d.ts +10 -165
  11. package/dist/core/index.js +86 -2498
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/impl/browser/connect/index.cjs +271 -0
  14. package/dist/impl/browser/connect/index.cjs.map +1 -0
  15. package/dist/impl/browser/connect/index.d.cts +137 -0
  16. package/dist/impl/browser/connect/index.d.ts +137 -0
  17. package/dist/impl/browser/connect/index.js +248 -0
  18. package/dist/impl/browser/connect/index.js.map +1 -0
  19. package/dist/impl/browser/index.cjs +201 -28
  20. package/dist/impl/browser/index.cjs.map +1 -1
  21. package/dist/impl/browser/index.js +201 -28
  22. package/dist/impl/browser/index.js.map +1 -1
  23. package/dist/impl/browser/ipfs.cjs +6 -1
  24. package/dist/impl/browser/ipfs.cjs.map +1 -1
  25. package/dist/impl/browser/ipfs.js +6 -1
  26. package/dist/impl/browser/ipfs.js.map +1 -1
  27. package/dist/impl/nodejs/connect/index.cjs +372 -0
  28. package/dist/impl/nodejs/connect/index.cjs.map +1 -0
  29. package/dist/impl/nodejs/connect/index.d.cts +178 -0
  30. package/dist/impl/nodejs/connect/index.d.ts +178 -0
  31. package/dist/impl/nodejs/connect/index.js +333 -0
  32. package/dist/impl/nodejs/connect/index.js.map +1 -0
  33. package/dist/impl/nodejs/index.cjs +201 -28
  34. package/dist/impl/nodejs/index.cjs.map +1 -1
  35. package/dist/impl/nodejs/index.d.cts +2 -21
  36. package/dist/impl/nodejs/index.d.ts +2 -21
  37. package/dist/impl/nodejs/index.js +201 -28
  38. package/dist/impl/nodejs/index.js.map +1 -1
  39. package/dist/index.cjs +232 -2513
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +59 -169
  42. package/dist/index.d.ts +59 -169
  43. package/dist/index.js +228 -2506
  44. package/dist/index.js.map +1 -1
  45. package/package.json +31 -1
@@ -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 {
@@ -3365,6 +3368,7 @@ async function loadIpnsModule() {
3365
3368
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3366
3369
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3367
3370
  const record = await createIPNSRecord(
3371
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3368
3372
  keyPair,
3369
3373
  `/ipfs/${cid}`,
3370
3374
  sequenceNumber,
@@ -5109,26 +5113,37 @@ var CoinGeckoPriceProvider = class {
5109
5113
  timeout;
5110
5114
  debug;
5111
5115
  baseUrl;
5116
+ storage;
5117
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
5118
+ fetchPromise = null;
5119
+ /** Token names being fetched in the current in-flight request */
5120
+ fetchNames = null;
5121
+ /** Whether persistent cache has been loaded into memory */
5122
+ persistentCacheLoaded = false;
5123
+ /** Promise for loading persistent cache (deduplication) */
5124
+ loadCachePromise = null;
5112
5125
  constructor(config) {
5113
5126
  this.apiKey = config?.apiKey;
5114
5127
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
5115
5128
  this.timeout = config?.timeout ?? 1e4;
5116
5129
  this.debug = config?.debug ?? false;
5130
+ this.storage = config?.storage ?? null;
5117
5131
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
5118
5132
  }
5119
5133
  async getPrices(tokenNames) {
5120
5134
  if (tokenNames.length === 0) {
5121
5135
  return /* @__PURE__ */ new Map();
5122
5136
  }
5137
+ if (!this.persistentCacheLoaded && this.storage) {
5138
+ await this.loadFromStorage();
5139
+ }
5123
5140
  const now = Date.now();
5124
5141
  const result = /* @__PURE__ */ new Map();
5125
5142
  const uncachedNames = [];
5126
5143
  for (const name of tokenNames) {
5127
5144
  const cached = this.cache.get(name);
5128
5145
  if (cached && cached.expiresAt > now) {
5129
- if (cached.price !== null) {
5130
- result.set(name, cached.price);
5131
- }
5146
+ result.set(name, cached.price);
5132
5147
  } else {
5133
5148
  uncachedNames.push(name);
5134
5149
  }
@@ -5136,6 +5151,41 @@ var CoinGeckoPriceProvider = class {
5136
5151
  if (uncachedNames.length === 0) {
5137
5152
  return result;
5138
5153
  }
5154
+ if (this.fetchPromise && this.fetchNames) {
5155
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
5156
+ if (allCovered) {
5157
+ if (this.debug) {
5158
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
5159
+ }
5160
+ const fetched = await this.fetchPromise;
5161
+ for (const name of uncachedNames) {
5162
+ const price = fetched.get(name);
5163
+ if (price) {
5164
+ result.set(name, price);
5165
+ }
5166
+ }
5167
+ return result;
5168
+ }
5169
+ }
5170
+ const fetchPromise = this.doFetch(uncachedNames);
5171
+ this.fetchPromise = fetchPromise;
5172
+ this.fetchNames = new Set(uncachedNames);
5173
+ try {
5174
+ const fetched = await fetchPromise;
5175
+ for (const [name, price] of fetched) {
5176
+ result.set(name, price);
5177
+ }
5178
+ } finally {
5179
+ if (this.fetchPromise === fetchPromise) {
5180
+ this.fetchPromise = null;
5181
+ this.fetchNames = null;
5182
+ }
5183
+ }
5184
+ return result;
5185
+ }
5186
+ async doFetch(uncachedNames) {
5187
+ const result = /* @__PURE__ */ new Map();
5188
+ const now = Date.now();
5139
5189
  try {
5140
5190
  const ids = uncachedNames.join(",");
5141
5191
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -5151,6 +5201,9 @@ var CoinGeckoPriceProvider = class {
5151
5201
  signal: AbortSignal.timeout(this.timeout)
5152
5202
  });
5153
5203
  if (!response.ok) {
5204
+ if (response.status === 429) {
5205
+ this.extendCacheOnRateLimit(uncachedNames);
5206
+ }
5154
5207
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
5155
5208
  }
5156
5209
  const data = await response.json();
@@ -5169,25 +5222,113 @@ var CoinGeckoPriceProvider = class {
5169
5222
  }
5170
5223
  for (const name of uncachedNames) {
5171
5224
  if (!result.has(name)) {
5172
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
5225
+ const zeroPrice = {
5226
+ tokenName: name,
5227
+ priceUsd: 0,
5228
+ priceEur: 0,
5229
+ change24h: 0,
5230
+ timestamp: now
5231
+ };
5232
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
5233
+ result.set(name, zeroPrice);
5173
5234
  }
5174
5235
  }
5175
5236
  if (this.debug) {
5176
5237
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
5177
5238
  }
5239
+ this.saveToStorage();
5178
5240
  } catch (error) {
5179
5241
  if (this.debug) {
5180
5242
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
5181
5243
  }
5182
5244
  for (const name of uncachedNames) {
5183
5245
  const stale = this.cache.get(name);
5184
- if (stale?.price) {
5246
+ if (stale) {
5185
5247
  result.set(name, stale.price);
5186
5248
  }
5187
5249
  }
5188
5250
  }
5189
5251
  return result;
5190
5252
  }
5253
+ // ===========================================================================
5254
+ // Persistent Storage
5255
+ // ===========================================================================
5256
+ /**
5257
+ * Load cached prices from StorageProvider into in-memory cache.
5258
+ * Only loads entries that are still within cacheTtlMs.
5259
+ */
5260
+ async loadFromStorage() {
5261
+ if (this.loadCachePromise) {
5262
+ return this.loadCachePromise;
5263
+ }
5264
+ this.loadCachePromise = this.doLoadFromStorage();
5265
+ try {
5266
+ await this.loadCachePromise;
5267
+ } finally {
5268
+ this.loadCachePromise = null;
5269
+ }
5270
+ }
5271
+ async doLoadFromStorage() {
5272
+ this.persistentCacheLoaded = true;
5273
+ if (!this.storage) return;
5274
+ try {
5275
+ const [cached, cachedTs] = await Promise.all([
5276
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
5277
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
5278
+ ]);
5279
+ if (!cached || !cachedTs) return;
5280
+ const ts = parseInt(cachedTs, 10);
5281
+ if (isNaN(ts)) return;
5282
+ const age = Date.now() - ts;
5283
+ if (age > this.cacheTtlMs) return;
5284
+ const data = JSON.parse(cached);
5285
+ const expiresAt = ts + this.cacheTtlMs;
5286
+ for (const [name, price] of Object.entries(data)) {
5287
+ if (!this.cache.has(name)) {
5288
+ this.cache.set(name, { price, expiresAt });
5289
+ }
5290
+ }
5291
+ if (this.debug) {
5292
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
5293
+ }
5294
+ } catch {
5295
+ }
5296
+ }
5297
+ /**
5298
+ * Save current prices to StorageProvider (fire-and-forget).
5299
+ */
5300
+ saveToStorage() {
5301
+ if (!this.storage) return;
5302
+ const data = {};
5303
+ for (const [name, entry] of this.cache) {
5304
+ data[name] = entry.price;
5305
+ }
5306
+ Promise.all([
5307
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
5308
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
5309
+ ]).catch(() => {
5310
+ });
5311
+ }
5312
+ // ===========================================================================
5313
+ // Rate-limit handling
5314
+ // ===========================================================================
5315
+ /**
5316
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
5317
+ * don't immediately retry and hammer the API.
5318
+ */
5319
+ extendCacheOnRateLimit(names) {
5320
+ const backoffMs = 6e4;
5321
+ const extendedExpiry = Date.now() + backoffMs;
5322
+ for (const name of names) {
5323
+ const existing = this.cache.get(name);
5324
+ if (existing) {
5325
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
5326
+ }
5327
+ }
5328
+ if (this.debug) {
5329
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
5330
+ }
5331
+ }
5191
5332
  async getPrice(tokenName) {
5192
5333
  const prices = await this.getPrices([tokenName]);
5193
5334
  return prices.get(tokenName) ?? null;
@@ -5221,6 +5362,7 @@ var TokenRegistry = class _TokenRegistry {
5221
5362
  refreshTimer = null;
5222
5363
  lastRefreshAt = 0;
5223
5364
  refreshPromise = null;
5365
+ initialLoadPromise = null;
5224
5366
  constructor() {
5225
5367
  this.definitionsById = /* @__PURE__ */ new Map();
5226
5368
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -5259,13 +5401,8 @@ var TokenRegistry = class _TokenRegistry {
5259
5401
  if (options.refreshIntervalMs !== void 0) {
5260
5402
  instance.refreshIntervalMs = options.refreshIntervalMs;
5261
5403
  }
5262
- if (instance.storage) {
5263
- instance.loadFromCache();
5264
- }
5265
5404
  const autoRefresh = options.autoRefresh ?? true;
5266
- if (autoRefresh && instance.remoteUrl) {
5267
- instance.startAutoRefresh();
5268
- }
5405
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
5269
5406
  }
5270
5407
  /**
5271
5408
  * Reset the singleton instance (useful for testing).
@@ -5283,6 +5420,53 @@ var TokenRegistry = class _TokenRegistry {
5283
5420
  static destroy() {
5284
5421
  _TokenRegistry.resetInstance();
5285
5422
  }
5423
+ /**
5424
+ * Wait for the initial data load (cache or remote) to complete.
5425
+ * Returns true if data was loaded, false if not (timeout or no data source).
5426
+ *
5427
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5428
+ */
5429
+ static async waitForReady(timeoutMs = 1e4) {
5430
+ const instance = _TokenRegistry.getInstance();
5431
+ if (!instance.initialLoadPromise) {
5432
+ return instance.definitionsById.size > 0;
5433
+ }
5434
+ if (timeoutMs <= 0) {
5435
+ return instance.initialLoadPromise;
5436
+ }
5437
+ return Promise.race([
5438
+ instance.initialLoadPromise,
5439
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5440
+ ]);
5441
+ }
5442
+ // ===========================================================================
5443
+ // Initial Load
5444
+ // ===========================================================================
5445
+ /**
5446
+ * Perform initial data load: try cache first, fall back to remote fetch.
5447
+ * After initial data is available, start periodic auto-refresh if configured.
5448
+ */
5449
+ async performInitialLoad(autoRefresh) {
5450
+ let loaded = false;
5451
+ if (this.storage) {
5452
+ loaded = await this.loadFromCache();
5453
+ }
5454
+ if (loaded) {
5455
+ if (autoRefresh && this.remoteUrl) {
5456
+ this.startAutoRefresh();
5457
+ }
5458
+ return true;
5459
+ }
5460
+ if (autoRefresh && this.remoteUrl) {
5461
+ loaded = await this.refreshFromRemote();
5462
+ this.stopAutoRefresh();
5463
+ this.refreshTimer = setInterval(() => {
5464
+ this.refreshFromRemote();
5465
+ }, this.refreshIntervalMs);
5466
+ return loaded;
5467
+ }
5468
+ return false;
5469
+ }
5286
5470
  // ===========================================================================
5287
5471
  // Cache (StorageProvider)
5288
5472
  // ===========================================================================
@@ -5612,7 +5796,7 @@ function resolveL1Config(network, config) {
5612
5796
  enableVesting: config.enableVesting
5613
5797
  };
5614
5798
  }
5615
- function resolvePriceConfig(config) {
5799
+ function resolvePriceConfig(config, storage) {
5616
5800
  if (config === void 0) {
5617
5801
  return void 0;
5618
5802
  }
@@ -5622,7 +5806,8 @@ function resolvePriceConfig(config) {
5622
5806
  baseUrl: config.baseUrl,
5623
5807
  cacheTtlMs: config.cacheTtlMs,
5624
5808
  timeout: config.timeout,
5625
- debug: config.debug
5809
+ debug: config.debug,
5810
+ storage
5626
5811
  };
5627
5812
  }
5628
5813
  function resolveArrayConfig(defaults, replace, additional) {
@@ -5649,16 +5834,6 @@ function resolveGroupChatConfig(network, config) {
5649
5834
  relays: config.relays ?? [...netConfig.groupRelays]
5650
5835
  };
5651
5836
  }
5652
- function resolveMarketConfig(config) {
5653
- if (!config) return void 0;
5654
- if (config === true) {
5655
- return { apiUrl: DEFAULT_MARKET_API_URL };
5656
- }
5657
- return {
5658
- apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5659
- timeout: config.timeout
5660
- };
5661
- }
5662
5837
 
5663
5838
  // impl/browser/index.ts
5664
5839
  if (typeof globalThis.Buffer === "undefined") {
@@ -5716,8 +5891,8 @@ function createBrowserProviders(config) {
5716
5891
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5717
5892
  const l1Config = resolveL1Config(network, config?.l1);
5718
5893
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
5719
- const priceConfig = resolvePriceConfig(config?.price);
5720
5894
  const storage = createLocalStorageProvider(config?.storage);
5895
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5721
5896
  const ipfsConfig = tokenSyncConfig?.ipfs;
5722
5897
  const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
5723
5898
  gateways: ipfsConfig.gateways,
@@ -5727,11 +5902,9 @@ function createBrowserProviders(config) {
5727
5902
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5728
5903
  const networkConfig = getNetworkConfig(network);
5729
5904
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5730
- const market = resolveMarketConfig(config?.market);
5731
5905
  return {
5732
5906
  storage,
5733
5907
  groupChat,
5734
- market,
5735
5908
  transport: createNostrTransportProvider({
5736
5909
  relays: transportConfig.relays,
5737
5910
  timeout: transportConfig.timeout,