@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.
@@ -74,7 +74,9 @@ var STORAGE_KEYS_GLOBAL = {
74
74
  /** Nametag cache per address (separate from tracked addresses registry) */
75
75
  ADDRESS_NAMETAGS: "address_nametags",
76
76
  /** Active addresses registry (JSON: TrackedAddressesStorage) */
77
- TRACKED_ADDRESSES: "tracked_addresses"
77
+ TRACKED_ADDRESSES: "tracked_addresses",
78
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
79
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
78
80
  };
79
81
  var STORAGE_KEYS_ADDRESS = {
80
82
  /** Pending transfers for this address */
@@ -1141,6 +1143,9 @@ var NostrTransportProvider = class {
1141
1143
  type = "p2p";
1142
1144
  description = "P2P messaging via Nostr protocol";
1143
1145
  config;
1146
+ storage = null;
1147
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1148
+ lastEventTs = 0;
1144
1149
  identity = null;
1145
1150
  keyManager = null;
1146
1151
  status = "disconnected";
@@ -1166,6 +1171,7 @@ var NostrTransportProvider = class {
1166
1171
  createWebSocket: config.createWebSocket,
1167
1172
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
1168
1173
  };
1174
+ this.storage = config.storage ?? null;
1169
1175
  }
1170
1176
  // ===========================================================================
1171
1177
  // BaseProvider Implementation
@@ -1204,7 +1210,14 @@ var NostrTransportProvider = class {
1204
1210
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1205
1211
  }
1206
1212
  });
1207
- await this.nostrClient.connect(...this.config.relays);
1213
+ await Promise.race([
1214
+ this.nostrClient.connect(...this.config.relays),
1215
+ new Promise(
1216
+ (_, reject) => setTimeout(() => reject(new Error(
1217
+ `Transport connection timed out after ${this.config.timeout}ms`
1218
+ )), this.config.timeout)
1219
+ )
1220
+ ]);
1208
1221
  if (!this.nostrClient.isConnected()) {
1209
1222
  throw new Error("Failed to connect to any relay");
1210
1223
  }
@@ -1212,7 +1225,7 @@ var NostrTransportProvider = class {
1212
1225
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1213
1226
  this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
1214
1227
  if (this.identity) {
1215
- this.subscribeToEvents();
1228
+ await this.subscribeToEvents();
1216
1229
  }
1217
1230
  } catch (error) {
1218
1231
  this.status = "error";
@@ -1365,11 +1378,18 @@ var NostrTransportProvider = class {
1365
1378
  this.log("NostrClient reconnected to relay:", url);
1366
1379
  }
1367
1380
  });
1368
- await this.nostrClient.connect(...this.config.relays);
1369
- this.subscribeToEvents();
1381
+ await Promise.race([
1382
+ this.nostrClient.connect(...this.config.relays),
1383
+ new Promise(
1384
+ (_, reject) => setTimeout(() => reject(new Error(
1385
+ `Transport reconnection timed out after ${this.config.timeout}ms`
1386
+ )), this.config.timeout)
1387
+ )
1388
+ ]);
1389
+ await this.subscribeToEvents();
1370
1390
  oldClient.disconnect();
1371
1391
  } else if (this.isConnected()) {
1372
- this.subscribeToEvents();
1392
+ await this.subscribeToEvents();
1373
1393
  }
1374
1394
  }
1375
1395
  /**
@@ -1510,7 +1530,7 @@ var NostrTransportProvider = class {
1510
1530
  return this.resolveNametagInfo(identifier);
1511
1531
  }
1512
1532
  async resolveNametag(nametag) {
1513
- this.ensureReady();
1533
+ this.ensureConnected();
1514
1534
  const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
1515
1535
  let events = await this.queryEvents({
1516
1536
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1534,7 +1554,7 @@ var NostrTransportProvider = class {
1534
1554
  return null;
1535
1555
  }
1536
1556
  async resolveNametagInfo(nametag) {
1537
- this.ensureReady();
1557
+ this.ensureConnected();
1538
1558
  const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
1539
1559
  let events = await this.queryEvents({
1540
1560
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1611,7 +1631,7 @@ var NostrTransportProvider = class {
1611
1631
  * Works with both new identity binding events and legacy nametag binding events.
1612
1632
  */
1613
1633
  async resolveAddressInfo(address) {
1614
- this.ensureReady();
1634
+ this.ensureConnected();
1615
1635
  const addressHash = hashAddressForTag(address);
1616
1636
  const events = await this.queryEvents({
1617
1637
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
@@ -1646,7 +1666,7 @@ var NostrTransportProvider = class {
1646
1666
  * Queries binding events authored by the given pubkey.
1647
1667
  */
1648
1668
  async resolveTransportPubkeyInfo(transportPubkey) {
1649
- this.ensureReady();
1669
+ this.ensureConnected();
1650
1670
  const events = await this.queryEvents({
1651
1671
  kinds: [EVENT_KINDS.NAMETAG_BINDING],
1652
1672
  authors: [transportPubkey],
@@ -1897,10 +1917,31 @@ var NostrTransportProvider = class {
1897
1917
  this.handleBroadcast(event);
1898
1918
  break;
1899
1919
  }
1920
+ if (event.created_at && this.storage && this.keyManager) {
1921
+ const kind = event.kind;
1922
+ if (kind === EVENT_KINDS.DIRECT_MESSAGE || kind === EVENT_KINDS.TOKEN_TRANSFER || kind === EVENT_KINDS.PAYMENT_REQUEST || kind === EVENT_KINDS.PAYMENT_REQUEST_RESPONSE) {
1923
+ this.updateLastEventTimestamp(event.created_at);
1924
+ }
1925
+ }
1900
1926
  } catch (error) {
1901
1927
  this.log("Failed to handle event:", error);
1902
1928
  }
1903
1929
  }
1930
+ /**
1931
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
1932
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
1933
+ * when multiple events arrive in quick succession.
1934
+ */
1935
+ updateLastEventTimestamp(createdAt) {
1936
+ if (!this.storage || !this.keyManager) return;
1937
+ if (createdAt <= this.lastEventTs) return;
1938
+ this.lastEventTs = createdAt;
1939
+ const pubkey = this.keyManager.getPublicKeyHex();
1940
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${pubkey.slice(0, 16)}`;
1941
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
1942
+ this.log("Failed to save last event timestamp:", err);
1943
+ });
1944
+ }
1904
1945
  async handleDirectMessage(event) {
1905
1946
  this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
1906
1947
  }
@@ -1979,6 +2020,7 @@ var NostrTransportProvider = class {
1979
2020
  const request = {
1980
2021
  id: event.id,
1981
2022
  senderTransportPubkey: event.pubkey,
2023
+ senderNametag: requestData.recipientNametag,
1982
2024
  request: {
1983
2025
  requestId: requestData.requestId,
1984
2026
  amount: requestData.amount,
@@ -2133,7 +2175,7 @@ var NostrTransportProvider = class {
2133
2175
  // Track subscription IDs for cleanup
2134
2176
  walletSubscriptionId = null;
2135
2177
  chatSubscriptionId = null;
2136
- subscribeToEvents() {
2178
+ async subscribeToEvents() {
2137
2179
  this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2138
2180
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2139
2181
  this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
@@ -2153,6 +2195,27 @@ var NostrTransportProvider = class {
2153
2195
  }
2154
2196
  const nostrPubkey = this.keyManager.getPublicKeyHex();
2155
2197
  this.log("Subscribing with Nostr pubkey:", nostrPubkey);
2198
+ let since;
2199
+ if (this.storage) {
2200
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${nostrPubkey.slice(0, 16)}`;
2201
+ try {
2202
+ const stored = await this.storage.get(storageKey);
2203
+ if (stored) {
2204
+ since = parseInt(stored, 10);
2205
+ this.lastEventTs = since;
2206
+ this.log("Resuming from stored event timestamp:", since);
2207
+ } else {
2208
+ since = Math.floor(Date.now() / 1e3);
2209
+ this.log("No stored timestamp, starting from now:", since);
2210
+ }
2211
+ } catch (err) {
2212
+ this.log("Failed to read last event timestamp, falling back to now:", err);
2213
+ since = Math.floor(Date.now() / 1e3);
2214
+ }
2215
+ } else {
2216
+ since = Math.floor(Date.now() / 1e3) - 86400;
2217
+ this.log("No storage adapter, using 24h fallback");
2218
+ }
2156
2219
  const walletFilter = new import_nostr_js_sdk.Filter();
2157
2220
  walletFilter.kinds = [
2158
2221
  EVENT_KINDS.DIRECT_MESSAGE,
@@ -2161,7 +2224,7 @@ var NostrTransportProvider = class {
2161
2224
  EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
2162
2225
  ];
2163
2226
  walletFilter["#p"] = [nostrPubkey];
2164
- walletFilter.since = Math.floor(Date.now() / 1e3) - 86400;
2227
+ walletFilter.since = since;
2165
2228
  this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
2166
2229
  onEvent: (event) => {
2167
2230
  this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
@@ -2264,10 +2327,13 @@ var NostrTransportProvider = class {
2264
2327
  // ===========================================================================
2265
2328
  // Private: Helpers
2266
2329
  // ===========================================================================
2267
- ensureReady() {
2330
+ ensureConnected() {
2268
2331
  if (!this.isConnected()) {
2269
2332
  throw new Error("NostrTransportProvider not connected");
2270
2333
  }
2334
+ }
2335
+ ensureReady() {
2336
+ this.ensureConnected();
2271
2337
  if (!this.identity) {
2272
2338
  throw new Error("Identity not set");
2273
2339
  }
@@ -2955,10 +3021,11 @@ function createNodeProviders(config) {
2955
3021
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
2956
3022
  const l1Config = resolveL1Config(network, config?.l1);
2957
3023
  const priceConfig = resolvePriceConfig(config?.price);
3024
+ const storage = createFileStorageProvider({
3025
+ dataDir: config?.dataDir ?? "./sphere-data"
3026
+ });
2958
3027
  return {
2959
- storage: createFileStorageProvider({
2960
- dataDir: config?.dataDir ?? "./sphere-data"
2961
- }),
3028
+ storage,
2962
3029
  tokenStorage: createFileTokenStorageProvider({
2963
3030
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
2964
3031
  }),
@@ -2966,7 +3033,8 @@ function createNodeProviders(config) {
2966
3033
  relays: transportConfig.relays,
2967
3034
  timeout: transportConfig.timeout,
2968
3035
  autoReconnect: transportConfig.autoReconnect,
2969
- debug: transportConfig.debug
3036
+ debug: transportConfig.debug,
3037
+ storage
2970
3038
  }),
2971
3039
  oracle: createUnicityAggregatorProvider({
2972
3040
  url: oracleConfig.url,