@unicitylabs/sphere-sdk 0.2.2 → 0.2.3

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.
@@ -82,7 +82,9 @@ var STORAGE_KEYS_GLOBAL = {
82
82
  /** Nametag cache per address (separate from tracked addresses registry) */
83
83
  ADDRESS_NAMETAGS: "address_nametags",
84
84
  /** Active addresses registry (JSON: TrackedAddressesStorage) */
85
- TRACKED_ADDRESSES: "tracked_addresses"
85
+ TRACKED_ADDRESSES: "tracked_addresses",
86
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
87
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
86
88
  };
87
89
  var STORAGE_KEYS_ADDRESS = {
88
90
  /** Pending transfers for this address */
@@ -555,6 +557,13 @@ var IndexedDBTokenStorageProvider = class {
555
557
  this.db = null;
556
558
  }
557
559
  this.status = "disconnected";
560
+ const CLEAR_TIMEOUT = 1500;
561
+ const withTimeout = (promise, ms, label) => Promise.race([
562
+ promise,
563
+ new Promise(
564
+ (_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
565
+ )
566
+ ]);
558
567
  const deleteDb = (name) => new Promise((resolve) => {
559
568
  const req = indexedDB.deleteDatabase(name);
560
569
  req.onsuccess = () => resolve();
@@ -563,7 +572,11 @@ var IndexedDBTokenStorageProvider = class {
563
572
  });
564
573
  try {
565
574
  if (typeof indexedDB.databases === "function") {
566
- const dbs = await indexedDB.databases();
575
+ const dbs = await withTimeout(
576
+ indexedDB.databases(),
577
+ CLEAR_TIMEOUT,
578
+ "indexedDB.databases()"
579
+ );
567
580
  await Promise.all(
568
581
  dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
569
582
  );
@@ -571,7 +584,8 @@ var IndexedDBTokenStorageProvider = class {
571
584
  await deleteDb(this.dbName);
572
585
  }
573
586
  return true;
574
- } catch {
587
+ } catch (err) {
588
+ console.warn("[IndexedDBTokenStorage] clear() failed:", err);
575
589
  return false;
576
590
  }
577
591
  }
@@ -1325,6 +1339,9 @@ var NostrTransportProvider = class {
1325
1339
  type = "p2p";
1326
1340
  description = "P2P messaging via Nostr protocol";
1327
1341
  config;
1342
+ storage = null;
1343
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1344
+ lastEventTs = 0;
1328
1345
  identity = null;
1329
1346
  keyManager = null;
1330
1347
  status = "disconnected";
@@ -1350,6 +1367,7 @@ var NostrTransportProvider = class {
1350
1367
  createWebSocket: config.createWebSocket,
1351
1368
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
1352
1369
  };
1370
+ this.storage = config.storage ?? null;
1353
1371
  }
1354
1372
  // ===========================================================================
1355
1373
  // BaseProvider Implementation
@@ -1388,7 +1406,14 @@ var NostrTransportProvider = class {
1388
1406
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1389
1407
  }
1390
1408
  });
1391
- await this.nostrClient.connect(...this.config.relays);
1409
+ await Promise.race([
1410
+ this.nostrClient.connect(...this.config.relays),
1411
+ new Promise(
1412
+ (_, reject) => setTimeout(() => reject(new Error(
1413
+ `Transport connection timed out after ${this.config.timeout}ms`
1414
+ )), this.config.timeout)
1415
+ )
1416
+ ]);
1392
1417
  if (!this.nostrClient.isConnected()) {
1393
1418
  throw new Error("Failed to connect to any relay");
1394
1419
  }
@@ -1396,7 +1421,7 @@ var NostrTransportProvider = class {
1396
1421
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1397
1422
  this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
1398
1423
  if (this.identity) {
1399
- this.subscribeToEvents();
1424
+ await this.subscribeToEvents();
1400
1425
  }
1401
1426
  } catch (error) {
1402
1427
  this.status = "error";
@@ -1549,11 +1574,18 @@ var NostrTransportProvider = class {
1549
1574
  this.log("NostrClient reconnected to relay:", url);
1550
1575
  }
1551
1576
  });
1552
- await this.nostrClient.connect(...this.config.relays);
1553
- this.subscribeToEvents();
1577
+ await Promise.race([
1578
+ this.nostrClient.connect(...this.config.relays),
1579
+ new Promise(
1580
+ (_, reject) => setTimeout(() => reject(new Error(
1581
+ `Transport reconnection timed out after ${this.config.timeout}ms`
1582
+ )), this.config.timeout)
1583
+ )
1584
+ ]);
1585
+ await this.subscribeToEvents();
1554
1586
  oldClient.disconnect();
1555
1587
  } else if (this.isConnected()) {
1556
- this.subscribeToEvents();
1588
+ await this.subscribeToEvents();
1557
1589
  }
1558
1590
  }
1559
1591
  /**
@@ -2081,10 +2113,31 @@ var NostrTransportProvider = class {
2081
2113
  this.handleBroadcast(event);
2082
2114
  break;
2083
2115
  }
2116
+ if (event.created_at && this.storage && this.keyManager) {
2117
+ const kind = event.kind;
2118
+ if (kind === EVENT_KINDS.DIRECT_MESSAGE || kind === EVENT_KINDS.TOKEN_TRANSFER || kind === EVENT_KINDS.PAYMENT_REQUEST || kind === EVENT_KINDS.PAYMENT_REQUEST_RESPONSE) {
2119
+ this.updateLastEventTimestamp(event.created_at);
2120
+ }
2121
+ }
2084
2122
  } catch (error) {
2085
2123
  this.log("Failed to handle event:", error);
2086
2124
  }
2087
2125
  }
2126
+ /**
2127
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
2128
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
2129
+ * when multiple events arrive in quick succession.
2130
+ */
2131
+ updateLastEventTimestamp(createdAt) {
2132
+ if (!this.storage || !this.keyManager) return;
2133
+ if (createdAt <= this.lastEventTs) return;
2134
+ this.lastEventTs = createdAt;
2135
+ const pubkey = this.keyManager.getPublicKeyHex();
2136
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${pubkey.slice(0, 16)}`;
2137
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
2138
+ this.log("Failed to save last event timestamp:", err);
2139
+ });
2140
+ }
2088
2141
  async handleDirectMessage(event) {
2089
2142
  this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
2090
2143
  }
@@ -2163,6 +2216,7 @@ var NostrTransportProvider = class {
2163
2216
  const request = {
2164
2217
  id: event.id,
2165
2218
  senderTransportPubkey: event.pubkey,
2219
+ senderNametag: requestData.recipientNametag,
2166
2220
  request: {
2167
2221
  requestId: requestData.requestId,
2168
2222
  amount: requestData.amount,
@@ -2317,7 +2371,7 @@ var NostrTransportProvider = class {
2317
2371
  // Track subscription IDs for cleanup
2318
2372
  walletSubscriptionId = null;
2319
2373
  chatSubscriptionId = null;
2320
- subscribeToEvents() {
2374
+ async subscribeToEvents() {
2321
2375
  this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2322
2376
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2323
2377
  this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
@@ -2337,6 +2391,27 @@ var NostrTransportProvider = class {
2337
2391
  }
2338
2392
  const nostrPubkey = this.keyManager.getPublicKeyHex();
2339
2393
  this.log("Subscribing with Nostr pubkey:", nostrPubkey);
2394
+ let since;
2395
+ if (this.storage) {
2396
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${nostrPubkey.slice(0, 16)}`;
2397
+ try {
2398
+ const stored = await this.storage.get(storageKey);
2399
+ if (stored) {
2400
+ since = parseInt(stored, 10);
2401
+ this.lastEventTs = since;
2402
+ this.log("Resuming from stored event timestamp:", since);
2403
+ } else {
2404
+ since = Math.floor(Date.now() / 1e3);
2405
+ this.log("No stored timestamp, starting from now:", since);
2406
+ }
2407
+ } catch (err) {
2408
+ this.log("Failed to read last event timestamp, falling back to now:", err);
2409
+ since = Math.floor(Date.now() / 1e3);
2410
+ }
2411
+ } else {
2412
+ since = Math.floor(Date.now() / 1e3) - 86400;
2413
+ this.log("No storage adapter, using 24h fallback");
2414
+ }
2340
2415
  const walletFilter = new import_nostr_js_sdk.Filter();
2341
2416
  walletFilter.kinds = [
2342
2417
  EVENT_KINDS.DIRECT_MESSAGE,
@@ -2345,7 +2420,7 @@ var NostrTransportProvider = class {
2345
2420
  EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
2346
2421
  ];
2347
2422
  walletFilter["#p"] = [nostrPubkey];
2348
- walletFilter.since = Math.floor(Date.now() / 1e3) - 86400;
2423
+ walletFilter.since = since;
2349
2424
  this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
2350
2425
  onEvent: (event) => {
2351
2426
  this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
@@ -3255,15 +3330,17 @@ function createBrowserProviders(config) {
3255
3330
  const l1Config = resolveL1Config(network, config?.l1);
3256
3331
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
3257
3332
  const priceConfig = resolvePriceConfig(config?.price);
3333
+ const storage = createLocalStorageProvider(config?.storage);
3258
3334
  return {
3259
- storage: createLocalStorageProvider(config?.storage),
3335
+ storage,
3260
3336
  transport: createNostrTransportProvider({
3261
3337
  relays: transportConfig.relays,
3262
3338
  timeout: transportConfig.timeout,
3263
3339
  autoReconnect: transportConfig.autoReconnect,
3264
3340
  reconnectDelay: transportConfig.reconnectDelay,
3265
3341
  maxReconnectAttempts: transportConfig.maxReconnectAttempts,
3266
- debug: transportConfig.debug
3342
+ debug: transportConfig.debug,
3343
+ storage
3267
3344
  }),
3268
3345
  oracle: createUnicityAggregatorProvider({
3269
3346
  url: oracleConfig.url,