@unicitylabs/sphere-sdk 0.6.1 → 0.6.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.
@@ -145,7 +145,7 @@ function getAddressId(directAddress) {
145
145
  const last = hash.slice(-6).toLowerCase();
146
146
  return `DIRECT_${first}_${last}`;
147
147
  }
148
- var DEFAULT_ENCRYPTION_KEY, STORAGE_KEYS_GLOBAL, STORAGE_KEYS_ADDRESS, STORAGE_KEYS, DEFAULT_NOSTR_RELAYS, NIP29_KINDS, DEFAULT_AGGREGATOR_URL, DEV_AGGREGATOR_URL, TEST_AGGREGATOR_URL, DEFAULT_IPFS_GATEWAYS, DEFAULT_BASE_PATH, DEFAULT_DERIVATION_PATH, DEFAULT_ELECTRUM_URL, TEST_ELECTRUM_URL, TOKEN_REGISTRY_URL, TOKEN_REGISTRY_REFRESH_INTERVAL, TEST_NOSTR_RELAYS, DEFAULT_GROUP_RELAYS, NETWORKS;
148
+ var DEFAULT_ENCRYPTION_KEY, STORAGE_KEYS_GLOBAL, STORAGE_KEYS_ADDRESS, STORAGE_KEYS, DEFAULT_NOSTR_RELAYS, NOSTR_EVENT_KINDS, NIP29_KINDS, DEFAULT_AGGREGATOR_URL, DEV_AGGREGATOR_URL, TEST_AGGREGATOR_URL, DEFAULT_IPFS_GATEWAYS, DEFAULT_BASE_PATH, DEFAULT_DERIVATION_PATH, DEFAULT_ELECTRUM_URL, TEST_ELECTRUM_URL, TOKEN_REGISTRY_URL, TOKEN_REGISTRY_REFRESH_INTERVAL, TEST_NOSTR_RELAYS, DEFAULT_GROUP_RELAYS, NETWORKS, TIMEOUTS;
149
149
  var init_constants = __esm({
150
150
  "constants.ts"() {
151
151
  "use strict";
@@ -222,6 +222,20 @@ var init_constants = __esm({
222
222
  "wss://nos.lol",
223
223
  "wss://relay.nostr.band"
224
224
  ];
225
+ NOSTR_EVENT_KINDS = {
226
+ /** NIP-04 encrypted direct message */
227
+ DIRECT_MESSAGE: 4,
228
+ /** Token transfer (Unicity custom - 31113) */
229
+ TOKEN_TRANSFER: 31113,
230
+ /** Payment request (Unicity custom - 31115) */
231
+ PAYMENT_REQUEST: 31115,
232
+ /** Payment request response (Unicity custom - 31116) */
233
+ PAYMENT_REQUEST_RESPONSE: 31116,
234
+ /** Nametag binding (NIP-78 app-specific data) */
235
+ NAMETAG_BINDING: 30078,
236
+ /** Public broadcast */
237
+ BROADCAST: 1
238
+ };
225
239
  NIP29_KINDS = {
226
240
  /** Chat message sent to group */
227
241
  CHAT_MESSAGE: 9,
@@ -303,6 +317,18 @@ var init_constants = __esm({
303
317
  tokenRegistryUrl: TOKEN_REGISTRY_URL
304
318
  }
305
319
  };
320
+ TIMEOUTS = {
321
+ /** WebSocket connection timeout */
322
+ WEBSOCKET_CONNECT: 1e4,
323
+ /** Nostr relay reconnect delay */
324
+ NOSTR_RECONNECT_DELAY: 3e3,
325
+ /** Max reconnect attempts */
326
+ MAX_RECONNECT_ATTEMPTS: 5,
327
+ /** Proof polling interval */
328
+ PROOF_POLL_INTERVAL: 1e3,
329
+ /** Sync interval */
330
+ SYNC_INTERVAL: 6e4
331
+ };
306
332
  }
307
333
  });
308
334
 
@@ -753,6 +779,1112 @@ var init_network = __esm({
753
779
  init_logger();
754
780
  init_errors();
755
781
 
782
+ // transport/MultiAddressTransportMux.ts
783
+ init_logger();
784
+ init_errors();
785
+ import { Buffer as Buffer2 } from "buffer";
786
+ import {
787
+ NostrKeyManager,
788
+ NIP04,
789
+ NIP17,
790
+ Event as NostrEventClass,
791
+ EventKinds,
792
+ NostrClient,
793
+ Filter,
794
+ isChatMessage,
795
+ isReadReceipt
796
+ } from "@unicitylabs/nostr-js-sdk";
797
+
798
+ // transport/websocket.ts
799
+ function defaultUUIDGenerator() {
800
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
801
+ return crypto.randomUUID();
802
+ }
803
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
804
+ const r = Math.random() * 16 | 0;
805
+ const v = c === "x" ? r : r & 3 | 8;
806
+ return v.toString(16);
807
+ });
808
+ }
809
+
810
+ // transport/MultiAddressTransportMux.ts
811
+ init_constants();
812
+ var EVENT_KINDS = NOSTR_EVENT_KINDS;
813
+ var COMPOSING_INDICATOR_KIND = 25050;
814
+ var MultiAddressTransportMux = class {
815
+ config;
816
+ storage = null;
817
+ // Single NostrClient — one WebSocket connection for all addresses
818
+ nostrClient = null;
819
+ // KeyManager used for NostrClient creation (uses first address or temp key)
820
+ primaryKeyManager = null;
821
+ status = "disconnected";
822
+ // Per-address entries
823
+ addresses = /* @__PURE__ */ new Map();
824
+ // pubkey → address index (for fast routing)
825
+ pubkeyToIndex = /* @__PURE__ */ new Map();
826
+ // Subscription IDs
827
+ walletSubscriptionId = null;
828
+ chatSubscriptionId = null;
829
+ chatEoseFired = false;
830
+ chatEoseHandlers = [];
831
+ // Dedup
832
+ processedEventIds = /* @__PURE__ */ new Set();
833
+ // Event callbacks (mux-level, forwarded to all adapters)
834
+ eventCallbacks = /* @__PURE__ */ new Set();
835
+ constructor(config) {
836
+ this.config = {
837
+ relays: config.relays ?? [...DEFAULT_NOSTR_RELAYS],
838
+ timeout: config.timeout ?? TIMEOUTS.WEBSOCKET_CONNECT,
839
+ autoReconnect: config.autoReconnect ?? true,
840
+ reconnectDelay: config.reconnectDelay ?? TIMEOUTS.NOSTR_RECONNECT_DELAY,
841
+ maxReconnectAttempts: config.maxReconnectAttempts ?? TIMEOUTS.MAX_RECONNECT_ATTEMPTS,
842
+ createWebSocket: config.createWebSocket,
843
+ generateUUID: config.generateUUID ?? defaultUUIDGenerator
844
+ };
845
+ this.storage = config.storage ?? null;
846
+ }
847
+ // ===========================================================================
848
+ // Address Management
849
+ // ===========================================================================
850
+ /**
851
+ * Add an address to the multiplexer.
852
+ * Creates an AddressTransportAdapter for this address.
853
+ * If already connected, updates subscriptions to include the new pubkey.
854
+ */
855
+ async addAddress(index, identity, resolveDelegate) {
856
+ const existing = this.addresses.get(index);
857
+ if (existing) {
858
+ existing.identity = identity;
859
+ existing.keyManager = NostrKeyManager.fromPrivateKey(Buffer2.from(identity.privateKey, "hex"));
860
+ existing.nostrPubkey = existing.keyManager.getPublicKeyHex();
861
+ for (const [pk, idx] of this.pubkeyToIndex) {
862
+ if (idx === index) this.pubkeyToIndex.delete(pk);
863
+ }
864
+ this.pubkeyToIndex.set(existing.nostrPubkey, index);
865
+ logger.debug("Mux", `Updated address ${index}, pubkey: ${existing.nostrPubkey.slice(0, 16)}...`);
866
+ await this.updateSubscriptions();
867
+ return existing.adapter;
868
+ }
869
+ const keyManager = NostrKeyManager.fromPrivateKey(Buffer2.from(identity.privateKey, "hex"));
870
+ const nostrPubkey = keyManager.getPublicKeyHex();
871
+ const adapter = new AddressTransportAdapter(this, index, identity, resolveDelegate);
872
+ const entry = {
873
+ index,
874
+ identity,
875
+ keyManager,
876
+ nostrPubkey,
877
+ adapter,
878
+ lastEventTs: 0,
879
+ fallbackSince: null
880
+ };
881
+ this.addresses.set(index, entry);
882
+ this.pubkeyToIndex.set(nostrPubkey, index);
883
+ logger.debug("Mux", `Added address ${index}, pubkey: ${nostrPubkey.slice(0, 16)}..., total: ${this.addresses.size}`);
884
+ if (this.addresses.size === 1) {
885
+ this.primaryKeyManager = keyManager;
886
+ }
887
+ if (this.isConnected()) {
888
+ await this.updateSubscriptions();
889
+ }
890
+ return adapter;
891
+ }
892
+ /**
893
+ * Remove an address from the multiplexer.
894
+ * Stops routing events to this address.
895
+ */
896
+ async removeAddress(index) {
897
+ const entry = this.addresses.get(index);
898
+ if (!entry) return;
899
+ this.pubkeyToIndex.delete(entry.nostrPubkey);
900
+ this.addresses.delete(index);
901
+ logger.debug("Mux", `Removed address ${index}, remaining: ${this.addresses.size}`);
902
+ if (this.isConnected() && this.addresses.size > 0) {
903
+ await this.updateSubscriptions();
904
+ }
905
+ }
906
+ /**
907
+ * Get adapter for a specific address index.
908
+ */
909
+ getAdapter(index) {
910
+ return this.addresses.get(index)?.adapter;
911
+ }
912
+ /**
913
+ * Set fallback 'since' for an address (consumed once on next subscription setup).
914
+ */
915
+ setFallbackSince(index, sinceSeconds) {
916
+ const entry = this.addresses.get(index);
917
+ if (entry) {
918
+ entry.fallbackSince = sinceSeconds;
919
+ }
920
+ }
921
+ // ===========================================================================
922
+ // Connection Management (delegated from adapters)
923
+ // ===========================================================================
924
+ async connect() {
925
+ if (this.status === "connected") return;
926
+ this.status = "connecting";
927
+ try {
928
+ if (!this.primaryKeyManager) {
929
+ const tempKey = Buffer2.alloc(32);
930
+ crypto.getRandomValues(tempKey);
931
+ this.primaryKeyManager = NostrKeyManager.fromPrivateKey(tempKey);
932
+ }
933
+ this.nostrClient = new NostrClient(this.primaryKeyManager, {
934
+ autoReconnect: this.config.autoReconnect,
935
+ reconnectIntervalMs: this.config.reconnectDelay,
936
+ maxReconnectIntervalMs: this.config.reconnectDelay * 16,
937
+ pingIntervalMs: 15e3
938
+ });
939
+ this.nostrClient.addConnectionListener({
940
+ onConnect: (url) => {
941
+ logger.debug("Mux", "Connected to relay:", url);
942
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
943
+ },
944
+ onDisconnect: (url, reason) => {
945
+ logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
946
+ },
947
+ onReconnecting: (url, attempt) => {
948
+ logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
949
+ this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
950
+ },
951
+ onReconnected: (url) => {
952
+ logger.debug("Mux", "Reconnected to relay:", url);
953
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
954
+ }
955
+ });
956
+ await Promise.race([
957
+ this.nostrClient.connect(...this.config.relays),
958
+ new Promise(
959
+ (_, reject) => setTimeout(() => reject(new Error(
960
+ `Transport connection timed out after ${this.config.timeout}ms`
961
+ )), this.config.timeout)
962
+ )
963
+ ]);
964
+ if (!this.nostrClient.isConnected()) {
965
+ throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
966
+ }
967
+ this.status = "connected";
968
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
969
+ if (this.addresses.size > 0) {
970
+ await this.updateSubscriptions();
971
+ }
972
+ } catch (error) {
973
+ this.status = "error";
974
+ throw error;
975
+ }
976
+ }
977
+ async disconnect() {
978
+ if (this.nostrClient) {
979
+ this.nostrClient.disconnect();
980
+ this.nostrClient = null;
981
+ }
982
+ this.walletSubscriptionId = null;
983
+ this.chatSubscriptionId = null;
984
+ this.chatEoseFired = false;
985
+ this.status = "disconnected";
986
+ this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
987
+ }
988
+ isConnected() {
989
+ return this.status === "connected" && this.nostrClient?.isConnected() === true;
990
+ }
991
+ getStatus() {
992
+ return this.status;
993
+ }
994
+ // ===========================================================================
995
+ // Relay Management
996
+ // ===========================================================================
997
+ getRelays() {
998
+ return [...this.config.relays];
999
+ }
1000
+ getConnectedRelays() {
1001
+ if (!this.nostrClient) return [];
1002
+ return Array.from(this.nostrClient.getConnectedRelays());
1003
+ }
1004
+ async addRelay(relayUrl) {
1005
+ if (this.config.relays.includes(relayUrl)) return false;
1006
+ this.config.relays.push(relayUrl);
1007
+ if (this.status === "connected" && this.nostrClient) {
1008
+ try {
1009
+ await this.nostrClient.connect(relayUrl);
1010
+ this.emitEvent({ type: "transport:relay_added", timestamp: Date.now(), data: { relay: relayUrl, connected: true } });
1011
+ return true;
1012
+ } catch (error) {
1013
+ this.emitEvent({ type: "transport:relay_added", timestamp: Date.now(), data: { relay: relayUrl, connected: false, error: String(error) } });
1014
+ return false;
1015
+ }
1016
+ }
1017
+ return true;
1018
+ }
1019
+ async removeRelay(relayUrl) {
1020
+ const idx = this.config.relays.indexOf(relayUrl);
1021
+ if (idx === -1) return false;
1022
+ this.config.relays.splice(idx, 1);
1023
+ this.emitEvent({ type: "transport:relay_removed", timestamp: Date.now(), data: { relay: relayUrl } });
1024
+ return true;
1025
+ }
1026
+ hasRelay(relayUrl) {
1027
+ return this.config.relays.includes(relayUrl);
1028
+ }
1029
+ isRelayConnected(relayUrl) {
1030
+ if (!this.nostrClient) return false;
1031
+ return this.nostrClient.getConnectedRelays().has(relayUrl);
1032
+ }
1033
+ // ===========================================================================
1034
+ // Subscription Management
1035
+ // ===========================================================================
1036
+ /**
1037
+ * Update Nostr subscriptions to listen for events on ALL registered address pubkeys.
1038
+ * Called whenever addresses are added/removed.
1039
+ */
1040
+ async updateSubscriptions() {
1041
+ if (!this.nostrClient || this.addresses.size === 0) return;
1042
+ if (this.walletSubscriptionId) {
1043
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1044
+ this.walletSubscriptionId = null;
1045
+ }
1046
+ if (this.chatSubscriptionId) {
1047
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1048
+ this.chatSubscriptionId = null;
1049
+ }
1050
+ const allPubkeys = [];
1051
+ for (const entry of this.addresses.values()) {
1052
+ allPubkeys.push(entry.nostrPubkey);
1053
+ }
1054
+ logger.debug("Mux", `Subscribing for ${allPubkeys.length} address(es):`, allPubkeys.map((p) => p.slice(0, 12)).join(", "));
1055
+ let globalSince = Math.floor(Date.now() / 1e3);
1056
+ for (const entry of this.addresses.values()) {
1057
+ const since = await this.getAddressSince(entry);
1058
+ if (since < globalSince) {
1059
+ globalSince = since;
1060
+ }
1061
+ }
1062
+ const walletFilter = new Filter();
1063
+ walletFilter.kinds = [
1064
+ EVENT_KINDS.DIRECT_MESSAGE,
1065
+ EVENT_KINDS.TOKEN_TRANSFER,
1066
+ EVENT_KINDS.PAYMENT_REQUEST,
1067
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
1068
+ ];
1069
+ walletFilter["#p"] = allPubkeys;
1070
+ walletFilter.since = globalSince;
1071
+ this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
1072
+ onEvent: (event) => {
1073
+ this.handleEvent({
1074
+ id: event.id,
1075
+ kind: event.kind,
1076
+ content: event.content,
1077
+ tags: event.tags,
1078
+ pubkey: event.pubkey,
1079
+ created_at: event.created_at,
1080
+ sig: event.sig
1081
+ });
1082
+ },
1083
+ onEndOfStoredEvents: () => {
1084
+ logger.debug("Mux", "Wallet subscription EOSE");
1085
+ },
1086
+ onError: (_subId, error) => {
1087
+ logger.debug("Mux", "Wallet subscription error:", error);
1088
+ }
1089
+ });
1090
+ const chatFilter = new Filter();
1091
+ chatFilter.kinds = [EventKinds.GIFT_WRAP];
1092
+ chatFilter["#p"] = allPubkeys;
1093
+ this.chatSubscriptionId = this.nostrClient.subscribe(chatFilter, {
1094
+ onEvent: (event) => {
1095
+ this.handleEvent({
1096
+ id: event.id,
1097
+ kind: event.kind,
1098
+ content: event.content,
1099
+ tags: event.tags,
1100
+ pubkey: event.pubkey,
1101
+ created_at: event.created_at,
1102
+ sig: event.sig
1103
+ });
1104
+ },
1105
+ onEndOfStoredEvents: () => {
1106
+ logger.debug("Mux", "Chat subscription EOSE");
1107
+ if (!this.chatEoseFired) {
1108
+ this.chatEoseFired = true;
1109
+ for (const handler of this.chatEoseHandlers) {
1110
+ try {
1111
+ handler();
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ }
1116
+ },
1117
+ onError: (_subId, error) => {
1118
+ logger.debug("Mux", "Chat subscription error:", error);
1119
+ }
1120
+ });
1121
+ }
1122
+ /**
1123
+ * Determine 'since' timestamp for an address entry.
1124
+ */
1125
+ async getAddressSince(entry) {
1126
+ if (this.storage) {
1127
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${entry.nostrPubkey.slice(0, 16)}`;
1128
+ try {
1129
+ const stored = await this.storage.get(storageKey);
1130
+ if (stored) {
1131
+ const ts = parseInt(stored, 10);
1132
+ entry.lastEventTs = ts;
1133
+ entry.fallbackSince = null;
1134
+ return ts;
1135
+ } else if (entry.fallbackSince !== null) {
1136
+ const ts = entry.fallbackSince;
1137
+ entry.lastEventTs = ts;
1138
+ entry.fallbackSince = null;
1139
+ return ts;
1140
+ }
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ return Math.floor(Date.now() / 1e3);
1145
+ }
1146
+ // ===========================================================================
1147
+ // Event Routing
1148
+ // ===========================================================================
1149
+ /**
1150
+ * Route an incoming Nostr event to the correct address adapter.
1151
+ */
1152
+ async handleEvent(event) {
1153
+ if (event.id && this.processedEventIds.has(event.id)) return;
1154
+ if (event.id) this.processedEventIds.add(event.id);
1155
+ try {
1156
+ if (event.kind === EventKinds.GIFT_WRAP) {
1157
+ await this.routeGiftWrap(event);
1158
+ } else {
1159
+ const recipientPubkey = this.extractRecipientPubkey(event);
1160
+ if (!recipientPubkey) {
1161
+ logger.debug("Mux", "Event has no #p tag, dropping:", event.id?.slice(0, 12));
1162
+ return;
1163
+ }
1164
+ const addressIndex = this.pubkeyToIndex.get(recipientPubkey);
1165
+ if (addressIndex === void 0) {
1166
+ logger.debug("Mux", "Event for unknown pubkey:", recipientPubkey.slice(0, 16), "dropping");
1167
+ return;
1168
+ }
1169
+ const entry = this.addresses.get(addressIndex);
1170
+ if (!entry) return;
1171
+ await this.dispatchWalletEvent(entry, event);
1172
+ }
1173
+ } catch (error) {
1174
+ logger.debug("Mux", "Failed to handle event:", event.id?.slice(0, 12), error);
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Extract recipient pubkey from event's #p tag.
1179
+ * Returns the first #p value that matches a known address pubkey,
1180
+ * or the first #p value if none match.
1181
+ */
1182
+ extractRecipientPubkey(event) {
1183
+ const pTags = event.tags?.filter((t) => t[0] === "p");
1184
+ if (!pTags || pTags.length === 0) return null;
1185
+ for (const tag of pTags) {
1186
+ if (tag[1] && this.pubkeyToIndex.has(tag[1])) {
1187
+ return tag[1];
1188
+ }
1189
+ }
1190
+ return pTags[0]?.[1] ?? null;
1191
+ }
1192
+ /**
1193
+ * Route a gift wrap event by trying decryption with each address keyManager.
1194
+ */
1195
+ async routeGiftWrap(event) {
1196
+ for (const entry of this.addresses.values()) {
1197
+ try {
1198
+ const pm = NIP17.unwrap(event, entry.keyManager);
1199
+ logger.debug("Mux", `Gift wrap decrypted by address ${entry.index}, sender: ${pm.senderPubkey?.slice(0, 16)}`);
1200
+ if (pm.senderPubkey === entry.nostrPubkey) {
1201
+ try {
1202
+ const parsed = JSON.parse(pm.content);
1203
+ if (parsed?.selfWrap && parsed.recipientPubkey) {
1204
+ const message2 = {
1205
+ id: parsed.originalId || pm.eventId,
1206
+ senderTransportPubkey: pm.senderPubkey,
1207
+ senderNametag: parsed.senderNametag,
1208
+ recipientTransportPubkey: parsed.recipientPubkey,
1209
+ content: parsed.text ?? "",
1210
+ timestamp: pm.timestamp * 1e3,
1211
+ encrypted: true,
1212
+ isSelfWrap: true
1213
+ };
1214
+ entry.adapter.dispatchMessage(message2);
1215
+ return;
1216
+ }
1217
+ } catch {
1218
+ }
1219
+ return;
1220
+ }
1221
+ if (isReadReceipt(pm)) {
1222
+ if (pm.replyToEventId) {
1223
+ const receipt = {
1224
+ senderTransportPubkey: pm.senderPubkey,
1225
+ messageEventId: pm.replyToEventId,
1226
+ timestamp: pm.timestamp * 1e3
1227
+ };
1228
+ entry.adapter.dispatchReadReceipt(receipt);
1229
+ }
1230
+ return;
1231
+ }
1232
+ if (pm.kind === COMPOSING_INDICATOR_KIND) {
1233
+ let senderNametag2;
1234
+ let expiresIn = 3e4;
1235
+ try {
1236
+ const parsed = JSON.parse(pm.content);
1237
+ senderNametag2 = parsed.senderNametag || void 0;
1238
+ expiresIn = parsed.expiresIn ?? 3e4;
1239
+ } catch {
1240
+ }
1241
+ entry.adapter.dispatchComposingIndicator({
1242
+ senderPubkey: pm.senderPubkey,
1243
+ senderNametag: senderNametag2,
1244
+ expiresIn
1245
+ });
1246
+ return;
1247
+ }
1248
+ try {
1249
+ const parsed = JSON.parse(pm.content);
1250
+ if (parsed?.type === "typing") {
1251
+ const indicator = {
1252
+ senderTransportPubkey: pm.senderPubkey,
1253
+ senderNametag: parsed.senderNametag,
1254
+ timestamp: pm.timestamp * 1e3
1255
+ };
1256
+ entry.adapter.dispatchTypingIndicator(indicator);
1257
+ return;
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ if (!isChatMessage(pm)) return;
1262
+ let content = pm.content;
1263
+ let senderNametag;
1264
+ try {
1265
+ const parsed = JSON.parse(content);
1266
+ if (typeof parsed === "object" && parsed.text !== void 0) {
1267
+ content = parsed.text;
1268
+ senderNametag = parsed.senderNametag || void 0;
1269
+ }
1270
+ } catch {
1271
+ }
1272
+ const message = {
1273
+ id: event.id,
1274
+ senderTransportPubkey: pm.senderPubkey,
1275
+ senderNametag,
1276
+ content,
1277
+ timestamp: pm.timestamp * 1e3,
1278
+ encrypted: true
1279
+ };
1280
+ entry.adapter.dispatchMessage(message);
1281
+ return;
1282
+ } catch {
1283
+ continue;
1284
+ }
1285
+ }
1286
+ logger.debug("Mux", "Gift wrap could not be decrypted by any address");
1287
+ }
1288
+ /**
1289
+ * Dispatch a wallet event (non-gift-wrap) to the correct address adapter.
1290
+ */
1291
+ async dispatchWalletEvent(entry, event) {
1292
+ switch (event.kind) {
1293
+ case EVENT_KINDS.DIRECT_MESSAGE:
1294
+ break;
1295
+ case EVENT_KINDS.TOKEN_TRANSFER:
1296
+ await this.handleTokenTransfer(entry, event);
1297
+ break;
1298
+ case EVENT_KINDS.PAYMENT_REQUEST:
1299
+ await this.handlePaymentRequest(entry, event);
1300
+ break;
1301
+ case EVENT_KINDS.PAYMENT_REQUEST_RESPONSE:
1302
+ await this.handlePaymentRequestResponse(entry, event);
1303
+ break;
1304
+ }
1305
+ if (event.created_at) {
1306
+ this.updateLastEventTimestamp(entry, event.created_at);
1307
+ }
1308
+ }
1309
+ async handleTokenTransfer(entry, event) {
1310
+ try {
1311
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1312
+ const payload = JSON.parse(content);
1313
+ const transfer = {
1314
+ id: event.id,
1315
+ senderTransportPubkey: event.pubkey,
1316
+ payload,
1317
+ timestamp: event.created_at * 1e3
1318
+ };
1319
+ entry.adapter.dispatchTokenTransfer(transfer);
1320
+ } catch (err) {
1321
+ logger.debug("Mux", `Token transfer decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1322
+ }
1323
+ }
1324
+ async handlePaymentRequest(entry, event) {
1325
+ try {
1326
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1327
+ const requestData = JSON.parse(content);
1328
+ const request = {
1329
+ id: event.id,
1330
+ senderTransportPubkey: event.pubkey,
1331
+ request: {
1332
+ requestId: requestData.requestId,
1333
+ amount: requestData.amount,
1334
+ coinId: requestData.coinId,
1335
+ message: requestData.message,
1336
+ recipientNametag: requestData.recipientNametag,
1337
+ metadata: requestData.metadata
1338
+ },
1339
+ timestamp: event.created_at * 1e3
1340
+ };
1341
+ entry.adapter.dispatchPaymentRequest(request);
1342
+ } catch (err) {
1343
+ logger.debug("Mux", `Payment request decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1344
+ }
1345
+ }
1346
+ async handlePaymentRequestResponse(entry, event) {
1347
+ try {
1348
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1349
+ const responseData = JSON.parse(content);
1350
+ const response = {
1351
+ id: event.id,
1352
+ responderTransportPubkey: event.pubkey,
1353
+ response: {
1354
+ requestId: responseData.requestId,
1355
+ responseType: responseData.responseType,
1356
+ message: responseData.message,
1357
+ transferId: responseData.transferId
1358
+ },
1359
+ timestamp: event.created_at * 1e3
1360
+ };
1361
+ entry.adapter.dispatchPaymentRequestResponse(response);
1362
+ } catch (err) {
1363
+ logger.debug("Mux", `Payment response decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1364
+ }
1365
+ }
1366
+ // ===========================================================================
1367
+ // Crypto Helpers
1368
+ // ===========================================================================
1369
+ async decryptContent(entry, content, senderPubkey) {
1370
+ const decrypted = await NIP04.decryptHex(
1371
+ content,
1372
+ entry.keyManager.getPrivateKeyHex(),
1373
+ senderPubkey
1374
+ );
1375
+ return this.stripContentPrefix(decrypted);
1376
+ }
1377
+ stripContentPrefix(content) {
1378
+ const prefixes = ["payment_request:", "token_transfer:", "payment_response:"];
1379
+ for (const prefix of prefixes) {
1380
+ if (content.startsWith(prefix)) return content.slice(prefix.length);
1381
+ }
1382
+ return content;
1383
+ }
1384
+ // ===========================================================================
1385
+ // Sending (called by adapters)
1386
+ // ===========================================================================
1387
+ /**
1388
+ * Create an encrypted event using a specific address's keyManager.
1389
+ * Used by AddressTransportAdapter for sending.
1390
+ */
1391
+ async createAndPublishEncryptedEvent(addressIndex, kind, content, tags) {
1392
+ const entry = this.addresses.get(addressIndex);
1393
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1394
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1395
+ const recipientTag = tags.find((t) => t[0] === "p");
1396
+ if (!recipientTag?.[1]) throw new SphereError("No recipient pubkey in tags", "VALIDATION_ERROR");
1397
+ const encrypted = await NIP04.encryptHex(
1398
+ content,
1399
+ entry.keyManager.getPrivateKeyHex(),
1400
+ recipientTag[1]
1401
+ );
1402
+ const signedEvent = NostrEventClass.create(entry.keyManager, { kind, content: encrypted, tags });
1403
+ const nostrEvent = NostrEventClass.fromJSON({
1404
+ id: signedEvent.id,
1405
+ kind: signedEvent.kind,
1406
+ content: signedEvent.content,
1407
+ tags: signedEvent.tags,
1408
+ pubkey: signedEvent.pubkey,
1409
+ created_at: signedEvent.created_at,
1410
+ sig: signedEvent.sig
1411
+ });
1412
+ await this.nostrClient.publishEvent(nostrEvent);
1413
+ return signedEvent.id;
1414
+ }
1415
+ /**
1416
+ * Create and publish a NIP-17 gift wrap message for a specific address.
1417
+ */
1418
+ async sendGiftWrap(addressIndex, recipientPubkey, content) {
1419
+ const entry = this.addresses.get(addressIndex);
1420
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1421
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1422
+ const nostrRecipient = recipientPubkey.length === 66 && (recipientPubkey.startsWith("02") || recipientPubkey.startsWith("03")) ? recipientPubkey.slice(2) : recipientPubkey;
1423
+ const giftWrap = NIP17.createGiftWrap(entry.keyManager, nostrRecipient, content);
1424
+ const giftWrapEvent = NostrEventClass.fromJSON(giftWrap);
1425
+ await this.nostrClient.publishEvent(giftWrapEvent);
1426
+ const selfPubkey = entry.keyManager.getPublicKeyHex();
1427
+ const senderNametag = entry.identity.nametag;
1428
+ const selfWrapContent = JSON.stringify({
1429
+ selfWrap: true,
1430
+ originalId: giftWrap.id,
1431
+ recipientPubkey,
1432
+ senderNametag,
1433
+ text: content
1434
+ });
1435
+ const selfGiftWrap = NIP17.createGiftWrap(entry.keyManager, selfPubkey, selfWrapContent);
1436
+ const selfGiftWrapEvent = NostrEventClass.fromJSON(selfGiftWrap);
1437
+ this.nostrClient.publishEvent(selfGiftWrapEvent).catch((err) => {
1438
+ logger.debug("Mux", "Self-wrap publish failed:", err);
1439
+ });
1440
+ return giftWrap.id;
1441
+ }
1442
+ /**
1443
+ * Publish a raw event (e.g., identity binding, broadcast).
1444
+ */
1445
+ async publishRawEvent(addressIndex, kind, content, tags) {
1446
+ const entry = this.addresses.get(addressIndex);
1447
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1448
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1449
+ const signedEvent = NostrEventClass.create(entry.keyManager, { kind, content, tags });
1450
+ const nostrEvent = NostrEventClass.fromJSON({
1451
+ id: signedEvent.id,
1452
+ kind: signedEvent.kind,
1453
+ content: signedEvent.content,
1454
+ tags: signedEvent.tags,
1455
+ pubkey: signedEvent.pubkey,
1456
+ created_at: signedEvent.created_at,
1457
+ sig: signedEvent.sig
1458
+ });
1459
+ await this.nostrClient.publishEvent(nostrEvent);
1460
+ return signedEvent.id;
1461
+ }
1462
+ // ===========================================================================
1463
+ // Resolve Methods (delegates to inner — these are stateless relay queries)
1464
+ // ===========================================================================
1465
+ /**
1466
+ * Get the NostrClient for resolve operations.
1467
+ * Adapters use this for resolve*, publishIdentityBinding, etc.
1468
+ */
1469
+ getNostrClient() {
1470
+ return this.nostrClient;
1471
+ }
1472
+ /**
1473
+ * Get keyManager for a specific address (used by adapters for resolve/binding).
1474
+ */
1475
+ getKeyManager(addressIndex) {
1476
+ return this.addresses.get(addressIndex)?.keyManager ?? null;
1477
+ }
1478
+ /**
1479
+ * Get identity for a specific address.
1480
+ */
1481
+ getIdentity(addressIndex) {
1482
+ return this.addresses.get(addressIndex)?.identity ?? null;
1483
+ }
1484
+ // ===========================================================================
1485
+ // Event timestamp persistence
1486
+ // ===========================================================================
1487
+ updateLastEventTimestamp(entry, createdAt) {
1488
+ if (!this.storage) return;
1489
+ if (createdAt <= entry.lastEventTs) return;
1490
+ entry.lastEventTs = createdAt;
1491
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${entry.nostrPubkey.slice(0, 16)}`;
1492
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
1493
+ logger.debug("Mux", "Failed to save last event timestamp:", err);
1494
+ });
1495
+ }
1496
+ // ===========================================================================
1497
+ // Mux-level event system
1498
+ // ===========================================================================
1499
+ onTransportEvent(callback) {
1500
+ this.eventCallbacks.add(callback);
1501
+ return () => this.eventCallbacks.delete(callback);
1502
+ }
1503
+ onChatReady(handler) {
1504
+ if (this.chatEoseFired) {
1505
+ try {
1506
+ handler();
1507
+ } catch {
1508
+ }
1509
+ return () => {
1510
+ };
1511
+ }
1512
+ this.chatEoseHandlers.push(handler);
1513
+ return () => {
1514
+ const idx = this.chatEoseHandlers.indexOf(handler);
1515
+ if (idx >= 0) this.chatEoseHandlers.splice(idx, 1);
1516
+ };
1517
+ }
1518
+ emitEvent(event) {
1519
+ for (const cb of this.eventCallbacks) {
1520
+ try {
1521
+ cb(event);
1522
+ } catch {
1523
+ }
1524
+ }
1525
+ for (const entry of this.addresses.values()) {
1526
+ entry.adapter.emitTransportEvent(event);
1527
+ }
1528
+ }
1529
+ // ===========================================================================
1530
+ // Dedup Management
1531
+ // ===========================================================================
1532
+ /**
1533
+ * Clear processed event IDs (e.g., on address change or periodic cleanup).
1534
+ */
1535
+ clearProcessedEvents() {
1536
+ this.processedEventIds.clear();
1537
+ }
1538
+ /**
1539
+ * Get the storage adapter (for adapters that need it).
1540
+ */
1541
+ getStorage() {
1542
+ return this.storage;
1543
+ }
1544
+ /**
1545
+ * Get the UUID generator.
1546
+ */
1547
+ getUUIDGenerator() {
1548
+ return this.config.generateUUID;
1549
+ }
1550
+ };
1551
+ var AddressTransportAdapter = class {
1552
+ id;
1553
+ name;
1554
+ type = "p2p";
1555
+ description;
1556
+ mux;
1557
+ addressIndex;
1558
+ identity;
1559
+ resolveDelegate;
1560
+ // Per-address handler sets
1561
+ messageHandlers = /* @__PURE__ */ new Set();
1562
+ transferHandlers = /* @__PURE__ */ new Set();
1563
+ paymentRequestHandlers = /* @__PURE__ */ new Set();
1564
+ paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1565
+ readReceiptHandlers = /* @__PURE__ */ new Set();
1566
+ typingIndicatorHandlers = /* @__PURE__ */ new Set();
1567
+ composingHandlers = /* @__PURE__ */ new Set();
1568
+ instantSplitBundleHandlers = /* @__PURE__ */ new Set();
1569
+ broadcastHandlers = /* @__PURE__ */ new Map();
1570
+ eventCallbacks = /* @__PURE__ */ new Set();
1571
+ pendingMessages = [];
1572
+ chatEoseHandlers = [];
1573
+ constructor(mux, addressIndex, identity, resolveDelegate) {
1574
+ this.mux = mux;
1575
+ this.addressIndex = addressIndex;
1576
+ this.identity = identity;
1577
+ this.resolveDelegate = resolveDelegate ?? null;
1578
+ this.id = `nostr-addr-${addressIndex}`;
1579
+ this.name = `Nostr Transport (address ${addressIndex})`;
1580
+ this.description = `P2P messaging for address index ${addressIndex}`;
1581
+ }
1582
+ // ===========================================================================
1583
+ // BaseProvider — delegates to mux
1584
+ // ===========================================================================
1585
+ async connect() {
1586
+ await this.mux.connect();
1587
+ }
1588
+ async disconnect() {
1589
+ }
1590
+ isConnected() {
1591
+ return this.mux.isConnected();
1592
+ }
1593
+ getStatus() {
1594
+ return this.mux.getStatus();
1595
+ }
1596
+ // ===========================================================================
1597
+ // Identity (no-op — mux manages identity via addAddress)
1598
+ // ===========================================================================
1599
+ async setIdentity(identity) {
1600
+ this.identity = identity;
1601
+ await this.mux.addAddress(this.addressIndex, identity);
1602
+ }
1603
+ // ===========================================================================
1604
+ // Sending — delegates to mux with this address's keyManager
1605
+ // ===========================================================================
1606
+ async sendMessage(recipientPubkey, content) {
1607
+ const senderNametag = this.identity.nametag;
1608
+ const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1609
+ return this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, wrappedContent);
1610
+ }
1611
+ async sendTokenTransfer(recipientPubkey, payload) {
1612
+ const content = "token_transfer:" + JSON.stringify(payload);
1613
+ const uniqueD = `token-transfer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1614
+ return this.mux.createAndPublishEncryptedEvent(
1615
+ this.addressIndex,
1616
+ EVENT_KINDS.TOKEN_TRANSFER,
1617
+ content,
1618
+ [["p", recipientPubkey], ["d", uniqueD], ["type", "token_transfer"]]
1619
+ );
1620
+ }
1621
+ async sendPaymentRequest(recipientPubkey, payload) {
1622
+ const requestId2 = this.mux.getUUIDGenerator()();
1623
+ const amount = typeof payload.amount === "bigint" ? payload.amount.toString() : payload.amount;
1624
+ const requestContent = {
1625
+ requestId: requestId2,
1626
+ amount,
1627
+ coinId: payload.coinId,
1628
+ message: payload.message,
1629
+ recipientNametag: payload.recipientNametag,
1630
+ deadline: Date.now() + 5 * 60 * 1e3
1631
+ };
1632
+ const content = "payment_request:" + JSON.stringify(requestContent);
1633
+ const tags = [
1634
+ ["p", recipientPubkey],
1635
+ ["type", "payment_request"],
1636
+ ["amount", amount]
1637
+ ];
1638
+ if (payload.recipientNametag) {
1639
+ tags.push(["recipient", payload.recipientNametag]);
1640
+ }
1641
+ return this.mux.createAndPublishEncryptedEvent(
1642
+ this.addressIndex,
1643
+ EVENT_KINDS.PAYMENT_REQUEST,
1644
+ content,
1645
+ tags
1646
+ );
1647
+ }
1648
+ async sendPaymentRequestResponse(recipientPubkey, response) {
1649
+ const content = "payment_response:" + JSON.stringify(response);
1650
+ return this.mux.createAndPublishEncryptedEvent(
1651
+ this.addressIndex,
1652
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE,
1653
+ content,
1654
+ [["p", recipientPubkey], ["type", "payment_response"]]
1655
+ );
1656
+ }
1657
+ async sendReadReceipt(recipientPubkey, messageEventId) {
1658
+ const content = JSON.stringify({ type: "read_receipt", messageEventId });
1659
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1660
+ }
1661
+ async sendTypingIndicator(recipientPubkey) {
1662
+ const content = JSON.stringify({
1663
+ type: "typing",
1664
+ senderNametag: this.identity.nametag
1665
+ });
1666
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1667
+ }
1668
+ async sendComposingIndicator(recipientPubkey, content) {
1669
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1670
+ }
1671
+ async sendInstantSplitBundle(recipientPubkey, bundle) {
1672
+ const content = "token_transfer:" + JSON.stringify({
1673
+ type: "instant_split",
1674
+ ...bundle
1675
+ });
1676
+ const uniqueD = `instant-split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1677
+ return this.mux.createAndPublishEncryptedEvent(
1678
+ this.addressIndex,
1679
+ EVENT_KINDS.TOKEN_TRANSFER,
1680
+ content,
1681
+ [["p", recipientPubkey], ["d", uniqueD], ["type", "instant_split"]]
1682
+ );
1683
+ }
1684
+ // ===========================================================================
1685
+ // Subscription handlers — per-address
1686
+ // ===========================================================================
1687
+ onMessage(handler) {
1688
+ this.messageHandlers.add(handler);
1689
+ if (this.pendingMessages.length > 0) {
1690
+ const pending2 = this.pendingMessages;
1691
+ this.pendingMessages = [];
1692
+ for (const msg of pending2) {
1693
+ try {
1694
+ handler(msg);
1695
+ } catch {
1696
+ }
1697
+ }
1698
+ }
1699
+ return () => this.messageHandlers.delete(handler);
1700
+ }
1701
+ onTokenTransfer(handler) {
1702
+ this.transferHandlers.add(handler);
1703
+ return () => this.transferHandlers.delete(handler);
1704
+ }
1705
+ onPaymentRequest(handler) {
1706
+ this.paymentRequestHandlers.add(handler);
1707
+ return () => this.paymentRequestHandlers.delete(handler);
1708
+ }
1709
+ onPaymentRequestResponse(handler) {
1710
+ this.paymentRequestResponseHandlers.add(handler);
1711
+ return () => this.paymentRequestResponseHandlers.delete(handler);
1712
+ }
1713
+ onReadReceipt(handler) {
1714
+ this.readReceiptHandlers.add(handler);
1715
+ return () => this.readReceiptHandlers.delete(handler);
1716
+ }
1717
+ onTypingIndicator(handler) {
1718
+ this.typingIndicatorHandlers.add(handler);
1719
+ return () => this.typingIndicatorHandlers.delete(handler);
1720
+ }
1721
+ onComposing(handler) {
1722
+ this.composingHandlers.add(handler);
1723
+ return () => this.composingHandlers.delete(handler);
1724
+ }
1725
+ onInstantSplitReceived(handler) {
1726
+ this.instantSplitBundleHandlers.add(handler);
1727
+ return () => this.instantSplitBundleHandlers.delete(handler);
1728
+ }
1729
+ subscribeToBroadcast(tags, handler) {
1730
+ const key = tags.sort().join(":");
1731
+ if (!this.broadcastHandlers.has(key)) {
1732
+ this.broadcastHandlers.set(key, /* @__PURE__ */ new Set());
1733
+ }
1734
+ this.broadcastHandlers.get(key).add(handler);
1735
+ return () => this.broadcastHandlers.get(key)?.delete(handler);
1736
+ }
1737
+ async publishBroadcast(content, tags) {
1738
+ const eventTags = tags ? tags.map((t) => ["t", t]) : [];
1739
+ return this.mux.publishRawEvent(this.addressIndex, 30023, content, eventTags);
1740
+ }
1741
+ // ===========================================================================
1742
+ // Resolve methods — delegate to original NostrTransportProvider
1743
+ // These are stateless relay queries, shared across all addresses
1744
+ // ===========================================================================
1745
+ async resolve(identifier) {
1746
+ return this.resolveDelegate?.resolve?.(identifier) ?? null;
1747
+ }
1748
+ async resolveNametag(nametag) {
1749
+ return this.resolveDelegate?.resolveNametag?.(nametag) ?? null;
1750
+ }
1751
+ async resolveNametagInfo(nametag) {
1752
+ return this.resolveDelegate?.resolveNametagInfo?.(nametag) ?? null;
1753
+ }
1754
+ async resolveAddressInfo(address) {
1755
+ return this.resolveDelegate?.resolveAddressInfo?.(address) ?? null;
1756
+ }
1757
+ async resolveTransportPubkeyInfo(transportPubkey) {
1758
+ return this.resolveDelegate?.resolveTransportPubkeyInfo?.(transportPubkey) ?? null;
1759
+ }
1760
+ async discoverAddresses(transportPubkeys) {
1761
+ return this.resolveDelegate?.discoverAddresses?.(transportPubkeys) ?? [];
1762
+ }
1763
+ async recoverNametag() {
1764
+ return this.resolveDelegate?.recoverNametag?.() ?? null;
1765
+ }
1766
+ async publishIdentityBinding(chainPubkey, l1Address, directAddress, nametag) {
1767
+ return this.resolveDelegate?.publishIdentityBinding?.(chainPubkey, l1Address, directAddress, nametag) ?? false;
1768
+ }
1769
+ // ===========================================================================
1770
+ // Relay Management — delegates to mux
1771
+ // ===========================================================================
1772
+ getRelays() {
1773
+ return this.mux.getRelays();
1774
+ }
1775
+ getConnectedRelays() {
1776
+ return this.mux.getConnectedRelays();
1777
+ }
1778
+ async addRelay(relayUrl) {
1779
+ return this.mux.addRelay(relayUrl);
1780
+ }
1781
+ async removeRelay(relayUrl) {
1782
+ return this.mux.removeRelay(relayUrl);
1783
+ }
1784
+ hasRelay(relayUrl) {
1785
+ return this.mux.hasRelay(relayUrl);
1786
+ }
1787
+ isRelayConnected(relayUrl) {
1788
+ return this.mux.isRelayConnected(relayUrl);
1789
+ }
1790
+ setFallbackSince(sinceSeconds) {
1791
+ this.mux.setFallbackSince(this.addressIndex, sinceSeconds);
1792
+ }
1793
+ async fetchPendingEvents() {
1794
+ }
1795
+ onChatReady(handler) {
1796
+ return this.mux.onChatReady(handler);
1797
+ }
1798
+ // ===========================================================================
1799
+ // Dispatch methods — called by MultiAddressTransportMux to route events
1800
+ // ===========================================================================
1801
+ dispatchMessage(message) {
1802
+ if (this.messageHandlers.size === 0) {
1803
+ this.pendingMessages.push(message);
1804
+ return;
1805
+ }
1806
+ for (const handler of this.messageHandlers) {
1807
+ try {
1808
+ handler(message);
1809
+ } catch (e) {
1810
+ logger.debug("MuxAdapter", "Message handler error:", e);
1811
+ }
1812
+ }
1813
+ }
1814
+ dispatchTokenTransfer(transfer) {
1815
+ for (const handler of this.transferHandlers) {
1816
+ try {
1817
+ handler(transfer);
1818
+ } catch (e) {
1819
+ logger.debug("MuxAdapter", "Transfer handler error:", e);
1820
+ }
1821
+ }
1822
+ }
1823
+ dispatchPaymentRequest(request) {
1824
+ for (const handler of this.paymentRequestHandlers) {
1825
+ try {
1826
+ handler(request);
1827
+ } catch (e) {
1828
+ logger.debug("MuxAdapter", "Payment request handler error:", e);
1829
+ }
1830
+ }
1831
+ }
1832
+ dispatchPaymentRequestResponse(response) {
1833
+ for (const handler of this.paymentRequestResponseHandlers) {
1834
+ try {
1835
+ handler(response);
1836
+ } catch (e) {
1837
+ logger.debug("MuxAdapter", "Payment response handler error:", e);
1838
+ }
1839
+ }
1840
+ }
1841
+ dispatchReadReceipt(receipt) {
1842
+ for (const handler of this.readReceiptHandlers) {
1843
+ try {
1844
+ handler(receipt);
1845
+ } catch (e) {
1846
+ logger.debug("MuxAdapter", "Read receipt handler error:", e);
1847
+ }
1848
+ }
1849
+ }
1850
+ dispatchTypingIndicator(indicator) {
1851
+ for (const handler of this.typingIndicatorHandlers) {
1852
+ try {
1853
+ handler(indicator);
1854
+ } catch (e) {
1855
+ logger.debug("MuxAdapter", "Typing handler error:", e);
1856
+ }
1857
+ }
1858
+ }
1859
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1860
+ dispatchComposingIndicator(indicator) {
1861
+ for (const handler of this.composingHandlers) {
1862
+ try {
1863
+ handler(indicator);
1864
+ } catch (e) {
1865
+ logger.debug("MuxAdapter", "Composing handler error:", e);
1866
+ }
1867
+ }
1868
+ }
1869
+ dispatchInstantSplitBundle(bundle) {
1870
+ for (const handler of this.instantSplitBundleHandlers) {
1871
+ try {
1872
+ handler(bundle);
1873
+ } catch (e) {
1874
+ logger.debug("MuxAdapter", "Instant split handler error:", e);
1875
+ }
1876
+ }
1877
+ }
1878
+ emitTransportEvent(event) {
1879
+ for (const cb of this.eventCallbacks) {
1880
+ try {
1881
+ cb(event);
1882
+ } catch {
1883
+ }
1884
+ }
1885
+ }
1886
+ };
1887
+
756
1888
  // modules/payments/L1PaymentsModule.ts
757
1889
  init_errors();
758
1890
  init_constants();
@@ -4616,6 +5748,15 @@ var PaymentsModule = class _PaymentsModule {
4616
5748
  this.unsubscribePaymentRequests = null;
4617
5749
  this.unsubscribePaymentRequestResponses?.();
4618
5750
  this.unsubscribePaymentRequestResponses = null;
5751
+ this.stopProofPolling();
5752
+ this.proofPollingJobs.clear();
5753
+ this.stopResolveUnconfirmedPolling();
5754
+ this.unsubscribeStorageEvents();
5755
+ for (const [, resolver] of this.pendingResponseResolvers) {
5756
+ clearTimeout(resolver.timeout);
5757
+ resolver.reject(new Error("Address switched"));
5758
+ }
5759
+ this.pendingResponseResolvers.clear();
4619
5760
  this.tokens.clear();
4620
5761
  this.pendingTransfers.clear();
4621
5762
  this.tombstones = [];
@@ -4664,6 +5805,13 @@ var PaymentsModule = class _PaymentsModule {
4664
5805
  try {
4665
5806
  const result = await provider.load();
4666
5807
  if (result.success && result.data) {
5808
+ const loadedMeta = result.data?._meta;
5809
+ const currentL1 = this.deps.identity.l1Address;
5810
+ const currentChain = this.deps.identity.chainPubkey;
5811
+ if (loadedMeta?.address && currentL1 && loadedMeta.address !== currentL1 && loadedMeta.address !== currentChain) {
5812
+ logger.warn("Payments", `Load: rejecting data from provider ${id} \u2014 address mismatch (got=${loadedMeta.address.slice(0, 20)}... expected=${currentL1.slice(0, 20)}...)`);
5813
+ continue;
5814
+ }
4667
5815
  this.loadFromStorageData(result.data);
4668
5816
  const txfData = result.data;
4669
5817
  if (txfData._history && txfData._history.length > 0) {
@@ -4745,6 +5893,11 @@ var PaymentsModule = class _PaymentsModule {
4745
5893
  */
4746
5894
  async send(request) {
4747
5895
  this.ensureInitialized();
5896
+ let resolveSendTracker;
5897
+ const sendTracker = new Promise((r) => {
5898
+ resolveSendTracker = r;
5899
+ });
5900
+ this.pendingBackgroundTasks.push(sendTracker);
4748
5901
  const result = {
4749
5902
  id: crypto.randomUUID(),
4750
5903
  status: "pending",
@@ -5032,6 +6185,8 @@ var PaymentsModule = class _PaymentsModule {
5032
6185
  }
5033
6186
  this.deps.emitEvent("transfer:failed", result);
5034
6187
  throw error;
6188
+ } finally {
6189
+ resolveSendTracker();
5035
6190
  }
5036
6191
  }
5037
6192
  /**
@@ -6044,9 +7199,12 @@ var PaymentsModule = class _PaymentsModule {
6044
7199
  * Call this before process exit to ensure all tokens are saved.
6045
7200
  */
6046
7201
  async waitForPendingOperations() {
7202
+ logger.debug("Payments", `waitForPendingOperations: ${this.pendingBackgroundTasks.length} pending tasks`);
6047
7203
  if (this.pendingBackgroundTasks.length > 0) {
7204
+ logger.debug("Payments", "waitForPendingOperations: waiting...");
6048
7205
  await Promise.allSettled(this.pendingBackgroundTasks);
6049
7206
  this.pendingBackgroundTasks = [];
7207
+ logger.debug("Payments", "waitForPendingOperations: all tasks completed");
6050
7208
  }
6051
7209
  }
6052
7210
  /**
@@ -7288,6 +8446,13 @@ var PaymentsModule = class _PaymentsModule {
7288
8446
  try {
7289
8447
  const result = await provider.sync(localData);
7290
8448
  if (result.success && result.merged) {
8449
+ const mergedMeta = result.merged?._meta;
8450
+ const currentL1 = this.deps.identity.l1Address;
8451
+ const currentChain = this.deps.identity.chainPubkey;
8452
+ if (mergedMeta?.address && currentL1 && mergedMeta.address !== currentL1 && mergedMeta.address !== currentChain) {
8453
+ logger.warn("Payments", `Sync: rejecting data from provider ${providerId} \u2014 address mismatch (got=${mergedMeta.address.slice(0, 20)}... expected=${currentL1.slice(0, 20)}...)`);
8454
+ continue;
8455
+ }
7291
8456
  const savedTokens = new Map(this.tokens);
7292
8457
  this.loadFromStorageData(result.merged);
7293
8458
  let restoredCount = 0;
@@ -8299,6 +9464,12 @@ var CommunicationsModule = class {
8299
9464
  this.unsubscribeComposing = deps.transport.onComposing?.((indicator) => {
8300
9465
  this.handleComposingIndicator(indicator);
8301
9466
  }) ?? null;
9467
+ if (deps.transport.onChatReady) {
9468
+ deps.transport.onChatReady(() => {
9469
+ const conversations = this.getConversations();
9470
+ deps.emitEvent("communications:ready", { conversationCount: conversations.size });
9471
+ });
9472
+ }
8302
9473
  }
8303
9474
  /**
8304
9475
  * Load messages from storage.
@@ -8714,9 +9885,9 @@ init_logger();
8714
9885
  init_errors();
8715
9886
  init_constants();
8716
9887
  import {
8717
- NostrClient,
8718
- NostrKeyManager,
8719
- Filter
9888
+ NostrClient as NostrClient2,
9889
+ NostrKeyManager as NostrKeyManager2,
9890
+ Filter as Filter2
8720
9891
  } from "@unicitylabs/nostr-js-sdk";
8721
9892
 
8722
9893
  // modules/groupchat/types.ts
@@ -8732,7 +9903,7 @@ var GroupVisibility = {
8732
9903
 
8733
9904
  // modules/groupchat/GroupChatModule.ts
8734
9905
  function createNip29Filter(data) {
8735
- return new Filter(data);
9906
+ return new Filter2(data);
8736
9907
  }
8737
9908
  var GroupChatModule = class {
8738
9909
  config;
@@ -8781,7 +9952,7 @@ var GroupChatModule = class {
8781
9952
  }
8782
9953
  this.deps = deps;
8783
9954
  const secretKey = Buffer.from(deps.identity.privateKey, "hex");
8784
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
9955
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8785
9956
  }
8786
9957
  async load() {
8787
9958
  this.ensureInitialized();
@@ -8916,7 +10087,7 @@ var GroupChatModule = class {
8916
10087
  }
8917
10088
  this.subscriptionIds = [];
8918
10089
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
8919
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
10090
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8920
10091
  if (this.groups.size === 0) {
8921
10092
  await this.restoreJoinedGroups();
8922
10093
  } else {
@@ -8928,13 +10099,13 @@ var GroupChatModule = class {
8928
10099
  this.ensureInitialized();
8929
10100
  if (!this.keyManager) {
8930
10101
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
8931
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
10102
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8932
10103
  }
8933
10104
  const primaryRelay = this.config.relays[0];
8934
10105
  if (primaryRelay) {
8935
10106
  await this.checkAndClearOnRelayChange(primaryRelay);
8936
10107
  }
8937
- this.client = new NostrClient(this.keyManager);
10108
+ this.client = new NostrClient2(this.keyManager);
8938
10109
  try {
8939
10110
  await this.client.connect(...this.config.relays);
8940
10111
  this.connected = true;
@@ -8945,6 +10116,7 @@ var GroupChatModule = class {
8945
10116
  await this.subscribeToJoinedGroups();
8946
10117
  }
8947
10118
  this.deps.emitEvent("groupchat:connection", { connected: true });
10119
+ this.deps.emitEvent("groupchat:ready", { groupCount: this.groups.size });
8948
10120
  } catch (error) {
8949
10121
  logger.error("GroupChat", "Failed to connect to relays", error);
8950
10122
  this.deps.emitEvent("groupchat:connection", { connected: false });
@@ -9192,7 +10364,7 @@ var GroupChatModule = class {
9192
10364
  if (!myPubkey) return [];
9193
10365
  const groupIdsWithMembership = /* @__PURE__ */ new Set();
9194
10366
  await this.oneshotSubscription(
9195
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10367
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9196
10368
  {
9197
10369
  onEvent: (event) => {
9198
10370
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9243,7 +10415,7 @@ var GroupChatModule = class {
9243
10415
  const memberCountsMap = /* @__PURE__ */ new Map();
9244
10416
  await Promise.all([
9245
10417
  this.oneshotSubscription(
9246
- new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
10418
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
9247
10419
  {
9248
10420
  onEvent: (event) => {
9249
10421
  const group = this.parseGroupMetadata(event);
@@ -9261,7 +10433,7 @@ var GroupChatModule = class {
9261
10433
  }
9262
10434
  ),
9263
10435
  this.oneshotSubscription(
9264
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10436
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9265
10437
  {
9266
10438
  onEvent: (event) => {
9267
10439
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9579,6 +10751,19 @@ var GroupChatModule = class {
9579
10751
  getMessages(groupId) {
9580
10752
  return (this.messages.get(groupId) || []).sort((a, b) => a.timestamp - b.timestamp);
9581
10753
  }
10754
+ getMessagesPage(groupId, options) {
10755
+ const limit = options?.limit ?? 20;
10756
+ const before = options?.before ?? Infinity;
10757
+ const groupMessages = this.messages.get(groupId) ?? [];
10758
+ const filtered = groupMessages.filter((m) => m.timestamp < before).sort((a, b) => b.timestamp - a.timestamp);
10759
+ const page = filtered.slice(0, limit);
10760
+ return {
10761
+ messages: page.reverse(),
10762
+ // chronological order
10763
+ hasMore: filtered.length > limit,
10764
+ oldestTimestamp: page.length > 0 ? page[0].timestamp : null
10765
+ };
10766
+ }
9582
10767
  getMembers(groupId) {
9583
10768
  return (this.members.get(groupId) || []).sort((a, b) => a.joinedAt - b.joinedAt);
9584
10769
  }
@@ -9751,7 +10936,7 @@ var GroupChatModule = class {
9751
10936
  if (!this.client) return /* @__PURE__ */ new Set();
9752
10937
  const adminPubkeys = /* @__PURE__ */ new Set();
9753
10938
  return this.oneshotSubscription(
9754
- new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
10939
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
9755
10940
  {
9756
10941
  onEvent: (event) => {
9757
10942
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9773,7 +10958,7 @@ var GroupChatModule = class {
9773
10958
  if (!this.client) return null;
9774
10959
  let result = null;
9775
10960
  return this.oneshotSubscription(
9776
- new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
10961
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
9777
10962
  {
9778
10963
  onEvent: (event) => {
9779
10964
  if (!result) result = this.parseGroupMetadata(event);
@@ -9810,7 +10995,7 @@ var GroupChatModule = class {
9810
10995
  if (!this.client) return [];
9811
10996
  const members = [];
9812
10997
  return this.oneshotSubscription(
9813
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
10998
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
9814
10999
  {
9815
11000
  onEvent: (event) => {
9816
11001
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9831,7 +11016,7 @@ var GroupChatModule = class {
9831
11016
  if (!this.client) return [];
9832
11017
  const adminPubkeys = [];
9833
11018
  return this.oneshotSubscription(
9834
- new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
11019
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
9835
11020
  {
9836
11021
  onEvent: (event) => {
9837
11022
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -13631,11 +14816,18 @@ var Sphere = class _Sphere {
13631
14816
  _transport;
13632
14817
  _oracle;
13633
14818
  _priceProvider;
13634
- // Modules
14819
+ // Modules (single-instance — backward compat, delegates to active address)
13635
14820
  _payments;
13636
14821
  _communications;
13637
14822
  _groupChat = null;
13638
14823
  _market = null;
14824
+ // Per-address module instances (Phase 2: independent parallel operation)
14825
+ _addressModules = /* @__PURE__ */ new Map();
14826
+ _transportMux = null;
14827
+ // Stored configs for creating per-address modules
14828
+ _l1Config;
14829
+ _groupChatConfig;
14830
+ _marketConfig;
13639
14831
  // Events
13640
14832
  eventHandlers = /* @__PURE__ */ new Map();
13641
14833
  // Provider management
@@ -13653,6 +14845,9 @@ var Sphere = class _Sphere {
13653
14845
  if (tokenStorage) {
13654
14846
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
13655
14847
  }
14848
+ this._l1Config = l1Config;
14849
+ this._groupChatConfig = groupChatConfig;
14850
+ this._marketConfig = marketConfig;
13656
14851
  this._payments = createPaymentsModule({ l1: l1Config });
13657
14852
  this._communications = createCommunicationsModule();
13658
14853
  this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
@@ -14911,7 +16106,7 @@ var Sphere = class _Sphere {
14911
16106
  nametags.set(0, newNametag);
14912
16107
  }
14913
16108
  const nametag = this._addressNametags.get(addressId)?.get(0);
14914
- this._identity = {
16109
+ const newIdentity = {
14915
16110
  privateKey: addressInfo.privateKey,
14916
16111
  chainPubkey: addressInfo.publicKey,
14917
16112
  l1Address: addressInfo.address,
@@ -14919,20 +16114,53 @@ var Sphere = class _Sphere {
14919
16114
  ipnsName: "12D3KooW" + ipnsHash,
14920
16115
  nametag
14921
16116
  };
16117
+ if (!this._addressModules.has(index)) {
16118
+ logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
16119
+ const addressTokenProviders = /* @__PURE__ */ new Map();
16120
+ for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
16121
+ if (provider.createForAddress) {
16122
+ const newProvider = provider.createForAddress();
16123
+ newProvider.setIdentity(newIdentity);
16124
+ await newProvider.initialize();
16125
+ addressTokenProviders.set(providerId, newProvider);
16126
+ } else {
16127
+ logger.warn("Sphere", `Token storage provider ${providerId} does not support createForAddress, reusing shared instance`);
16128
+ addressTokenProviders.set(providerId, provider);
16129
+ }
16130
+ }
16131
+ await this.initializeAddressModules(index, newIdentity, addressTokenProviders);
16132
+ } else {
16133
+ const moduleSet = this._addressModules.get(index);
16134
+ if (nametag !== moduleSet.identity.nametag) {
16135
+ moduleSet.identity = newIdentity;
16136
+ const addressTransport = moduleSet.transportAdapter ?? this._transport;
16137
+ moduleSet.payments.initialize({
16138
+ identity: newIdentity,
16139
+ storage: this._storage,
16140
+ tokenStorageProviders: moduleSet.tokenStorageProviders,
16141
+ transport: addressTransport,
16142
+ oracle: this._oracle,
16143
+ emitEvent: this.emitEvent.bind(this),
16144
+ chainCode: this._masterKey?.chainCode || void 0,
16145
+ price: this._priceProvider ?? void 0
16146
+ });
16147
+ }
16148
+ }
16149
+ this._identity = newIdentity;
14922
16150
  this._currentAddressIndex = index;
14923
16151
  await this._updateCachedProxyAddress();
16152
+ const activeModules = this._addressModules.get(index);
16153
+ this._payments = activeModules.payments;
16154
+ this._communications = activeModules.communications;
16155
+ this._groupChat = activeModules.groupChat;
16156
+ this._market = activeModules.market;
14924
16157
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
14925
16158
  this._storage.setIdentity(this._identity);
14926
- await this._transport.setIdentity(this._identity);
14927
- logger.debug("Sphere", `switchToAddress(${index}): re-initializing ${this._tokenStorageProviders.size} token storage provider(s)`);
14928
- for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
14929
- logger.debug("Sphere", `switchToAddress(${index}): shutdown provider=${providerId}`);
14930
- await provider.shutdown();
14931
- provider.setIdentity(this._identity);
14932
- logger.debug("Sphere", `switchToAddress(${index}): initialize provider=${providerId}`);
14933
- await provider.initialize();
16159
+ if (this._transport.setFallbackSince) {
16160
+ const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
16161
+ this._transport.setFallbackSince(fallbackTs);
14934
16162
  }
14935
- await this.reinitializeModulesForNewAddress();
16163
+ await this._transport.setIdentity(this._identity);
14936
16164
  this.emitEvent("identity:changed", {
14937
16165
  l1Address: this._identity.l1Address,
14938
16166
  directAddress: this._identity.directAddress,
@@ -14987,42 +16215,104 @@ var Sphere = class _Sphere {
14987
16215
  }
14988
16216
  }
14989
16217
  /**
14990
- * Re-initialize modules after address switch
16218
+ * Create a new set of per-address modules for the given index.
16219
+ * Each address gets its own PaymentsModule, CommunicationsModule, etc.
16220
+ * Modules are fully independent — they have their own token storage,
16221
+ * and can sync/finalize/split in background regardless of active address.
16222
+ *
16223
+ * @param index - HD address index
16224
+ * @param identity - Full identity for this address
16225
+ * @param tokenStorageProviders - Token storage providers for this address
14991
16226
  */
14992
- async reinitializeModulesForNewAddress() {
16227
+ async initializeAddressModules(index, identity, tokenStorageProviders) {
14993
16228
  const emitEvent = this.emitEvent.bind(this);
14994
- this._payments.initialize({
14995
- identity: this._identity,
16229
+ const adapter = await this.ensureTransportMux(index, identity);
16230
+ const addressTransport = adapter ?? this._transport;
16231
+ const payments = createPaymentsModule({ l1: this._l1Config });
16232
+ const communications = createCommunicationsModule();
16233
+ const groupChat = this._groupChatConfig ? createGroupChatModule(this._groupChatConfig) : null;
16234
+ const market = this._marketConfig ? createMarketModule(this._marketConfig) : null;
16235
+ payments.initialize({
16236
+ identity,
14996
16237
  storage: this._storage,
14997
- tokenStorageProviders: this._tokenStorageProviders,
14998
- transport: this._transport,
16238
+ tokenStorageProviders,
16239
+ transport: addressTransport,
14999
16240
  oracle: this._oracle,
15000
16241
  emitEvent,
15001
16242
  chainCode: this._masterKey?.chainCode || void 0,
15002
16243
  price: this._priceProvider ?? void 0
15003
16244
  });
15004
- this._communications.initialize({
15005
- identity: this._identity,
16245
+ communications.initialize({
16246
+ identity,
15006
16247
  storage: this._storage,
15007
- transport: this._transport,
16248
+ transport: addressTransport,
15008
16249
  emitEvent
15009
16250
  });
15010
- this._groupChat?.initialize({
15011
- identity: this._identity,
16251
+ groupChat?.initialize({
16252
+ identity,
15012
16253
  storage: this._storage,
15013
16254
  emitEvent
15014
16255
  });
15015
- this._market?.initialize({
15016
- identity: this._identity,
16256
+ market?.initialize({
16257
+ identity,
15017
16258
  emitEvent
15018
16259
  });
15019
- await this._payments.load();
15020
- await this._communications.load();
15021
- await this._groupChat?.load();
15022
- await this._market?.load();
15023
- this._payments.sync().catch((err) => {
15024
- logger.warn("Sphere", "Post-switch sync failed:", err);
16260
+ await payments.load();
16261
+ await communications.load();
16262
+ await groupChat?.load();
16263
+ await market?.load();
16264
+ const moduleSet = {
16265
+ index,
16266
+ identity,
16267
+ payments,
16268
+ communications,
16269
+ groupChat,
16270
+ market,
16271
+ transportAdapter: adapter,
16272
+ tokenStorageProviders: new Map(tokenStorageProviders),
16273
+ initialized: true
16274
+ };
16275
+ this._addressModules.set(index, moduleSet);
16276
+ logger.debug("Sphere", `Initialized per-address modules for address ${index} (transport: ${adapter ? "mux adapter" : "primary"})`);
16277
+ payments.sync().catch((err) => {
16278
+ logger.warn("Sphere", `Post-init sync failed for address ${index}:`, err);
15025
16279
  });
16280
+ return moduleSet;
16281
+ }
16282
+ /**
16283
+ * Ensure the transport multiplexer exists and register an address.
16284
+ * Creates the mux on first call. Returns an AddressTransportAdapter
16285
+ * that routes events for this address independently.
16286
+ * @returns AddressTransportAdapter or null if transport is not Nostr-based
16287
+ */
16288
+ async ensureTransportMux(index, identity) {
16289
+ const transport = this._transport;
16290
+ if (typeof transport.getWebSocketFactory !== "function" || typeof transport.getConfiguredRelays !== "function") {
16291
+ logger.debug("Sphere", "Transport does not support mux interface, skipping");
16292
+ return null;
16293
+ }
16294
+ const nostrTransport = transport;
16295
+ if (!this._transportMux) {
16296
+ this._transportMux = new MultiAddressTransportMux({
16297
+ relays: nostrTransport.getConfiguredRelays(),
16298
+ createWebSocket: nostrTransport.getWebSocketFactory(),
16299
+ storage: nostrTransport.getStorageAdapter() ?? void 0
16300
+ });
16301
+ await this._transportMux.connect();
16302
+ if (typeof nostrTransport.suppressSubscriptions === "function") {
16303
+ nostrTransport.suppressSubscriptions();
16304
+ }
16305
+ logger.debug("Sphere", "Transport mux created and connected");
16306
+ }
16307
+ const adapter = await this._transportMux.addAddress(index, identity, this._transport);
16308
+ return adapter;
16309
+ }
16310
+ /**
16311
+ * Get per-address modules for any address index (creates lazily if needed).
16312
+ * This allows accessing any address's modules without switching.
16313
+ */
16314
+ getAddressPayments(index) {
16315
+ return this._addressModules.get(index)?.payments;
15026
16316
  }
15027
16317
  /**
15028
16318
  * Derive address at a specific index
@@ -15950,10 +17240,33 @@ var Sphere = class _Sphere {
15950
17240
  // ===========================================================================
15951
17241
  async destroy() {
15952
17242
  this.cleanupProviderEventSubscriptions();
17243
+ for (const [idx, moduleSet] of this._addressModules.entries()) {
17244
+ try {
17245
+ moduleSet.payments.destroy();
17246
+ moduleSet.communications.destroy();
17247
+ moduleSet.groupChat?.destroy();
17248
+ moduleSet.market?.destroy();
17249
+ for (const provider of moduleSet.tokenStorageProviders.values()) {
17250
+ try {
17251
+ await provider.shutdown();
17252
+ } catch {
17253
+ }
17254
+ }
17255
+ moduleSet.tokenStorageProviders.clear();
17256
+ logger.debug("Sphere", `Destroyed modules for address ${idx}`);
17257
+ } catch (err) {
17258
+ logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
17259
+ }
17260
+ }
17261
+ this._addressModules.clear();
15953
17262
  this._payments.destroy();
15954
17263
  this._communications.destroy();
15955
17264
  this._groupChat?.destroy();
15956
17265
  this._market?.destroy();
17266
+ if (this._transportMux) {
17267
+ await this._transportMux.disconnect();
17268
+ this._transportMux = null;
17269
+ }
15957
17270
  await this._transport.disconnect();
15958
17271
  await this._storage.disconnect();
15959
17272
  await this._oracle.disconnect();
@@ -16148,6 +17461,9 @@ var Sphere = class _Sphere {
16148
17461
  // ===========================================================================
16149
17462
  async initializeProviders() {
16150
17463
  this._storage.setIdentity(this._identity);
17464
+ if (this._transport.setFallbackSince) {
17465
+ this._transport.setFallbackSince(Math.floor(Date.now() / 1e3) - 86400);
17466
+ }
16151
17467
  await this._transport.setIdentity(this._identity);
16152
17468
  for (const provider of this._tokenStorageProviders.values()) {
16153
17469
  provider.setIdentity(this._identity);
@@ -16238,11 +17554,13 @@ var Sphere = class _Sphere {
16238
17554
  }
16239
17555
  async initializeModules() {
16240
17556
  const emitEvent = this.emitEvent.bind(this);
17557
+ const adapter = await this.ensureTransportMux(this._currentAddressIndex, this._identity);
17558
+ const moduleTransport = adapter ?? this._transport;
16241
17559
  this._payments.initialize({
16242
17560
  identity: this._identity,
16243
17561
  storage: this._storage,
16244
17562
  tokenStorageProviders: this._tokenStorageProviders,
16245
- transport: this._transport,
17563
+ transport: moduleTransport,
16246
17564
  oracle: this._oracle,
16247
17565
  emitEvent,
16248
17566
  // Pass chain code for L1 HD derivation
@@ -16253,7 +17571,7 @@ var Sphere = class _Sphere {
16253
17571
  this._communications.initialize({
16254
17572
  identity: this._identity,
16255
17573
  storage: this._storage,
16256
- transport: this._transport,
17574
+ transport: moduleTransport,
16257
17575
  emitEvent
16258
17576
  });
16259
17577
  this._groupChat?.initialize({
@@ -16269,6 +17587,17 @@ var Sphere = class _Sphere {
16269
17587
  await this._communications.load();
16270
17588
  await this._groupChat?.load();
16271
17589
  await this._market?.load();
17590
+ this._addressModules.set(this._currentAddressIndex, {
17591
+ index: this._currentAddressIndex,
17592
+ identity: this._identity,
17593
+ payments: this._payments,
17594
+ communications: this._communications,
17595
+ groupChat: this._groupChat,
17596
+ market: this._market,
17597
+ transportAdapter: adapter,
17598
+ tokenStorageProviders: new Map(this._tokenStorageProviders),
17599
+ initialized: true
17600
+ });
16272
17601
  }
16273
17602
  // ===========================================================================
16274
17603
  // Private: Helpers