@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.
@@ -552,6 +552,8 @@ interface IncomingPaymentRequest {
552
552
  id: string;
553
553
  /** Transport-specific pubkey of sender */
554
554
  senderTransportPubkey: string;
555
+ /** Sender's nametag (if included in encrypted content) */
556
+ senderNametag?: string;
555
557
  /** Parsed request data */
556
558
  request: {
557
559
  requestId: string;
@@ -675,6 +677,14 @@ type UUIDGenerator = () => string;
675
677
  * WebSocket is injected via factory for cross-platform support
676
678
  */
677
679
 
680
+ /**
681
+ * Minimal key-value storage interface for transport persistence.
682
+ * Used to persist the last processed event timestamp across sessions.
683
+ */
684
+ interface TransportStorageAdapter {
685
+ get(key: string): Promise<string | null>;
686
+ set(key: string, value: string): Promise<void>;
687
+ }
678
688
  interface NostrTransportProviderConfig {
679
689
  /** Nostr relay URLs */
680
690
  relays?: string[];
@@ -692,6 +702,8 @@ interface NostrTransportProviderConfig {
692
702
  createWebSocket: WebSocketFactory;
693
703
  /** UUID generator (optional, defaults to crypto.randomUUID) */
694
704
  generateUUID?: UUIDGenerator;
705
+ /** Optional storage adapter for persisting subscription timestamps */
706
+ storage?: TransportStorageAdapter;
695
707
  }
696
708
  declare class NostrTransportProvider implements TransportProvider {
697
709
  readonly id = "nostr";
@@ -699,6 +711,9 @@ declare class NostrTransportProvider implements TransportProvider {
699
711
  readonly type: "p2p";
700
712
  readonly description = "P2P messaging via Nostr protocol";
701
713
  private config;
714
+ private storage;
715
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
716
+ private lastEventTs;
702
717
  private identity;
703
718
  private keyManager;
704
719
  private status;
@@ -800,6 +815,12 @@ declare class NostrTransportProvider implements TransportProvider {
800
815
  publishBroadcast(content: string, tags?: string[]): Promise<string>;
801
816
  onEvent(callback: TransportEventCallback): () => void;
802
817
  private handleEvent;
818
+ /**
819
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
820
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
821
+ * when multiple events arrive in quick succession.
822
+ */
823
+ private updateLastEventTimestamp;
803
824
  private handleDirectMessage;
804
825
  private handleGiftWrap;
805
826
  private handleTokenTransfer;
@@ -552,6 +552,8 @@ interface IncomingPaymentRequest {
552
552
  id: string;
553
553
  /** Transport-specific pubkey of sender */
554
554
  senderTransportPubkey: string;
555
+ /** Sender's nametag (if included in encrypted content) */
556
+ senderNametag?: string;
555
557
  /** Parsed request data */
556
558
  request: {
557
559
  requestId: string;
@@ -675,6 +677,14 @@ type UUIDGenerator = () => string;
675
677
  * WebSocket is injected via factory for cross-platform support
676
678
  */
677
679
 
680
+ /**
681
+ * Minimal key-value storage interface for transport persistence.
682
+ * Used to persist the last processed event timestamp across sessions.
683
+ */
684
+ interface TransportStorageAdapter {
685
+ get(key: string): Promise<string | null>;
686
+ set(key: string, value: string): Promise<void>;
687
+ }
678
688
  interface NostrTransportProviderConfig {
679
689
  /** Nostr relay URLs */
680
690
  relays?: string[];
@@ -692,6 +702,8 @@ interface NostrTransportProviderConfig {
692
702
  createWebSocket: WebSocketFactory;
693
703
  /** UUID generator (optional, defaults to crypto.randomUUID) */
694
704
  generateUUID?: UUIDGenerator;
705
+ /** Optional storage adapter for persisting subscription timestamps */
706
+ storage?: TransportStorageAdapter;
695
707
  }
696
708
  declare class NostrTransportProvider implements TransportProvider {
697
709
  readonly id = "nostr";
@@ -699,6 +711,9 @@ declare class NostrTransportProvider implements TransportProvider {
699
711
  readonly type: "p2p";
700
712
  readonly description = "P2P messaging via Nostr protocol";
701
713
  private config;
714
+ private storage;
715
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
716
+ private lastEventTs;
702
717
  private identity;
703
718
  private keyManager;
704
719
  private status;
@@ -800,6 +815,12 @@ declare class NostrTransportProvider implements TransportProvider {
800
815
  publishBroadcast(content: string, tags?: string[]): Promise<string>;
801
816
  onEvent(callback: TransportEventCallback): () => void;
802
817
  private handleEvent;
818
+ /**
819
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
820
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
821
+ * when multiple events arrive in quick succession.
822
+ */
823
+ private updateLastEventTimestamp;
803
824
  private handleDirectMessage;
804
825
  private handleGiftWrap;
805
826
  private handleTokenTransfer;
@@ -25,7 +25,9 @@ var STORAGE_KEYS_GLOBAL = {
25
25
  /** Nametag cache per address (separate from tracked addresses registry) */
26
26
  ADDRESS_NAMETAGS: "address_nametags",
27
27
  /** Active addresses registry (JSON: TrackedAddressesStorage) */
28
- TRACKED_ADDRESSES: "tracked_addresses"
28
+ TRACKED_ADDRESSES: "tracked_addresses",
29
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
30
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
29
31
  };
30
32
  var STORAGE_KEYS_ADDRESS = {
31
33
  /** Pending transfers for this address */
@@ -1101,6 +1103,9 @@ var NostrTransportProvider = class {
1101
1103
  type = "p2p";
1102
1104
  description = "P2P messaging via Nostr protocol";
1103
1105
  config;
1106
+ storage = null;
1107
+ /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1108
+ lastEventTs = 0;
1104
1109
  identity = null;
1105
1110
  keyManager = null;
1106
1111
  status = "disconnected";
@@ -1126,6 +1131,7 @@ var NostrTransportProvider = class {
1126
1131
  createWebSocket: config.createWebSocket,
1127
1132
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
1128
1133
  };
1134
+ this.storage = config.storage ?? null;
1129
1135
  }
1130
1136
  // ===========================================================================
1131
1137
  // BaseProvider Implementation
@@ -1164,7 +1170,14 @@ var NostrTransportProvider = class {
1164
1170
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1165
1171
  }
1166
1172
  });
1167
- await this.nostrClient.connect(...this.config.relays);
1173
+ await Promise.race([
1174
+ this.nostrClient.connect(...this.config.relays),
1175
+ new Promise(
1176
+ (_, reject) => setTimeout(() => reject(new Error(
1177
+ `Transport connection timed out after ${this.config.timeout}ms`
1178
+ )), this.config.timeout)
1179
+ )
1180
+ ]);
1168
1181
  if (!this.nostrClient.isConnected()) {
1169
1182
  throw new Error("Failed to connect to any relay");
1170
1183
  }
@@ -1172,7 +1185,7 @@ var NostrTransportProvider = class {
1172
1185
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1173
1186
  this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
1174
1187
  if (this.identity) {
1175
- this.subscribeToEvents();
1188
+ await this.subscribeToEvents();
1176
1189
  }
1177
1190
  } catch (error) {
1178
1191
  this.status = "error";
@@ -1325,11 +1338,18 @@ var NostrTransportProvider = class {
1325
1338
  this.log("NostrClient reconnected to relay:", url);
1326
1339
  }
1327
1340
  });
1328
- await this.nostrClient.connect(...this.config.relays);
1329
- this.subscribeToEvents();
1341
+ await Promise.race([
1342
+ this.nostrClient.connect(...this.config.relays),
1343
+ new Promise(
1344
+ (_, reject) => setTimeout(() => reject(new Error(
1345
+ `Transport reconnection timed out after ${this.config.timeout}ms`
1346
+ )), this.config.timeout)
1347
+ )
1348
+ ]);
1349
+ await this.subscribeToEvents();
1330
1350
  oldClient.disconnect();
1331
1351
  } else if (this.isConnected()) {
1332
- this.subscribeToEvents();
1352
+ await this.subscribeToEvents();
1333
1353
  }
1334
1354
  }
1335
1355
  /**
@@ -1857,10 +1877,31 @@ var NostrTransportProvider = class {
1857
1877
  this.handleBroadcast(event);
1858
1878
  break;
1859
1879
  }
1880
+ if (event.created_at && this.storage && this.keyManager) {
1881
+ const kind = event.kind;
1882
+ if (kind === EVENT_KINDS.DIRECT_MESSAGE || kind === EVENT_KINDS.TOKEN_TRANSFER || kind === EVENT_KINDS.PAYMENT_REQUEST || kind === EVENT_KINDS.PAYMENT_REQUEST_RESPONSE) {
1883
+ this.updateLastEventTimestamp(event.created_at);
1884
+ }
1885
+ }
1860
1886
  } catch (error) {
1861
1887
  this.log("Failed to handle event:", error);
1862
1888
  }
1863
1889
  }
1890
+ /**
1891
+ * Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
1892
+ * Uses in-memory `lastEventTs` to avoid read-before-write race conditions
1893
+ * when multiple events arrive in quick succession.
1894
+ */
1895
+ updateLastEventTimestamp(createdAt) {
1896
+ if (!this.storage || !this.keyManager) return;
1897
+ if (createdAt <= this.lastEventTs) return;
1898
+ this.lastEventTs = createdAt;
1899
+ const pubkey = this.keyManager.getPublicKeyHex();
1900
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${pubkey.slice(0, 16)}`;
1901
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
1902
+ this.log("Failed to save last event timestamp:", err);
1903
+ });
1904
+ }
1864
1905
  async handleDirectMessage(event) {
1865
1906
  this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
1866
1907
  }
@@ -1939,6 +1980,7 @@ var NostrTransportProvider = class {
1939
1980
  const request = {
1940
1981
  id: event.id,
1941
1982
  senderTransportPubkey: event.pubkey,
1983
+ senderNametag: requestData.recipientNametag,
1942
1984
  request: {
1943
1985
  requestId: requestData.requestId,
1944
1986
  amount: requestData.amount,
@@ -2093,7 +2135,7 @@ var NostrTransportProvider = class {
2093
2135
  // Track subscription IDs for cleanup
2094
2136
  walletSubscriptionId = null;
2095
2137
  chatSubscriptionId = null;
2096
- subscribeToEvents() {
2138
+ async subscribeToEvents() {
2097
2139
  this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2098
2140
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2099
2141
  this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
@@ -2113,6 +2155,27 @@ var NostrTransportProvider = class {
2113
2155
  }
2114
2156
  const nostrPubkey = this.keyManager.getPublicKeyHex();
2115
2157
  this.log("Subscribing with Nostr pubkey:", nostrPubkey);
2158
+ let since;
2159
+ if (this.storage) {
2160
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${nostrPubkey.slice(0, 16)}`;
2161
+ try {
2162
+ const stored = await this.storage.get(storageKey);
2163
+ if (stored) {
2164
+ since = parseInt(stored, 10);
2165
+ this.lastEventTs = since;
2166
+ this.log("Resuming from stored event timestamp:", since);
2167
+ } else {
2168
+ since = Math.floor(Date.now() / 1e3);
2169
+ this.log("No stored timestamp, starting from now:", since);
2170
+ }
2171
+ } catch (err) {
2172
+ this.log("Failed to read last event timestamp, falling back to now:", err);
2173
+ since = Math.floor(Date.now() / 1e3);
2174
+ }
2175
+ } else {
2176
+ since = Math.floor(Date.now() / 1e3) - 86400;
2177
+ this.log("No storage adapter, using 24h fallback");
2178
+ }
2116
2179
  const walletFilter = new Filter();
2117
2180
  walletFilter.kinds = [
2118
2181
  EVENT_KINDS.DIRECT_MESSAGE,
@@ -2121,7 +2184,7 @@ var NostrTransportProvider = class {
2121
2184
  EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
2122
2185
  ];
2123
2186
  walletFilter["#p"] = [nostrPubkey];
2124
- walletFilter.since = Math.floor(Date.now() / 1e3) - 86400;
2187
+ walletFilter.since = since;
2125
2188
  this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
2126
2189
  onEvent: (event) => {
2127
2190
  this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
@@ -2918,10 +2981,11 @@ function createNodeProviders(config) {
2918
2981
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
2919
2982
  const l1Config = resolveL1Config(network, config?.l1);
2920
2983
  const priceConfig = resolvePriceConfig(config?.price);
2984
+ const storage = createFileStorageProvider({
2985
+ dataDir: config?.dataDir ?? "./sphere-data"
2986
+ });
2921
2987
  return {
2922
- storage: createFileStorageProvider({
2923
- dataDir: config?.dataDir ?? "./sphere-data"
2924
- }),
2988
+ storage,
2925
2989
  tokenStorage: createFileTokenStorageProvider({
2926
2990
  tokensDir: config?.tokensDir ?? "./sphere-tokens"
2927
2991
  }),
@@ -2929,7 +2993,8 @@ function createNodeProviders(config) {
2929
2993
  relays: transportConfig.relays,
2930
2994
  timeout: transportConfig.timeout,
2931
2995
  autoReconnect: transportConfig.autoReconnect,
2932
- debug: transportConfig.debug
2996
+ debug: transportConfig.debug,
2997
+ storage
2933
2998
  }),
2934
2999
  oracle: createUnicityAggregatorProvider({
2935
3000
  url: oracleConfig.url,