@unicitylabs/sphere-sdk 0.3.7 → 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 {
@@ -591,61 +594,36 @@ var IndexedDBTokenStorageProvider = class {
591
594
  return meta !== null;
592
595
  }
593
596
  async clear() {
594
- const dbNames = [this.dbName];
597
+ if (this.db) {
598
+ this.db.close();
599
+ this.db = null;
600
+ }
601
+ this.status = "disconnected";
602
+ const CLEAR_TIMEOUT = 1500;
603
+ const withTimeout = (promise, ms, label) => Promise.race([
604
+ promise,
605
+ new Promise(
606
+ (_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
607
+ )
608
+ ]);
609
+ const deleteDb = (name) => new Promise((resolve) => {
610
+ const req = indexedDB.deleteDatabase(name);
611
+ req.onsuccess = () => resolve();
612
+ req.onerror = () => resolve();
613
+ req.onblocked = () => resolve();
614
+ });
595
615
  try {
596
- if (this.db) {
597
- await this.clearStore(STORE_TOKENS);
598
- await this.clearStore(STORE_META);
599
- this.db.close();
600
- this.db = null;
601
- }
602
- this.status = "disconnected";
603
616
  if (typeof indexedDB.databases === "function") {
604
- try {
605
- const dbs = await Promise.race([
606
- indexedDB.databases(),
607
- new Promise(
608
- (_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)
609
- )
610
- ]);
611
- for (const dbInfo of dbs) {
612
- if (dbInfo.name && dbInfo.name.startsWith(this.dbNamePrefix) && dbInfo.name !== this.dbName) {
613
- dbNames.push(dbInfo.name);
614
- try {
615
- const db = await new Promise((resolve, reject) => {
616
- const req = indexedDB.open(dbInfo.name, DB_VERSION);
617
- req.onsuccess = () => resolve(req.result);
618
- req.onerror = () => reject(req.error);
619
- req.onupgradeneeded = (e) => {
620
- const d = e.target.result;
621
- if (!d.objectStoreNames.contains(STORE_TOKENS)) d.createObjectStore(STORE_TOKENS, { keyPath: "id" });
622
- if (!d.objectStoreNames.contains(STORE_META)) d.createObjectStore(STORE_META);
623
- };
624
- });
625
- const clearTx = db.transaction([STORE_TOKENS, STORE_META], "readwrite");
626
- clearTx.objectStore(STORE_TOKENS).clear();
627
- clearTx.objectStore(STORE_META).clear();
628
- await new Promise((resolve) => {
629
- clearTx.oncomplete = () => resolve();
630
- clearTx.onerror = () => resolve();
631
- });
632
- db.close();
633
- } catch {
634
- }
635
- }
636
- }
637
- } catch {
638
- }
639
- }
640
- for (const name of dbNames) {
641
- try {
642
- const req = indexedDB.deleteDatabase(name);
643
- req.onerror = () => {
644
- };
645
- req.onblocked = () => {
646
- };
647
- } catch {
648
- }
617
+ const dbs = await withTimeout(
618
+ indexedDB.databases(),
619
+ CLEAR_TIMEOUT,
620
+ "indexedDB.databases()"
621
+ );
622
+ await Promise.all(
623
+ dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
624
+ );
625
+ } else {
626
+ await deleteDb(this.dbName);
649
627
  }
650
628
  return true;
651
629
  } catch (err) {
@@ -1403,8 +1381,6 @@ var NostrTransportProvider = class {
1403
1381
  transferHandlers = /* @__PURE__ */ new Set();
1404
1382
  paymentRequestHandlers = /* @__PURE__ */ new Set();
1405
1383
  paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1406
- readReceiptHandlers = /* @__PURE__ */ new Set();
1407
- typingIndicatorHandlers = /* @__PURE__ */ new Set();
1408
1384
  broadcastHandlers = /* @__PURE__ */ new Map();
1409
1385
  eventCallbacks = /* @__PURE__ */ new Set();
1410
1386
  constructor(config) {
@@ -1656,18 +1632,6 @@ var NostrTransportProvider = class {
1656
1632
  const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1657
1633
  const giftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, wrappedContent);
1658
1634
  await this.publishEvent(giftWrap);
1659
- const selfWrapContent = JSON.stringify({
1660
- selfWrap: true,
1661
- originalId: giftWrap.id,
1662
- recipientPubkey,
1663
- senderNametag,
1664
- text: content
1665
- });
1666
- const selfPubkey = this.keyManager.getPublicKeyHex();
1667
- const selfGiftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, selfPubkey, selfWrapContent);
1668
- this.publishEvent(selfGiftWrap).catch((err) => {
1669
- this.log("Self-wrap publish failed:", err);
1670
- });
1671
1635
  this.emitEvent({
1672
1636
  type: "message:sent",
1673
1637
  timestamp: Date.now(),
@@ -1766,37 +1730,6 @@ var NostrTransportProvider = class {
1766
1730
  this.paymentRequestResponseHandlers.add(handler);
1767
1731
  return () => this.paymentRequestResponseHandlers.delete(handler);
1768
1732
  }
1769
- // ===========================================================================
1770
- // Read Receipts
1771
- // ===========================================================================
1772
- async sendReadReceipt(recipientTransportPubkey, messageEventId) {
1773
- if (!this.keyManager) throw new Error("Not initialized");
1774
- const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1775
- const event = import_nostr_js_sdk.NIP17.createReadReceipt(this.keyManager, nostrRecipient, messageEventId);
1776
- await this.publishEvent(event);
1777
- this.log("Sent read receipt for:", messageEventId, "to:", nostrRecipient.slice(0, 16));
1778
- }
1779
- onReadReceipt(handler) {
1780
- this.readReceiptHandlers.add(handler);
1781
- return () => this.readReceiptHandlers.delete(handler);
1782
- }
1783
- // ===========================================================================
1784
- // Typing Indicators
1785
- // ===========================================================================
1786
- async sendTypingIndicator(recipientTransportPubkey) {
1787
- if (!this.keyManager) throw new Error("Not initialized");
1788
- const nostrRecipient = recipientTransportPubkey.length === 66 ? recipientTransportPubkey.slice(2) : recipientTransportPubkey;
1789
- const content = JSON.stringify({
1790
- type: "typing",
1791
- senderNametag: this.identity?.nametag
1792
- });
1793
- const event = import_nostr_js_sdk.NIP17.createGiftWrap(this.keyManager, nostrRecipient, content);
1794
- await this.publishEvent(event);
1795
- }
1796
- onTypingIndicator(handler) {
1797
- this.typingIndicatorHandlers.add(handler);
1798
- return () => this.typingIndicatorHandlers.delete(handler);
1799
- }
1800
1733
  /**
1801
1734
  * Resolve any identifier to full peer information.
1802
1735
  * Routes to the appropriate specific resolve method based on identifier format.
@@ -2250,74 +2183,11 @@ var NostrTransportProvider = class {
2250
2183
  const pm = import_nostr_js_sdk.NIP17.unwrap(event, this.keyManager);
2251
2184
  this.log("Gift wrap unwrapped, sender:", pm.senderPubkey?.slice(0, 16), "kind:", pm.kind);
2252
2185
  if (pm.senderPubkey === this.keyManager.getPublicKeyHex()) {
2253
- try {
2254
- const parsed = JSON.parse(pm.content);
2255
- if (parsed?.selfWrap && parsed.recipientPubkey) {
2256
- this.log("Self-wrap replay for recipient:", parsed.recipientPubkey?.slice(0, 16));
2257
- const message2 = {
2258
- id: parsed.originalId || pm.eventId,
2259
- senderTransportPubkey: pm.senderPubkey,
2260
- senderNametag: parsed.senderNametag,
2261
- recipientTransportPubkey: parsed.recipientPubkey,
2262
- content: parsed.text ?? "",
2263
- timestamp: pm.timestamp * 1e3,
2264
- encrypted: true,
2265
- isSelfWrap: true
2266
- };
2267
- for (const handler of this.messageHandlers) {
2268
- try {
2269
- handler(message2);
2270
- } catch (e) {
2271
- this.log("Self-wrap handler error:", e);
2272
- }
2273
- }
2274
- return;
2275
- }
2276
- } catch {
2277
- }
2278
- this.log("Skipping own non-self-wrap message");
2186
+ this.log("Skipping own message");
2279
2187
  return;
2280
2188
  }
2281
- if ((0, import_nostr_js_sdk.isReadReceipt)(pm)) {
2282
- this.log("Read receipt from:", pm.senderPubkey?.slice(0, 16), "for:", pm.replyToEventId);
2283
- if (pm.replyToEventId) {
2284
- const receipt = {
2285
- senderTransportPubkey: pm.senderPubkey,
2286
- messageEventId: pm.replyToEventId,
2287
- timestamp: pm.timestamp * 1e3
2288
- };
2289
- for (const handler of this.readReceiptHandlers) {
2290
- try {
2291
- handler(receipt);
2292
- } catch (e) {
2293
- this.log("Read receipt handler error:", e);
2294
- }
2295
- }
2296
- }
2297
- return;
2298
- }
2299
- try {
2300
- const parsed = JSON.parse(pm.content);
2301
- if (parsed?.type === "typing") {
2302
- this.log("Typing indicator from:", pm.senderPubkey?.slice(0, 16));
2303
- const indicator = {
2304
- senderTransportPubkey: pm.senderPubkey,
2305
- senderNametag: parsed.senderNametag,
2306
- timestamp: pm.timestamp * 1e3
2307
- };
2308
- for (const handler of this.typingIndicatorHandlers) {
2309
- try {
2310
- handler(indicator);
2311
- } catch (e) {
2312
- this.log("Typing handler error:", e);
2313
- }
2314
- }
2315
- return;
2316
- }
2317
- } catch {
2318
- }
2319
- if (!(0, import_nostr_js_sdk.isChatMessage)(pm)) {
2320
- this.log("Skipping unknown message kind:", pm.kind);
2189
+ if (pm.kind !== import_nostr_js_sdk.EventKinds.CHAT_MESSAGE) {
2190
+ this.log("Skipping non-chat message, kind:", pm.kind);
2321
2191
  return;
2322
2192
  }
2323
2193
  let content = pm.content;
@@ -2332,9 +2202,7 @@ var NostrTransportProvider = class {
2332
2202
  }
2333
2203
  this.log("DM received from:", senderNametag || pm.senderPubkey?.slice(0, 16), "content:", content?.slice(0, 50));
2334
2204
  const message = {
2335
- // Use outer gift wrap event.id so it matches the sender's stored giftWrap.id.
2336
- // This ensures read receipts reference an ID the sender recognizes.
2337
- id: event.id,
2205
+ id: pm.eventId,
2338
2206
  senderTransportPubkey: pm.senderPubkey,
2339
2207
  senderNametag,
2340
2208
  content,
@@ -3412,6 +3280,7 @@ async function loadIpnsModule() {
3412
3280
  async function createSignedRecord(keyPair, cid, sequenceNumber, lifetimeMs = DEFAULT_LIFETIME_MS) {
3413
3281
  const { createIPNSRecord, marshalIPNSRecord } = await loadIpnsModule();
3414
3282
  const record = await createIPNSRecord(
3283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3415
3284
  keyPair,
3416
3285
  `/ipfs/${cid}`,
3417
3286
  sequenceNumber,
@@ -5156,26 +5025,37 @@ var CoinGeckoPriceProvider = class {
5156
5025
  timeout;
5157
5026
  debug;
5158
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;
5159
5037
  constructor(config) {
5160
5038
  this.apiKey = config?.apiKey;
5161
5039
  this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
5162
5040
  this.timeout = config?.timeout ?? 1e4;
5163
5041
  this.debug = config?.debug ?? false;
5042
+ this.storage = config?.storage ?? null;
5164
5043
  this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
5165
5044
  }
5166
5045
  async getPrices(tokenNames) {
5167
5046
  if (tokenNames.length === 0) {
5168
5047
  return /* @__PURE__ */ new Map();
5169
5048
  }
5049
+ if (!this.persistentCacheLoaded && this.storage) {
5050
+ await this.loadFromStorage();
5051
+ }
5170
5052
  const now = Date.now();
5171
5053
  const result = /* @__PURE__ */ new Map();
5172
5054
  const uncachedNames = [];
5173
5055
  for (const name of tokenNames) {
5174
5056
  const cached = this.cache.get(name);
5175
5057
  if (cached && cached.expiresAt > now) {
5176
- if (cached.price !== null) {
5177
- result.set(name, cached.price);
5178
- }
5058
+ result.set(name, cached.price);
5179
5059
  } else {
5180
5060
  uncachedNames.push(name);
5181
5061
  }
@@ -5183,6 +5063,41 @@ var CoinGeckoPriceProvider = class {
5183
5063
  if (uncachedNames.length === 0) {
5184
5064
  return result;
5185
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();
5186
5101
  try {
5187
5102
  const ids = uncachedNames.join(",");
5188
5103
  const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
@@ -5198,6 +5113,9 @@ var CoinGeckoPriceProvider = class {
5198
5113
  signal: AbortSignal.timeout(this.timeout)
5199
5114
  });
5200
5115
  if (!response.ok) {
5116
+ if (response.status === 429) {
5117
+ this.extendCacheOnRateLimit(uncachedNames);
5118
+ }
5201
5119
  throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
5202
5120
  }
5203
5121
  const data = await response.json();
@@ -5216,25 +5134,113 @@ var CoinGeckoPriceProvider = class {
5216
5134
  }
5217
5135
  for (const name of uncachedNames) {
5218
5136
  if (!result.has(name)) {
5219
- 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);
5220
5146
  }
5221
5147
  }
5222
5148
  if (this.debug) {
5223
5149
  console.log(`[CoinGecko] Fetched ${result.size} prices`);
5224
5150
  }
5151
+ this.saveToStorage();
5225
5152
  } catch (error) {
5226
5153
  if (this.debug) {
5227
5154
  console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
5228
5155
  }
5229
5156
  for (const name of uncachedNames) {
5230
5157
  const stale = this.cache.get(name);
5231
- if (stale?.price) {
5158
+ if (stale) {
5232
5159
  result.set(name, stale.price);
5233
5160
  }
5234
5161
  }
5235
5162
  }
5236
5163
  return result;
5237
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
+ }
5238
5244
  async getPrice(tokenName) {
5239
5245
  const prices = await this.getPrices([tokenName]);
5240
5246
  return prices.get(tokenName) ?? null;
@@ -5268,6 +5274,7 @@ var TokenRegistry = class _TokenRegistry {
5268
5274
  refreshTimer = null;
5269
5275
  lastRefreshAt = 0;
5270
5276
  refreshPromise = null;
5277
+ initialLoadPromise = null;
5271
5278
  constructor() {
5272
5279
  this.definitionsById = /* @__PURE__ */ new Map();
5273
5280
  this.definitionsBySymbol = /* @__PURE__ */ new Map();
@@ -5306,13 +5313,8 @@ var TokenRegistry = class _TokenRegistry {
5306
5313
  if (options.refreshIntervalMs !== void 0) {
5307
5314
  instance.refreshIntervalMs = options.refreshIntervalMs;
5308
5315
  }
5309
- if (instance.storage) {
5310
- instance.loadFromCache();
5311
- }
5312
5316
  const autoRefresh = options.autoRefresh ?? true;
5313
- if (autoRefresh && instance.remoteUrl) {
5314
- instance.startAutoRefresh();
5315
- }
5317
+ instance.initialLoadPromise = instance.performInitialLoad(autoRefresh);
5316
5318
  }
5317
5319
  /**
5318
5320
  * Reset the singleton instance (useful for testing).
@@ -5330,6 +5332,53 @@ var TokenRegistry = class _TokenRegistry {
5330
5332
  static destroy() {
5331
5333
  _TokenRegistry.resetInstance();
5332
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
+ }
5333
5382
  // ===========================================================================
5334
5383
  // Cache (StorageProvider)
5335
5384
  // ===========================================================================
@@ -5659,7 +5708,7 @@ function resolveL1Config(network, config) {
5659
5708
  enableVesting: config.enableVesting
5660
5709
  };
5661
5710
  }
5662
- function resolvePriceConfig(config) {
5711
+ function resolvePriceConfig(config, storage) {
5663
5712
  if (config === void 0) {
5664
5713
  return void 0;
5665
5714
  }
@@ -5669,7 +5718,8 @@ function resolvePriceConfig(config) {
5669
5718
  baseUrl: config.baseUrl,
5670
5719
  cacheTtlMs: config.cacheTtlMs,
5671
5720
  timeout: config.timeout,
5672
- debug: config.debug
5721
+ debug: config.debug,
5722
+ storage
5673
5723
  };
5674
5724
  }
5675
5725
  function resolveArrayConfig(defaults, replace, additional) {
@@ -5696,16 +5746,6 @@ function resolveGroupChatConfig(network, config) {
5696
5746
  relays: config.relays ?? [...netConfig.groupRelays]
5697
5747
  };
5698
5748
  }
5699
- function resolveMarketConfig(config) {
5700
- if (!config) return void 0;
5701
- if (config === true) {
5702
- return { apiUrl: DEFAULT_MARKET_API_URL };
5703
- }
5704
- return {
5705
- apiUrl: config.apiUrl ?? DEFAULT_MARKET_API_URL,
5706
- timeout: config.timeout
5707
- };
5708
- }
5709
5749
 
5710
5750
  // impl/browser/index.ts
5711
5751
  if (typeof globalThis.Buffer === "undefined") {
@@ -5763,8 +5803,8 @@ function createBrowserProviders(config) {
5763
5803
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
5764
5804
  const l1Config = resolveL1Config(network, config?.l1);
5765
5805
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
5766
- const priceConfig = resolvePriceConfig(config?.price);
5767
5806
  const storage = createLocalStorageProvider(config?.storage);
5807
+ const priceConfig = resolvePriceConfig(config?.price, storage);
5768
5808
  const ipfsConfig = tokenSyncConfig?.ipfs;
5769
5809
  const ipfsTokenStorage = ipfsConfig?.enabled ? createBrowserIpfsStorageProvider({
5770
5810
  gateways: ipfsConfig.gateways,
@@ -5774,11 +5814,9 @@ function createBrowserProviders(config) {
5774
5814
  const groupChat = resolveGroupChatConfig(network, config?.groupChat);
5775
5815
  const networkConfig = getNetworkConfig(network);
5776
5816
  TokenRegistry.configure({ remoteUrl: networkConfig.tokenRegistryUrl, storage });
5777
- const market = resolveMarketConfig(config?.market);
5778
5817
  return {
5779
5818
  storage,
5780
5819
  groupChat,
5781
- market,
5782
5820
  transport: createNostrTransportProvider({
5783
5821
  relays: transportConfig.relays,
5784
5822
  timeout: transportConfig.timeout,