@unicitylabs/sphere-sdk 0.2.1 → 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 */
@@ -550,12 +552,40 @@ var IndexedDBTokenStorageProvider = class {
550
552
  return meta !== null;
551
553
  }
552
554
  async clear() {
553
- if (!this.db) return false;
555
+ if (this.db) {
556
+ this.db.close();
557
+ this.db = null;
558
+ }
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
+ ]);
567
+ const deleteDb = (name) => new Promise((resolve) => {
568
+ const req = indexedDB.deleteDatabase(name);
569
+ req.onsuccess = () => resolve();
570
+ req.onerror = () => resolve();
571
+ req.onblocked = () => resolve();
572
+ });
554
573
  try {
555
- await this.clearStore(STORE_TOKENS);
556
- await this.clearStore(STORE_META);
574
+ if (typeof indexedDB.databases === "function") {
575
+ const dbs = await withTimeout(
576
+ indexedDB.databases(),
577
+ CLEAR_TIMEOUT,
578
+ "indexedDB.databases()"
579
+ );
580
+ await Promise.all(
581
+ dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
582
+ );
583
+ } else {
584
+ await deleteDb(this.dbName);
585
+ }
557
586
  return true;
558
- } catch {
587
+ } catch (err) {
588
+ console.warn("[IndexedDBTokenStorage] clear() failed:", err);
559
589
  return false;
560
590
  }
561
591
  }
@@ -1309,6 +1339,9 @@ var NostrTransportProvider = class {
1309
1339
  type = "p2p";
1310
1340
  description = "P2P messaging via Nostr protocol";
1311
1341
  config;
1342
+ storage = null;
1343
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1344
+ lastEventTs = 0;
1312
1345
  identity = null;
1313
1346
  keyManager = null;
1314
1347
  status = "disconnected";
@@ -1334,6 +1367,7 @@ var NostrTransportProvider = class {
1334
1367
  createWebSocket: config.createWebSocket,
1335
1368
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
1336
1369
  };
1370
+ this.storage = config.storage ?? null;
1337
1371
  }
1338
1372
  // ===========================================================================
1339
1373
  // BaseProvider Implementation
@@ -1372,7 +1406,14 @@ var NostrTransportProvider = class {
1372
1406
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1373
1407
  }
1374
1408
  });
1375
- 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
+ ]);
1376
1417
  if (!this.nostrClient.isConnected()) {
1377
1418
  throw new Error("Failed to connect to any relay");
1378
1419
  }
@@ -1380,7 +1421,7 @@ var NostrTransportProvider = class {
1380
1421
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1381
1422
  this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
1382
1423
  if (this.identity) {
1383
- this.subscribeToEvents();
1424
+ await this.subscribeToEvents();
1384
1425
  }
1385
1426
  } catch (error) {
1386
1427
  this.status = "error";
@@ -1533,11 +1574,18 @@ var NostrTransportProvider = class {
1533
1574
  this.log("NostrClient reconnected to relay:", url);
1534
1575
  }
1535
1576
  });
1536
- await this.nostrClient.connect(...this.config.relays);
1537
- 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();
1538
1586
  oldClient.disconnect();
1539
1587
  } else if (this.isConnected()) {
1540
- this.subscribeToEvents();
1588
+ await this.subscribeToEvents();
1541
1589
  }
1542
1590
  }
1543
1591
  /**
@@ -1678,7 +1726,7 @@ var NostrTransportProvider = class {
1678
1726
  return this.resolveNametagInfo(identifier);
1679
1727
  }
1680
1728
  async resolveNametag(nametag) {
1681
- this.ensureReady();
1729
+ this.ensureConnected();
1682
1730
  const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
1683
1731
  let events = await this.queryEvents({
1684
1732
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1702,7 +1750,7 @@ var NostrTransportProvider = class {
1702
1750
  return null;
1703
1751
  }
1704
1752
  async resolveNametagInfo(nametag) {
1705
- this.ensureReady();
1753
+ this.ensureConnected();
1706
1754
  const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
1707
1755
  let events = await this.queryEvents({
1708
1756
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1779,7 +1827,7 @@ var NostrTransportProvider = class {
1779
1827
  * Works with both new identity binding events and legacy nametag binding events.
1780
1828
  */
1781
1829
  async resolveAddressInfo(address) {
1782
- this.ensureReady();
1830
+ this.ensureConnected();
1783
1831
  const addressHash = hashAddressForTag(address);
1784
1832
  const events = await this.queryEvents({
1785
1833
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1814,7 +1862,7 @@ var NostrTransportProvider = class {
1814
1862
  * Queries binding events authored by the given pubkey.
1815
1863
  */
1816
1864
  async resolveTransportPubkeyInfo(transportPubkey) {
1817
- this.ensureReady();
1865
+ this.ensureConnected();
1818
1866
  const events = await this.queryEvents({
1819
1867
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
1820
1868
  authors: [transportPubkey],
@@ -2065,10 +2113,31 @@ var NostrTransportProvider = class {
2065
2113
  this.handleBroadcast(event);
2066
2114
  break;
2067
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
+ }
2068
2122
  } catch (error) {
2069
2123
  this.log("Failed to handle event:", error);
2070
2124
  }
2071
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
+ }
2072
2141
  async handleDirectMessage(event) {
2073
2142
  this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
2074
2143
  }
@@ -2147,6 +2216,7 @@ var NostrTransportProvider = class {
2147
2216
  const request = {
2148
2217
  id: event.id,
2149
2218
  senderTransportPubkey: event.pubkey,
2219
+ senderNametag: requestData.recipientNametag,
2150
2220
  request: {
2151
2221
  requestId: requestData.requestId,
2152
2222
  amount: requestData.amount,
@@ -2301,7 +2371,7 @@ var NostrTransportProvider = class {
2301
2371
  // Track subscription IDs for cleanup
2302
2372
  walletSubscriptionId = null;
2303
2373
  chatSubscriptionId = null;
2304
- subscribeToEvents() {
2374
+ async subscribeToEvents() {
2305
2375
  this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2306
2376
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2307
2377
  this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
@@ -2321,6 +2391,27 @@ var NostrTransportProvider = class {
2321
2391
  }
2322
2392
  const nostrPubkey = this.keyManager.getPublicKeyHex();
2323
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
+ }
2324
2415
  const walletFilter = new import_nostr_js_sdk.Filter();
2325
2416
  walletFilter.kinds = [
2326
2417
  EVENT_KINDS.DIRECT_MESSAGE,
@@ -2329,7 +2420,7 @@ var NostrTransportProvider = class {
2329
2420
  EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
2330
2421
  ];
2331
2422
  walletFilter["#p"] = [nostrPubkey];
2332
- walletFilter.since = Math.floor(Date.now() / 1e3) - 86400;
2423
+ walletFilter.since = since;
2333
2424
  this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
2334
2425
  onEvent: (event) => {
2335
2426
  this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
@@ -2432,10 +2523,13 @@ var NostrTransportProvider = class {
2432
2523
  // ===========================================================================
2433
2524
  // Private: Helpers
2434
2525
  // ===========================================================================
2435
- ensureReady() {
2526
+ ensureConnected() {
2436
2527
  if (!this.isConnected()) {
2437
2528
  throw new Error("NostrTransportProvider not connected");
2438
2529
  }
2530
+ }
2531
+ ensureReady() {
2532
+ this.ensureConnected();
2439
2533
  if (!this.identity) {
2440
2534
  throw new Error("Identity not set");
2441
2535
  }
@@ -3236,15 +3330,17 @@ function createBrowserProviders(config) {
3236
3330
  const l1Config = resolveL1Config(network, config?.l1);
3237
3331
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
3238
3332
  const priceConfig = resolvePriceConfig(config?.price);
3333
+ const storage = createLocalStorageProvider(config?.storage);
3239
3334
  return {
3240
- storage: createLocalStorageProvider(config?.storage),
3335
+ storage,
3241
3336
  transport: createNostrTransportProvider({
3242
3337
  relays: transportConfig.relays,
3243
3338
  timeout: transportConfig.timeout,
3244
3339
  autoReconnect: transportConfig.autoReconnect,
3245
3340
  reconnectDelay: transportConfig.reconnectDelay,
3246
3341
  maxReconnectAttempts: transportConfig.maxReconnectAttempts,
3247
- debug: transportConfig.debug
3342
+ debug: transportConfig.debug,
3343
+ storage
3248
3344
  }),
3249
3345
  oracle: createUnicityAggregatorProvider({
3250
3346
  url: oracleConfig.url,