@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
@@ -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 {
@@ -3152,6 +3155,7 @@ async function loadIpnsModule() {
3152
3155
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3153
3156
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3154
3157
  const record = await createIPNSRecord(
3158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3155
3159
  keyPair,
3156
3160
  `/ipfs/${cid}`,
3157
3161
  sequenceNumber,
@@ -4893,26 +4897,37 @@ var CoinGeckoPriceProvider = class {
4893
4897
  timeout;
4894
4898
  debug;
4895
4899
  baseUrl;
4900
+ storage;
4901
+ /** In-flight fetch promise for deduplication of concurrent getPrices() calls */
4902
+ fetchPromise = null;
4903
+ /** Token names being fetched in the current in-flight request */
4904
+ fetchNames = null;
4905
+ /** Whether persistent cache has been loaded into memory */
4906
+ persistentCacheLoaded = false;
4907
+ /** Promise for loading persistent cache (deduplication) */
4908
+ loadCachePromise = null;
4896
4909
  constructor(config) {
4897
4910
  this.apiKey = config?.apiKey;
4898
4911
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
4899
4912
  this.timeout = config?.timeout ?? 1e4;
4900
4913
  this.debug = config?.debug ?? false;
4914
+ this.storage = config?.storage ?? null;
4901
4915
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
4902
4916
  }
4903
4917
  async getPrices(tokenNames) {
4904
4918
  if (tokenNames.length === 0) {
4905
4919
  return /* @__PURE__ */ new Map();
4906
4920
  }
4921
+ if (!this.persistentCacheLoaded && this.storage) {
4922
+ await this.loadFromStorage();
4923
+ }
4907
4924
  const now = Date.now();
4908
4925
  const result = /* @__PURE__ */ new Map();
4909
4926
  const uncachedNames = [];
4910
4927
  for (const name of tokenNames) {
4911
4928
  const cached = this.cache.get(name);
4912
4929
  if (cached && cached.expiresAt > now) {
4913
- if (cached.price !== null) {
4914
- result.set(name, cached.price);
4915
- }
4930
+ result.set(name, cached.price);
4916
4931
  } else {
4917
4932
  uncachedNames.push(name);
4918
4933
  }
@@ -4920,6 +4935,41 @@ var CoinGeckoPriceProvider = class {
4920
4935
  if (uncachedNames.length === 0) {
4921
4936
  return result;
4922
4937
  }
4938
+ if (this.fetchPromise && this.fetchNames) {
4939
+ const allCovered = uncachedNames.every((n) => this.fetchNames.has(n));
4940
+ if (allCovered) {
4941
+ if (this.debug) {
4942
+ console.log(`[CoinGecko] Deduplicating request, reusing in-flight fetch`);
4943
+ }
4944
+ const fetched = await this.fetchPromise;
4945
+ for (const name of uncachedNames) {
4946
+ const price = fetched.get(name);
4947
+ if (price) {
4948
+ result.set(name, price);
4949
+ }
4950
+ }
4951
+ return result;
4952
+ }
4953
+ }
4954
+ const fetchPromise = this.doFetch(uncachedNames);
4955
+ this.fetchPromise = fetchPromise;
4956
+ this.fetchNames = new Set(uncachedNames);
4957
+ try {
4958
+ const fetched = await fetchPromise;
4959
+ for (const [name, price] of fetched) {
4960
+ result.set(name, price);
4961
+ }
4962
+ } finally {
4963
+ if (this.fetchPromise === fetchPromise) {
4964
+ this.fetchPromise = null;
4965
+ this.fetchNames = null;
4966
+ }
4967
+ }
4968
+ return result;
4969
+ }
4970
+ async doFetch(uncachedNames) {
4971
+ const result = /* @__PURE__ */ new Map();
4972
+ const now = Date.now();
4923
4973
  try {
4924
4974
  const ids = uncachedNames.join(",");
4925
4975
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -4935,6 +4985,9 @@ var CoinGeckoPriceProvider = class {
4935
4985
  signal: AbortSignal.timeout(this.timeout)
4936
4986
  });
4937
4987
  if (!response.ok) {
4988
+ if (response.status === 429) {
4989
+ this.extendCacheOnRateLimit(uncachedNames);
4990
+ }
4938
4991
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
4939
4992
  }
4940
4993
  const data = await response.json();
@@ -4953,25 +5006,113 @@ var CoinGeckoPriceProvider = class {
4953
5006
  }
4954
5007
  for (const name of uncachedNames) {
4955
5008
  if (!result.has(name)) {
4956
- this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
5009
+ const zeroPrice = {
5010
+ tokenName: name,
5011
+ priceUsd: 0,
5012
+ priceEur: 0,
5013
+ change24h: 0,
5014
+ timestamp: now
5015
+ };
5016
+ this.cache.set(name, { price: zeroPrice, expiresAt: now + this.cacheTtlMs });
5017
+ result.set(name, zeroPrice);
4957
5018
  }
4958
5019
  }
4959
5020
  if (this.debug) {
4960
5021
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
4961
5022
  }
5023
+ this.saveToStorage();
4962
5024
  } catch (error) {
4963
5025
  if (this.debug) {
4964
5026
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
4965
5027
  }
4966
5028
  for (const name of uncachedNames) {
4967
5029
  const stale = this.cache.get(name);
4968
- if (stale?.price) {
5030
+ if (stale) {
4969
5031
  result.set(name, stale.price);
4970
5032
  }
4971
5033
  }
4972
5034
  }
4973
5035
  return result;
4974
5036
  }
5037
+ // ===========================================================================
5038
+ // Persistent Storage
5039
+ // ===========================================================================
5040
+ /**
5041
+ * Load cached prices from StorageProvider into in-memory cache.
5042
+ * Only loads entries that are still within cacheTtlMs.
5043
+ */
5044
+ async loadFromStorage() {
5045
+ if (this.loadCachePromise) {
5046
+ return this.loadCachePromise;
5047
+ }
5048
+ this.loadCachePromise = this.doLoadFromStorage();
5049
+ try {
5050
+ await this.loadCachePromise;
5051
+ } finally {
5052
+ this.loadCachePromise = null;
5053
+ }
5054
+ }
5055
+ async doLoadFromStorage() {
5056
+ this.persistentCacheLoaded = true;
5057
+ if (!this.storage) return;
5058
+ try {
5059
+ const [cached, cachedTs] = await Promise.all([
5060
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE),
5061
+ this.storage.get(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS)
5062
+ ]);
5063
+ if (!cached || !cachedTs) return;
5064
+ const ts = parseInt(cachedTs, 10);
5065
+ if (isNaN(ts)) return;
5066
+ const age = Date.now() - ts;
5067
+ if (age > this.cacheTtlMs) return;
5068
+ const data = JSON.parse(cached);
5069
+ const expiresAt = ts + this.cacheTtlMs;
5070
+ for (const [name, price] of Object.entries(data)) {
5071
+ if (!this.cache.has(name)) {
5072
+ this.cache.set(name, { price, expiresAt });
5073
+ }
5074
+ }
5075
+ if (this.debug) {
5076
+ console.log(`[CoinGecko] Loaded ${Object.keys(data).length} prices from persistent cache`);
5077
+ }
5078
+ } catch {
5079
+ }
5080
+ }
5081
+ /**
5082
+ * Save current prices to StorageProvider (fire-and-forget).
5083
+ */
5084
+ saveToStorage() {
5085
+ if (!this.storage) return;
5086
+ const data = {};
5087
+ for (const [name, entry] of this.cache) {
5088
+ data[name] = entry.price;
5089
+ }
5090
+ Promise.all([
5091
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE, JSON.stringify(data)),
5092
+ this.storage.set(STORAGE_KEYS_GLOBAL.PRICE_CACHE_TS, String(Date.now()))
5093
+ ]).catch(() => {
5094
+ });
5095
+ }
5096
+ // ===========================================================================
5097
+ // Rate-limit handling
5098
+ // ===========================================================================
5099
+ /**
5100
+ * On 429 rate-limit, extend stale cache entries so subsequent calls
5101
+ * don't immediately retry and hammer the API.
5102
+ */
5103
+ extendCacheOnRateLimit(names) {
5104
+ const backoffMs = 6e4;
5105
+ const extendedExpiry = Date.now() + backoffMs;
5106
+ for (const name of names) {
5107
+ const existing = this.cache.get(name);
5108
+ if (existing) {
5109
+ existing.expiresAt = Math.max(existing.expiresAt, extendedExpiry);
5110
+ }
5111
+ }
5112
+ if (this.debug) {
5113
+ console.warn(`[CoinGecko] Rate-limited (429), extended cache TTL by ${backoffMs / 1e3}s`);
5114
+ }
5115
+ }
4975
5116
  async getPrice(tokenName) {
4976
5117
  const prices = await this.getPrices([tokenName]);
4977
5118
  return prices.get(tokenName) ?? null;
@@ -5005,6 +5146,7 @@ var TokenRegistry = class _TokenRegistry {
5005
5146
  refreshTimer = null;
5006
5147
  lastRefreshAt = 0;
5007
5148
  refreshPromise = null;
5149
+ initialLoadPromise = null;
5008
5150
  constructor() {
5009
5151
  this.definitionsById = /* @__PURE__ */ new Map();
5010
5152
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -5043,13 +5185,8 @@ var TokenRegistry = class _TokenRegistry {
5043
5185
  if (options.refreshIntervalMs !== void 0) {
5044
5186
  instance.refreshIntervalMs = options.refreshIntervalMs;
5045
5187
  }
5046
- if (instance.storage) {
5047
- instance.loadFromCache();
5048
- }
5049
5188
  const autoRefresh = options.autoRefresh ?? true;
5050
- if (autoRefresh && instance.remoteUrl) {
5051
- instance.startAutoRefresh();
5052
- }
5189
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
5053
5190
  }
5054
5191
  /**
5055
5192
  * Reset the singleton instance (useful for testing).
@@ -5067,6 +5204,53 @@ var TokenRegistry = class _TokenRegistry {
5067
5204
  static destroy() {
5068
5205
  _TokenRegistry.resetInstance();
5069
5206
  }
5207
+ /**
5208
+ * Wait for the initial data load (cache or remote) to complete.
5209
+ * Returns true if data was loaded, false if not (timeout or no data source).
5210
+ *
5211
+ * @param timeoutMs - Maximum wait time in ms (default: 10s). Set to 0 for no timeout.
5212
+ */
5213
+ static async waitForReady(timeoutMs = 1e4) {
5214
+ const instance = _TokenRegistry.getInstance();
5215
+ if (!instance.initialLoadPromise) {
5216
+ return instance.definitionsById.size > 0;
5217
+ }
5218
+ if (timeoutMs <= 0) {
5219
+ return instance.initialLoadPromise;
5220
+ }
5221
+ return Promise.race([
5222
+ instance.initialLoadPromise,
5223
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs))
5224
+ ]);
5225
+ }
5226
+ // ===========================================================================
5227
+ // Initial Load
5228
+ // ===========================================================================
5229
+ /**
5230
+ * Perform initial data load: try cache first, fall back to remote fetch.
5231
+ * After initial data is available, start periodic auto-refresh if configured.
5232
+ */
5233
+ async performInitialLoad(autoRefresh) {
5234
+ let loaded = false;
5235
+ if (this.storage) {
5236
+ loaded = await this.loadFromCache();
5237
+ }
5238
+ if (loaded) {
5239
+ if (autoRefresh && this.remoteUrl) {
5240
+ this.startAutoRefresh();
5241
+ }
5242
+ return true;
5243
+ }
5244
+ if (autoRefresh && this.remoteUrl) {
5245
+ loaded = await this.refreshFromRemote();
5246
+ this.stopAutoRefresh();
5247
+ this.refreshTimer = setInterval(() => {
5248
+ this.refreshFromRemote();
5249
+ }, this.refreshIntervalMs);
5250
+ return loaded;
5251
+ }
5252
+ return false;
5253
+ }
5070
5254
  // ===========================================================================
5071
5255
  // Cache (StorageProvider)
5072
5256
  // ===========================================================================
@@ -5396,7 +5580,7 @@ function resolveL1Config(network, config) {
5396
5580
  enableVesting: config.enableVesting
5397
5581
  };
5398
5582
  }
5399
- function resolvePriceConfig(config) {
5583
+ function resolvePriceConfig(config, storage) {
5400
5584
  if (config === void 0) {
5401
5585
  return void 0;
5402
5586
  }
@@ -5406,7 +5590,8 @@ function resolvePriceConfig(config) {
5406
5590
  baseUrl: config.baseUrl,
5407
5591
  cacheTtlMs: config.cacheTtlMs,
5408
5592
  timeout: config.timeout,
5409
- debug: config.debug
5593
+ debug: config.debug,
5594
+ storage
5410
5595
  };
5411
5596
  }
5412
5597
  function resolveGroupChatConfig(network, config) {
@@ -5423,16 +5608,6 @@ function resolveGroupChatConfig(network, config) {
5423
5608
  relays: config.relays ?? [...netConfig.groupRelays]
5424
5609
  };
5425
5610
  }
5426
- function resolveMarketConfig(config) {
5427
- if (!config) return void 0;
5428
- if (config === true) {
5429
- return { apiUrl: DEFAULT_MARKET_API_URL };
5430
- }
5431
- return {
5432
- apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5433
- timeout: config.timeout
5434
- };
5435
- }
5436
5611
 
5437
5612
  // impl/nodejs/index.ts
5438
5613
  function createNodeProviders(config) {
@@ -5440,21 +5615,19 @@ function createNodeProviders(config) {
5440
5615
  const transportConfig = resolveTransportConfig(network, config?.transport);
5441
5616
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5442
5617
  const l1Config = resolveL1Config(network, config?.l1);
5443
- const priceConfig = resolvePriceConfig(config?.price);
5444
5618
  const storage = createFileStorageProvider({
5445
5619
  dataDir: config?.dataDir ?? "./sphere-data",
5446
5620
  ...config?.walletFileName ? { fileName: config.walletFileName } : {}
5447
5621
  });
5622
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5448
5623
  const ipfsSync = config?.tokenSync?.ipfs;
5449
5624
  const ipfsTokenStorage = ipfsSync?.enabled ? createNodeIpfsStorageProvider(ipfsSync.config, storage) : void 0;
5450
5625
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5451
5626
  const networkConfig = getNetworkConfig(network);
5452
5627
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5453
- const market = resolveMarketConfig(config?.market);
5454
5628
  return {
5455
5629
  storage,
5456
5630
  groupChat,
5457
- market,
5458
5631
  tokenStorage: createFileTokenStorageProvider({
5459
5632
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
5460
5633
  }),