@unicitylabs/sphere-sdk 0.6.2 → 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;
@@ -8720,9 +9885,9 @@ init_logger();
8720
9885
  init_errors();
8721
9886
  init_constants();
8722
9887
  import {
8723
- NostrClient,
8724
- NostrKeyManager,
8725
- Filter
9888
+ NostrClient as NostrClient2,
9889
+ NostrKeyManager as NostrKeyManager2,
9890
+ Filter as Filter2
8726
9891
  } from "@unicitylabs/nostr-js-sdk";
8727
9892
 
8728
9893
  // modules/groupchat/types.ts
@@ -8738,7 +9903,7 @@ var GroupVisibility = {
8738
9903
 
8739
9904
  // modules/groupchat/GroupChatModule.ts
8740
9905
  function createNip29Filter(data) {
8741
- return new Filter(data);
9906
+ return new Filter2(data);
8742
9907
  }
8743
9908
  var GroupChatModule = class {
8744
9909
  config;
@@ -8787,7 +9952,7 @@ var GroupChatModule = class {
8787
9952
  }
8788
9953
  this.deps = deps;
8789
9954
  const secretKey = Buffer.from(deps.identity.privateKey, "hex");
8790
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
9955
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8791
9956
  }
8792
9957
  async load() {
8793
9958
  this.ensureInitialized();
@@ -8922,7 +10087,7 @@ var GroupChatModule = class {
8922
10087
  }
8923
10088
  this.subscriptionIds = [];
8924
10089
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
8925
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
10090
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8926
10091
  if (this.groups.size === 0) {
8927
10092
  await this.restoreJoinedGroups();
8928
10093
  } else {
@@ -8934,13 +10099,13 @@ var GroupChatModule = class {
8934
10099
  this.ensureInitialized();
8935
10100
  if (!this.keyManager) {
8936
10101
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
8937
- this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
10102
+ this.keyManager = NostrKeyManager2.fromPrivateKey(secretKey);
8938
10103
  }
8939
10104
  const primaryRelay = this.config.relays[0];
8940
10105
  if (primaryRelay) {
8941
10106
  await this.checkAndClearOnRelayChange(primaryRelay);
8942
10107
  }
8943
- this.client = new NostrClient(this.keyManager);
10108
+ this.client = new NostrClient2(this.keyManager);
8944
10109
  try {
8945
10110
  await this.client.connect(...this.config.relays);
8946
10111
  this.connected = true;
@@ -9199,7 +10364,7 @@ var GroupChatModule = class {
9199
10364
  if (!myPubkey) return [];
9200
10365
  const groupIdsWithMembership = /* @__PURE__ */ new Set();
9201
10366
  await this.oneshotSubscription(
9202
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10367
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9203
10368
  {
9204
10369
  onEvent: (event) => {
9205
10370
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9250,7 +10415,7 @@ var GroupChatModule = class {
9250
10415
  const memberCountsMap = /* @__PURE__ */ new Map();
9251
10416
  await Promise.all([
9252
10417
  this.oneshotSubscription(
9253
- new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
10418
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
9254
10419
  {
9255
10420
  onEvent: (event) => {
9256
10421
  const group = this.parseGroupMetadata(event);
@@ -9268,7 +10433,7 @@ var GroupChatModule = class {
9268
10433
  }
9269
10434
  ),
9270
10435
  this.oneshotSubscription(
9271
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10436
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9272
10437
  {
9273
10438
  onEvent: (event) => {
9274
10439
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9771,7 +10936,7 @@ var GroupChatModule = class {
9771
10936
  if (!this.client) return /* @__PURE__ */ new Set();
9772
10937
  const adminPubkeys = /* @__PURE__ */ new Set();
9773
10938
  return this.oneshotSubscription(
9774
- new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
10939
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
9775
10940
  {
9776
10941
  onEvent: (event) => {
9777
10942
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9793,7 +10958,7 @@ var GroupChatModule = class {
9793
10958
  if (!this.client) return null;
9794
10959
  let result = null;
9795
10960
  return this.oneshotSubscription(
9796
- new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
10961
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
9797
10962
  {
9798
10963
  onEvent: (event) => {
9799
10964
  if (!result) result = this.parseGroupMetadata(event);
@@ -9830,7 +10995,7 @@ var GroupChatModule = class {
9830
10995
  if (!this.client) return [];
9831
10996
  const members = [];
9832
10997
  return this.oneshotSubscription(
9833
- new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
10998
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
9834
10999
  {
9835
11000
  onEvent: (event) => {
9836
11001
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9851,7 +11016,7 @@ var GroupChatModule = class {
9851
11016
  if (!this.client) return [];
9852
11017
  const adminPubkeys = [];
9853
11018
  return this.oneshotSubscription(
9854
- new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
11019
+ new Filter2({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
9855
11020
  {
9856
11021
  onEvent: (event) => {
9857
11022
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -13651,11 +14816,18 @@ var Sphere = class _Sphere {
13651
14816
  _transport;
13652
14817
  _oracle;
13653
14818
  _priceProvider;
13654
- // Modules
14819
+ // Modules (single-instance — backward compat, delegates to active address)
13655
14820
  _payments;
13656
14821
  _communications;
13657
14822
  _groupChat = null;
13658
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;
13659
14831
  // Events
13660
14832
  eventHandlers = /* @__PURE__ */ new Map();
13661
14833
  // Provider management
@@ -13673,6 +14845,9 @@ var Sphere = class _Sphere {
13673
14845
  if (tokenStorage) {
13674
14846
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
13675
14847
  }
14848
+ this._l1Config = l1Config;
14849
+ this._groupChatConfig = groupChatConfig;
14850
+ this._marketConfig = marketConfig;
13676
14851
  this._payments = createPaymentsModule({ l1: l1Config });
13677
14852
  this._communications = createCommunicationsModule();
13678
14853
  this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
@@ -14931,7 +16106,7 @@ var Sphere = class _Sphere {
14931
16106
  nametags.set(0, newNametag);
14932
16107
  }
14933
16108
  const nametag = this._addressNametags.get(addressId)?.get(0);
14934
- this._identity = {
16109
+ const newIdentity = {
14935
16110
  privateKey: addressInfo.privateKey,
14936
16111
  chainPubkey: addressInfo.publicKey,
14937
16112
  l1Address: addressInfo.address,
@@ -14939,20 +16114,53 @@ var Sphere = class _Sphere {
14939
16114
  ipnsName: "12D3KooW" + ipnsHash,
14940
16115
  nametag
14941
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;
14942
16150
  this._currentAddressIndex = index;
14943
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;
14944
16157
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
14945
16158
  this._storage.setIdentity(this._identity);
14946
- await this._transport.setIdentity(this._identity);
14947
- logger.debug("Sphere", `switchToAddress(${index}): re-initializing ${this._tokenStorageProviders.size} token storage provider(s)`);
14948
- for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
14949
- logger.debug("Sphere", `switchToAddress(${index}): shutdown provider=${providerId}`);
14950
- await provider.shutdown();
14951
- provider.setIdentity(this._identity);
14952
- logger.debug("Sphere", `switchToAddress(${index}): initialize provider=${providerId}`);
14953
- await provider.initialize();
16159
+ if (this._transport.setFallbackSince) {
16160
+ const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
16161
+ this._transport.setFallbackSince(fallbackTs);
14954
16162
  }
14955
- await this.reinitializeModulesForNewAddress();
16163
+ await this._transport.setIdentity(this._identity);
14956
16164
  this.emitEvent("identity:changed", {
14957
16165
  l1Address: this._identity.l1Address,
14958
16166
  directAddress: this._identity.directAddress,
@@ -15007,42 +16215,104 @@ var Sphere = class _Sphere {
15007
16215
  }
15008
16216
  }
15009
16217
  /**
15010
- * 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
15011
16226
  */
15012
- async reinitializeModulesForNewAddress() {
16227
+ async initializeAddressModules(index, identity, tokenStorageProviders) {
15013
16228
  const emitEvent = this.emitEvent.bind(this);
15014
- this._payments.initialize({
15015
- 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,
15016
16237
  storage: this._storage,
15017
- tokenStorageProviders: this._tokenStorageProviders,
15018
- transport: this._transport,
16238
+ tokenStorageProviders,
16239
+ transport: addressTransport,
15019
16240
  oracle: this._oracle,
15020
16241
  emitEvent,
15021
16242
  chainCode: this._masterKey?.chainCode || void 0,
15022
16243
  price: this._priceProvider ?? void 0
15023
16244
  });
15024
- this._communications.initialize({
15025
- identity: this._identity,
16245
+ communications.initialize({
16246
+ identity,
15026
16247
  storage: this._storage,
15027
- transport: this._transport,
16248
+ transport: addressTransport,
15028
16249
  emitEvent
15029
16250
  });
15030
- this._groupChat?.initialize({
15031
- identity: this._identity,
16251
+ groupChat?.initialize({
16252
+ identity,
15032
16253
  storage: this._storage,
15033
16254
  emitEvent
15034
16255
  });
15035
- this._market?.initialize({
15036
- identity: this._identity,
16256
+ market?.initialize({
16257
+ identity,
15037
16258
  emitEvent
15038
16259
  });
15039
- await this._payments.load();
15040
- await this._communications.load();
15041
- await this._groupChat?.load();
15042
- await this._market?.load();
15043
- this._payments.sync().catch((err) => {
15044
- 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);
15045
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;
15046
16316
  }
15047
16317
  /**
15048
16318
  * Derive address at a specific index
@@ -15970,10 +17240,33 @@ var Sphere = class _Sphere {
15970
17240
  // ===========================================================================
15971
17241
  async destroy() {
15972
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();
15973
17262
  this._payments.destroy();
15974
17263
  this._communications.destroy();
15975
17264
  this._groupChat?.destroy();
15976
17265
  this._market?.destroy();
17266
+ if (this._transportMux) {
17267
+ await this._transportMux.disconnect();
17268
+ this._transportMux = null;
17269
+ }
15977
17270
  await this._transport.disconnect();
15978
17271
  await this._storage.disconnect();
15979
17272
  await this._oracle.disconnect();
@@ -16168,6 +17461,9 @@ var Sphere = class _Sphere {
16168
17461
  // ===========================================================================
16169
17462
  async initializeProviders() {
16170
17463
  this._storage.setIdentity(this._identity);
17464
+ if (this._transport.setFallbackSince) {
17465
+ this._transport.setFallbackSince(Math.floor(Date.now() / 1e3) - 86400);
17466
+ }
16171
17467
  await this._transport.setIdentity(this._identity);
16172
17468
  for (const provider of this._tokenStorageProviders.values()) {
16173
17469
  provider.setIdentity(this._identity);
@@ -16258,11 +17554,13 @@ var Sphere = class _Sphere {
16258
17554
  }
16259
17555
  async initializeModules() {
16260
17556
  const emitEvent = this.emitEvent.bind(this);
17557
+ const adapter = await this.ensureTransportMux(this._currentAddressIndex, this._identity);
17558
+ const moduleTransport = adapter ?? this._transport;
16261
17559
  this._payments.initialize({
16262
17560
  identity: this._identity,
16263
17561
  storage: this._storage,
16264
17562
  tokenStorageProviders: this._tokenStorageProviders,
16265
- transport: this._transport,
17563
+ transport: moduleTransport,
16266
17564
  oracle: this._oracle,
16267
17565
  emitEvent,
16268
17566
  // Pass chain code for L1 HD derivation
@@ -16273,7 +17571,7 @@ var Sphere = class _Sphere {
16273
17571
  this._communications.initialize({
16274
17572
  identity: this._identity,
16275
17573
  storage: this._storage,
16276
- transport: this._transport,
17574
+ transport: moduleTransport,
16277
17575
  emitEvent
16278
17576
  });
16279
17577
  this._groupChat?.initialize({
@@ -16289,6 +17587,17 @@ var Sphere = class _Sphere {
16289
17587
  await this._communications.load();
16290
17588
  await this._groupChat?.load();
16291
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
+ });
16292
17601
  }
16293
17602
  // ===========================================================================
16294
17603
  // Private: Helpers