@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.
@@ -24,7 +24,9 @@ var STORAGE_KEYS_GLOBAL = {
24
24
  /** Nametag cache per address (separate from tracked addresses registry) */
25
25
  ADDRESS_NAMETAGS: "address_nametags",
26
26
  /** Active addresses registry (JSON: TrackedAddressesStorage) */
27
- TRACKED_ADDRESSES: "tracked_addresses"
27
+ TRACKED_ADDRESSES: "tracked_addresses",
28
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
29
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
28
30
  };
29
31
  var STORAGE_KEYS_ADDRESS = {
30
32
  /** Pending transfers for this address */
@@ -497,6 +499,13 @@ var IndexedDBTokenStorageProvider = class {
497
499
  this.db = null;
498
500
  }
499
501
  this.status = "disconnected";
502
+ const CLEAR_TIMEOUT = 1500;
503
+ const withTimeout = (promise, ms, label) => Promise.race([
504
+ promise,
505
+ new Promise(
506
+ (_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
507
+ )
508
+ ]);
500
509
  const deleteDb = (name) => new Promise((resolve) => {
501
510
  const req = indexedDB.deleteDatabase(name);
502
511
  req.onsuccess = () => resolve();
@@ -505,7 +514,11 @@ var IndexedDBTokenStorageProvider = class {
505
514
  });
506
515
  try {
507
516
  if (typeof indexedDB.databases === "function") {
508
- const dbs = await indexedDB.databases();
517
+ const dbs = await withTimeout(
518
+ indexedDB.databases(),
519
+ CLEAR_TIMEOUT,
520
+ "indexedDB.databases()"
521
+ );
509
522
  await Promise.all(
510
523
  dbs.filter((db) => db.name?.startsWith(this.dbNamePrefix)).map((db) => deleteDb(db.name))
511
524
  );
@@ -513,7 +526,8 @@ var IndexedDBTokenStorageProvider = class {
513
526
  await deleteDb(this.dbName);
514
527
  }
515
528
  return true;
516
- } catch {
529
+ } catch (err) {
530
+ console.warn("[IndexedDBTokenStorage] clear() failed:", err);
517
531
  return false;
518
532
  }
519
533
  }
@@ -1276,6 +1290,9 @@ var NostrTransportProvider = class {
1276
1290
  type = "p2p";
1277
1291
  description = "P2P messaging via Nostr protocol";
1278
1292
  config;
1293
+ storage = null;
1294
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1295
+ lastEventTs = 0;
1279
1296
  identity = null;
1280
1297
  keyManager = null;
1281
1298
  status = "disconnected";
@@ -1301,6 +1318,7 @@ var NostrTransportProvider = class {
1301
1318
  createWebSocket: config.createWebSocket,
1302
1319
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
1303
1320
  };
1321
+ this.storage = config.storage ?? null;
1304
1322
  }
1305
1323
  // ===========================================================================
1306
1324
  // BaseProvider Implementation
@@ -1339,7 +1357,14 @@ var NostrTransportProvider = class {
1339
1357
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1340
1358
  }
1341
1359
  });
1342
- await this.nostrClient.connect(...this.config.relays);
1360
+ await Promise.race([
1361
+ this.nostrClient.connect(...this.config.relays),
1362
+ new Promise(
1363
+ (_, reject) => setTimeout(() => reject(new Error(
1364
+ `Transport connection timed out after ${this.config.timeout}ms`
1365
+ )), this.config.timeout)
1366
+ )
1367
+ ]);
1343
1368
  if (!this.nostrClient.isConnected()) {
1344
1369
  throw new Error("Failed to connect to any relay");
1345
1370
  }
@@ -1347,7 +1372,7 @@ var NostrTransportProvider = class {
1347
1372
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1348
1373
  this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
1349
1374
  if (this.identity) {
1350
- this.subscribeToEvents();
1375
+ await this.subscribeToEvents();
1351
1376
  }
1352
1377
  } catch (error) {
1353
1378
  this.status = "error";
@@ -1500,11 +1525,18 @@ var NostrTransportProvider = class {
1500
1525
  this.log("NostrClient reconnected to relay:", url);
1501
1526
  }
1502
1527
  });
1503
- await this.nostrClient.connect(...this.config.relays);
1504
- this.subscribeToEvents();
1528
+ await Promise.race([
1529
+ this.nostrClient.connect(...this.config.relays),
1530
+ new Promise(
1531
+ (_, reject) => setTimeout(() => reject(new Error(
1532
+ `Transport reconnection timed out after ${this.config.timeout}ms`
1533
+ )), this.config.timeout)
1534
+ )
1535
+ ]);
1536
+ await this.subscribeToEvents();
1505
1537
  oldClient.disconnect();
1506
1538
  } else if (this.isConnected()) {
1507
- this.subscribeToEvents();
1539
+ await this.subscribeToEvents();
1508
1540
  }
1509
1541
  }
1510
1542
  /**
@@ -2032,10 +2064,31 @@ var NostrTransportProvider = class {
2032
2064
  this.handleBroadcast(event);
2033
2065
  break;
2034
2066
  }
2067
+ if (event.created_at && this.storage && this.keyManager) {
2068
+ const kind = event.kind;
2069
+ if (kind === EVENT_KINDS.DIRECT_MESSAGE || kind === EVENT_KINDS.TOKEN_TRANSFER || kind === EVENT_KINDS.PAYMENT_REQUEST || kind === EVENT_KINDS.PAYMENT_REQUEST_RESPONSE) {
2070
+ this.updateLastEventTimestamp(event.created_at);
2071
+ }
2072
+ }
2035
2073
  } catch (error) {
2036
2074
  this.log("Failed to handle event:", error);
2037
2075
  }
2038
2076
  }
2077
+ /**
2078
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
2079
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
2080
+ * when multiple events arrive in quick succession.
2081
+ */
2082
+ updateLastEventTimestamp(createdAt) {
2083
+ if (!this.storage || !this.keyManager) return;
2084
+ if (createdAt <= this.lastEventTs) return;
2085
+ this.lastEventTs = createdAt;
2086
+ const pubkey = this.keyManager.getPublicKeyHex();
2087
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${pubkey.slice(0, 16)}`;
2088
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
2089
+ this.log("Failed to save last event timestamp:", err);
2090
+ });
2091
+ }
2039
2092
  async handleDirectMessage(event) {
2040
2093
  this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
2041
2094
  }
@@ -2114,6 +2167,7 @@ var NostrTransportProvider = class {
2114
2167
  const request = {
2115
2168
  id: event.id,
2116
2169
  senderTransportPubkey: event.pubkey,
2170
+ senderNametag: requestData.recipientNametag,
2117
2171
  request: {
2118
2172
  requestId: requestData.requestId,
2119
2173
  amount: requestData.amount,
@@ -2268,7 +2322,7 @@ var NostrTransportProvider = class {
2268
2322
  // Track subscription IDs for cleanup
2269
2323
  walletSubscriptionId = null;
2270
2324
  chatSubscriptionId = null;
2271
- subscribeToEvents() {
2325
+ async subscribeToEvents() {
2272
2326
  this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2273
2327
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2274
2328
  this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
@@ -2288,6 +2342,27 @@ var NostrTransportProvider = class {
2288
2342
  }
2289
2343
  const nostrPubkey = this.keyManager.getPublicKeyHex();
2290
2344
  this.log("Subscribing with Nostr pubkey:", nostrPubkey);
2345
+ let since;
2346
+ if (this.storage) {
2347
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${nostrPubkey.slice(0, 16)}`;
2348
+ try {
2349
+ const stored = await this.storage.get(storageKey);
2350
+ if (stored) {
2351
+ since = parseInt(stored, 10);
2352
+ this.lastEventTs = since;
2353
+ this.log("Resuming from stored event timestamp:", since);
2354
+ } else {
2355
+ since = Math.floor(Date.now() / 1e3);
2356
+ this.log("No stored timestamp, starting from now:", since);
2357
+ }
2358
+ } catch (err) {
2359
+ this.log("Failed to read last event timestamp, falling back to now:", err);
2360
+ since = Math.floor(Date.now() / 1e3);
2361
+ }
2362
+ } else {
2363
+ since = Math.floor(Date.now() / 1e3) - 86400;
2364
+ this.log("No storage adapter, using 24h fallback");
2365
+ }
2291
2366
  const walletFilter = new Filter();
2292
2367
  walletFilter.kinds = [
2293
2368
  EVENT_KINDS.DIRECT_MESSAGE,
@@ -2296,7 +2371,7 @@ var NostrTransportProvider = class {
2296
2371
  EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
2297
2372
  ];
2298
2373
  walletFilter["#p"] = [nostrPubkey];
2299
- walletFilter.since = Math.floor(Date.now() / 1e3) - 86400;
2374
+ walletFilter.since = since;
2300
2375
  this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
2301
2376
  onEvent: (event) => {
2302
2377
  this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
@@ -3206,15 +3281,17 @@ function createBrowserProviders(config) {
3206
3281
  const l1Config = resolveL1Config(network, config?.l1);
3207
3282
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
3208
3283
  const priceConfig = resolvePriceConfig(config?.price);
3284
+ const storage = createLocalStorageProvider(config?.storage);
3209
3285
  return {
3210
- storage: createLocalStorageProvider(config?.storage),
3286
+ storage,
3211
3287
  transport: createNostrTransportProvider({
3212
3288
  relays: transportConfig.relays,
3213
3289
  timeout: transportConfig.timeout,
3214
3290
  autoReconnect: transportConfig.autoReconnect,
3215
3291
  reconnectDelay: transportConfig.reconnectDelay,
3216
3292
  maxReconnectAttempts: transportConfig.maxReconnectAttempts,
3217
- debug: transportConfig.debug
3293
+ debug: transportConfig.debug,
3294
+ storage
3218
3295
  }),
3219
3296
  oracle: createUnicityAggregatorProvider({
3220
3297
  url: oracleConfig.url,