@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.
@@ -161,7 +161,7 @@ function getAddressId(directAddress) {
161
161
  const last = hash.slice(-6).toLowerCase();
162
162
  return `DIRECT_${first}_${last}`;
163
163
  }
164
- 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;
164
+ 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;
165
165
  var init_constants = __esm({
166
166
  "constants.ts"() {
167
167
  "use strict";
@@ -238,6 +238,20 @@ var init_constants = __esm({
238
238
  "wss://nos.lol",
239
239
  "wss://relay.nostr.band"
240
240
  ];
241
+ NOSTR_EVENT_KINDS = {
242
+ /** NIP-04 encrypted direct message */
243
+ DIRECT_MESSAGE: 4,
244
+ /** Token transfer (Unicity custom - 31113) */
245
+ TOKEN_TRANSFER: 31113,
246
+ /** Payment request (Unicity custom - 31115) */
247
+ PAYMENT_REQUEST: 31115,
248
+ /** Payment request response (Unicity custom - 31116) */
249
+ PAYMENT_REQUEST_RESPONSE: 31116,
250
+ /** Nametag binding (NIP-78 app-specific data) */
251
+ NAMETAG_BINDING: 30078,
252
+ /** Public broadcast */
253
+ BROADCAST: 1
254
+ };
241
255
  NIP29_KINDS = {
242
256
  /** Chat message sent to group */
243
257
  CHAT_MESSAGE: 9,
@@ -319,6 +333,18 @@ var init_constants = __esm({
319
333
  tokenRegistryUrl: TOKEN_REGISTRY_URL
320
334
  }
321
335
  };
336
+ TIMEOUTS = {
337
+ /** WebSocket connection timeout */
338
+ WEBSOCKET_CONNECT: 1e4,
339
+ /** Nostr relay reconnect delay */
340
+ NOSTR_RECONNECT_DELAY: 3e3,
341
+ /** Max reconnect attempts */
342
+ MAX_RECONNECT_ATTEMPTS: 5,
343
+ /** Proof polling interval */
344
+ PROOF_POLL_INTERVAL: 1e3,
345
+ /** Sync interval */
346
+ SYNC_INTERVAL: 6e4
347
+ };
322
348
  }
323
349
  });
324
350
 
@@ -855,6 +881,1102 @@ module.exports = __toCommonJS(core_exports);
855
881
  init_logger();
856
882
  init_errors();
857
883
 
884
+ // transport/MultiAddressTransportMux.ts
885
+ var import_buffer = require("buffer");
886
+ var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
887
+ init_logger();
888
+ init_errors();
889
+
890
+ // transport/websocket.ts
891
+ function defaultUUIDGenerator() {
892
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
893
+ return crypto.randomUUID();
894
+ }
895
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
896
+ const r = Math.random() * 16 | 0;
897
+ const v = c === "x" ? r : r & 3 | 8;
898
+ return v.toString(16);
899
+ });
900
+ }
901
+
902
+ // transport/MultiAddressTransportMux.ts
903
+ init_constants();
904
+ var EVENT_KINDS = NOSTR_EVENT_KINDS;
905
+ var COMPOSING_INDICATOR_KIND = 25050;
906
+ var MultiAddressTransportMux = class {
907
+ config;
908
+ storage = null;
909
+ // Single NostrClient — one WebSocket connection for all addresses
910
+ nostrClient = null;
911
+ // KeyManager used for NostrClient creation (uses first address or temp key)
912
+ primaryKeyManager = null;
913
+ status = "disconnected";
914
+ // Per-address entries
915
+ addresses = /* @__PURE__ */ new Map();
916
+ // pubkey → address index (for fast routing)
917
+ pubkeyToIndex = /* @__PURE__ */ new Map();
918
+ // Subscription IDs
919
+ walletSubscriptionId = null;
920
+ chatSubscriptionId = null;
921
+ chatEoseFired = false;
922
+ chatEoseHandlers = [];
923
+ // Dedup
924
+ processedEventIds = /* @__PURE__ */ new Set();
925
+ // Event callbacks (mux-level, forwarded to all adapters)
926
+ eventCallbacks = /* @__PURE__ */ new Set();
927
+ constructor(config) {
928
+ this.config = {
929
+ relays: config.relays ?? [...DEFAULT_NOSTR_RELAYS],
930
+ timeout: config.timeout ?? TIMEOUTS.WEBSOCKET_CONNECT,
931
+ autoReconnect: config.autoReconnect ?? true,
932
+ reconnectDelay: config.reconnectDelay ?? TIMEOUTS.NOSTR_RECONNECT_DELAY,
933
+ maxReconnectAttempts: config.maxReconnectAttempts ?? TIMEOUTS.MAX_RECONNECT_ATTEMPTS,
934
+ createWebSocket: config.createWebSocket,
935
+ generateUUID: config.generateUUID ?? defaultUUIDGenerator
936
+ };
937
+ this.storage = config.storage ?? null;
938
+ }
939
+ // ===========================================================================
940
+ // Address Management
941
+ // ===========================================================================
942
+ /**
943
+ * Add an address to the multiplexer.
944
+ * Creates an AddressTransportAdapter for this address.
945
+ * If already connected, updates subscriptions to include the new pubkey.
946
+ */
947
+ async addAddress(index, identity, resolveDelegate) {
948
+ const existing = this.addresses.get(index);
949
+ if (existing) {
950
+ existing.identity = identity;
951
+ existing.keyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(import_buffer.Buffer.from(identity.privateKey, "hex"));
952
+ existing.nostrPubkey = existing.keyManager.getPublicKeyHex();
953
+ for (const [pk, idx] of this.pubkeyToIndex) {
954
+ if (idx === index) this.pubkeyToIndex.delete(pk);
955
+ }
956
+ this.pubkeyToIndex.set(existing.nostrPubkey, index);
957
+ logger.debug("Mux", `Updated address ${index}, pubkey: ${existing.nostrPubkey.slice(0, 16)}...`);
958
+ await this.updateSubscriptions();
959
+ return existing.adapter;
960
+ }
961
+ const keyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(import_buffer.Buffer.from(identity.privateKey, "hex"));
962
+ const nostrPubkey = keyManager.getPublicKeyHex();
963
+ const adapter = new AddressTransportAdapter(this, index, identity, resolveDelegate);
964
+ const entry = {
965
+ index,
966
+ identity,
967
+ keyManager,
968
+ nostrPubkey,
969
+ adapter,
970
+ lastEventTs: 0,
971
+ fallbackSince: null
972
+ };
973
+ this.addresses.set(index, entry);
974
+ this.pubkeyToIndex.set(nostrPubkey, index);
975
+ logger.debug("Mux", `Added address ${index}, pubkey: ${nostrPubkey.slice(0, 16)}..., total: ${this.addresses.size}`);
976
+ if (this.addresses.size === 1) {
977
+ this.primaryKeyManager = keyManager;
978
+ }
979
+ if (this.isConnected()) {
980
+ await this.updateSubscriptions();
981
+ }
982
+ return adapter;
983
+ }
984
+ /**
985
+ * Remove an address from the multiplexer.
986
+ * Stops routing events to this address.
987
+ */
988
+ async removeAddress(index) {
989
+ const entry = this.addresses.get(index);
990
+ if (!entry) return;
991
+ this.pubkeyToIndex.delete(entry.nostrPubkey);
992
+ this.addresses.delete(index);
993
+ logger.debug("Mux", `Removed address ${index}, remaining: ${this.addresses.size}`);
994
+ if (this.isConnected() && this.addresses.size > 0) {
995
+ await this.updateSubscriptions();
996
+ }
997
+ }
998
+ /**
999
+ * Get adapter for a specific address index.
1000
+ */
1001
+ getAdapter(index) {
1002
+ return this.addresses.get(index)?.adapter;
1003
+ }
1004
+ /**
1005
+ * Set fallback 'since' for an address (consumed once on next subscription setup).
1006
+ */
1007
+ setFallbackSince(index, sinceSeconds) {
1008
+ const entry = this.addresses.get(index);
1009
+ if (entry) {
1010
+ entry.fallbackSince = sinceSeconds;
1011
+ }
1012
+ }
1013
+ // ===========================================================================
1014
+ // Connection Management (delegated from adapters)
1015
+ // ===========================================================================
1016
+ async connect() {
1017
+ if (this.status === "connected") return;
1018
+ this.status = "connecting";
1019
+ try {
1020
+ if (!this.primaryKeyManager) {
1021
+ const tempKey = import_buffer.Buffer.alloc(32);
1022
+ crypto.getRandomValues(tempKey);
1023
+ this.primaryKeyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(tempKey);
1024
+ }
1025
+ this.nostrClient = new import_nostr_js_sdk.NostrClient(this.primaryKeyManager, {
1026
+ autoReconnect: this.config.autoReconnect,
1027
+ reconnectIntervalMs: this.config.reconnectDelay,
1028
+ maxReconnectIntervalMs: this.config.reconnectDelay * 16,
1029
+ pingIntervalMs: 15e3
1030
+ });
1031
+ this.nostrClient.addConnectionListener({
1032
+ onConnect: (url) => {
1033
+ logger.debug("Mux", "Connected to relay:", url);
1034
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1035
+ },
1036
+ onDisconnect: (url, reason) => {
1037
+ logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
1038
+ },
1039
+ onReconnecting: (url, attempt) => {
1040
+ logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
1041
+ this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
1042
+ },
1043
+ onReconnected: (url) => {
1044
+ logger.debug("Mux", "Reconnected to relay:", url);
1045
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1046
+ }
1047
+ });
1048
+ await Promise.race([
1049
+ this.nostrClient.connect(...this.config.relays),
1050
+ new Promise(
1051
+ (_, reject) => setTimeout(() => reject(new Error(
1052
+ `Transport connection timed out after ${this.config.timeout}ms`
1053
+ )), this.config.timeout)
1054
+ )
1055
+ ]);
1056
+ if (!this.nostrClient.isConnected()) {
1057
+ throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
1058
+ }
1059
+ this.status = "connected";
1060
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
1061
+ if (this.addresses.size > 0) {
1062
+ await this.updateSubscriptions();
1063
+ }
1064
+ } catch (error) {
1065
+ this.status = "error";
1066
+ throw error;
1067
+ }
1068
+ }
1069
+ async disconnect() {
1070
+ if (this.nostrClient) {
1071
+ this.nostrClient.disconnect();
1072
+ this.nostrClient = null;
1073
+ }
1074
+ this.walletSubscriptionId = null;
1075
+ this.chatSubscriptionId = null;
1076
+ this.chatEoseFired = false;
1077
+ this.status = "disconnected";
1078
+ this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
1079
+ }
1080
+ isConnected() {
1081
+ return this.status === "connected" && this.nostrClient?.isConnected() === true;
1082
+ }
1083
+ getStatus() {
1084
+ return this.status;
1085
+ }
1086
+ // ===========================================================================
1087
+ // Relay Management
1088
+ // ===========================================================================
1089
+ getRelays() {
1090
+ return [...this.config.relays];
1091
+ }
1092
+ getConnectedRelays() {
1093
+ if (!this.nostrClient) return [];
1094
+ return Array.from(this.nostrClient.getConnectedRelays());
1095
+ }
1096
+ async addRelay(relayUrl) {
1097
+ if (this.config.relays.includes(relayUrl)) return false;
1098
+ this.config.relays.push(relayUrl);
1099
+ if (this.status === "connected" && this.nostrClient) {
1100
+ try {
1101
+ await this.nostrClient.connect(relayUrl);
1102
+ this.emitEvent({ type: "transport:relay_added", timestamp: Date.now(), data: { relay: relayUrl, connected: true } });
1103
+ return true;
1104
+ } catch (error) {
1105
+ this.emitEvent({ type: "transport:relay_added", timestamp: Date.now(), data: { relay: relayUrl, connected: false, error: String(error) } });
1106
+ return false;
1107
+ }
1108
+ }
1109
+ return true;
1110
+ }
1111
+ async removeRelay(relayUrl) {
1112
+ const idx = this.config.relays.indexOf(relayUrl);
1113
+ if (idx === -1) return false;
1114
+ this.config.relays.splice(idx, 1);
1115
+ this.emitEvent({ type: "transport:relay_removed", timestamp: Date.now(), data: { relay: relayUrl } });
1116
+ return true;
1117
+ }
1118
+ hasRelay(relayUrl) {
1119
+ return this.config.relays.includes(relayUrl);
1120
+ }
1121
+ isRelayConnected(relayUrl) {
1122
+ if (!this.nostrClient) return false;
1123
+ return this.nostrClient.getConnectedRelays().has(relayUrl);
1124
+ }
1125
+ // ===========================================================================
1126
+ // Subscription Management
1127
+ // ===========================================================================
1128
+ /**
1129
+ * Update Nostr subscriptions to listen for events on ALL registered address pubkeys.
1130
+ * Called whenever addresses are added/removed.
1131
+ */
1132
+ async updateSubscriptions() {
1133
+ if (!this.nostrClient || this.addresses.size === 0) return;
1134
+ if (this.walletSubscriptionId) {
1135
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1136
+ this.walletSubscriptionId = null;
1137
+ }
1138
+ if (this.chatSubscriptionId) {
1139
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1140
+ this.chatSubscriptionId = null;
1141
+ }
1142
+ const allPubkeys = [];
1143
+ for (const entry of this.addresses.values()) {
1144
+ allPubkeys.push(entry.nostrPubkey);
1145
+ }
1146
+ logger.debug("Mux", `Subscribing for ${allPubkeys.length} address(es):`, allPubkeys.map((p) => p.slice(0, 12)).join(", "));
1147
+ let globalSince = Math.floor(Date.now() / 1e3);
1148
+ for (const entry of this.addresses.values()) {
1149
+ const since = await this.getAddressSince(entry);
1150
+ if (since < globalSince) {
1151
+ globalSince = since;
1152
+ }
1153
+ }
1154
+ const walletFilter = new import_nostr_js_sdk.Filter();
1155
+ walletFilter.kinds = [
1156
+ EVENT_KINDS.DIRECT_MESSAGE,
1157
+ EVENT_KINDS.TOKEN_TRANSFER,
1158
+ EVENT_KINDS.PAYMENT_REQUEST,
1159
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
1160
+ ];
1161
+ walletFilter["#p"] = allPubkeys;
1162
+ walletFilter.since = globalSince;
1163
+ this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
1164
+ onEvent: (event) => {
1165
+ this.handleEvent({
1166
+ id: event.id,
1167
+ kind: event.kind,
1168
+ content: event.content,
1169
+ tags: event.tags,
1170
+ pubkey: event.pubkey,
1171
+ created_at: event.created_at,
1172
+ sig: event.sig
1173
+ });
1174
+ },
1175
+ onEndOfStoredEvents: () => {
1176
+ logger.debug("Mux", "Wallet subscription EOSE");
1177
+ },
1178
+ onError: (_subId, error) => {
1179
+ logger.debug("Mux", "Wallet subscription error:", error);
1180
+ }
1181
+ });
1182
+ const chatFilter = new import_nostr_js_sdk.Filter();
1183
+ chatFilter.kinds = [import_nostr_js_sdk.EventKinds.GIFT_WRAP];
1184
+ chatFilter["#p"] = allPubkeys;
1185
+ this.chatSubscriptionId = this.nostrClient.subscribe(chatFilter, {
1186
+ onEvent: (event) => {
1187
+ this.handleEvent({
1188
+ id: event.id,
1189
+ kind: event.kind,
1190
+ content: event.content,
1191
+ tags: event.tags,
1192
+ pubkey: event.pubkey,
1193
+ created_at: event.created_at,
1194
+ sig: event.sig
1195
+ });
1196
+ },
1197
+ onEndOfStoredEvents: () => {
1198
+ logger.debug("Mux", "Chat subscription EOSE");
1199
+ if (!this.chatEoseFired) {
1200
+ this.chatEoseFired = true;
1201
+ for (const handler of this.chatEoseHandlers) {
1202
+ try {
1203
+ handler();
1204
+ } catch {
1205
+ }
1206
+ }
1207
+ }
1208
+ },
1209
+ onError: (_subId, error) => {
1210
+ logger.debug("Mux", "Chat subscription error:", error);
1211
+ }
1212
+ });
1213
+ }
1214
+ /**
1215
+ * Determine 'since' timestamp for an address entry.
1216
+ */
1217
+ async getAddressSince(entry) {
1218
+ if (this.storage) {
1219
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${entry.nostrPubkey.slice(0, 16)}`;
1220
+ try {
1221
+ const stored = await this.storage.get(storageKey);
1222
+ if (stored) {
1223
+ const ts = parseInt(stored, 10);
1224
+ entry.lastEventTs = ts;
1225
+ entry.fallbackSince = null;
1226
+ return ts;
1227
+ } else if (entry.fallbackSince !== null) {
1228
+ const ts = entry.fallbackSince;
1229
+ entry.lastEventTs = ts;
1230
+ entry.fallbackSince = null;
1231
+ return ts;
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ return Math.floor(Date.now() / 1e3);
1237
+ }
1238
+ // ===========================================================================
1239
+ // Event Routing
1240
+ // ===========================================================================
1241
+ /**
1242
+ * Route an incoming Nostr event to the correct address adapter.
1243
+ */
1244
+ async handleEvent(event) {
1245
+ if (event.id && this.processedEventIds.has(event.id)) return;
1246
+ if (event.id) this.processedEventIds.add(event.id);
1247
+ try {
1248
+ if (event.kind === import_nostr_js_sdk.EventKinds.GIFT_WRAP) {
1249
+ await this.routeGiftWrap(event);
1250
+ } else {
1251
+ const recipientPubkey = this.extractRecipientPubkey(event);
1252
+ if (!recipientPubkey) {
1253
+ logger.debug("Mux", "Event has no #p tag, dropping:", event.id?.slice(0, 12));
1254
+ return;
1255
+ }
1256
+ const addressIndex = this.pubkeyToIndex.get(recipientPubkey);
1257
+ if (addressIndex === void 0) {
1258
+ logger.debug("Mux", "Event for unknown pubkey:", recipientPubkey.slice(0, 16), "dropping");
1259
+ return;
1260
+ }
1261
+ const entry = this.addresses.get(addressIndex);
1262
+ if (!entry) return;
1263
+ await this.dispatchWalletEvent(entry, event);
1264
+ }
1265
+ } catch (error) {
1266
+ logger.debug("Mux", "Failed to handle event:", event.id?.slice(0, 12), error);
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Extract recipient pubkey from event's #p tag.
1271
+ * Returns the first #p value that matches a known address pubkey,
1272
+ * or the first #p value if none match.
1273
+ */
1274
+ extractRecipientPubkey(event) {
1275
+ const pTags = event.tags?.filter((t) => t[0] === "p");
1276
+ if (!pTags || pTags.length === 0) return null;
1277
+ for (const tag of pTags) {
1278
+ if (tag[1] && this.pubkeyToIndex.has(tag[1])) {
1279
+ return tag[1];
1280
+ }
1281
+ }
1282
+ return pTags[0]?.[1] ?? null;
1283
+ }
1284
+ /**
1285
+ * Route a gift wrap event by trying decryption with each address keyManager.
1286
+ */
1287
+ async routeGiftWrap(event) {
1288
+ for (const entry of this.addresses.values()) {
1289
+ try {
1290
+ const pm = import_nostr_js_sdk.NIP17.unwrap(event, entry.keyManager);
1291
+ logger.debug("Mux", `Gift wrap decrypted by address ${entry.index}, sender: ${pm.senderPubkey?.slice(0, 16)}`);
1292
+ if (pm.senderPubkey === entry.nostrPubkey) {
1293
+ try {
1294
+ const parsed = JSON.parse(pm.content);
1295
+ if (parsed?.selfWrap && parsed.recipientPubkey) {
1296
+ const message2 = {
1297
+ id: parsed.originalId || pm.eventId,
1298
+ senderTransportPubkey: pm.senderPubkey,
1299
+ senderNametag: parsed.senderNametag,
1300
+ recipientTransportPubkey: parsed.recipientPubkey,
1301
+ content: parsed.text ?? "",
1302
+ timestamp: pm.timestamp * 1e3,
1303
+ encrypted: true,
1304
+ isSelfWrap: true
1305
+ };
1306
+ entry.adapter.dispatchMessage(message2);
1307
+ return;
1308
+ }
1309
+ } catch {
1310
+ }
1311
+ return;
1312
+ }
1313
+ if ((0, import_nostr_js_sdk.isReadReceipt)(pm)) {
1314
+ if (pm.replyToEventId) {
1315
+ const receipt = {
1316
+ senderTransportPubkey: pm.senderPubkey,
1317
+ messageEventId: pm.replyToEventId,
1318
+ timestamp: pm.timestamp * 1e3
1319
+ };
1320
+ entry.adapter.dispatchReadReceipt(receipt);
1321
+ }
1322
+ return;
1323
+ }
1324
+ if (pm.kind === COMPOSING_INDICATOR_KIND) {
1325
+ let senderNametag2;
1326
+ let expiresIn = 3e4;
1327
+ try {
1328
+ const parsed = JSON.parse(pm.content);
1329
+ senderNametag2 = parsed.senderNametag || void 0;
1330
+ expiresIn = parsed.expiresIn ?? 3e4;
1331
+ } catch {
1332
+ }
1333
+ entry.adapter.dispatchComposingIndicator({
1334
+ senderPubkey: pm.senderPubkey,
1335
+ senderNametag: senderNametag2,
1336
+ expiresIn
1337
+ });
1338
+ return;
1339
+ }
1340
+ try {
1341
+ const parsed = JSON.parse(pm.content);
1342
+ if (parsed?.type === "typing") {
1343
+ const indicator = {
1344
+ senderTransportPubkey: pm.senderPubkey,
1345
+ senderNametag: parsed.senderNametag,
1346
+ timestamp: pm.timestamp * 1e3
1347
+ };
1348
+ entry.adapter.dispatchTypingIndicator(indicator);
1349
+ return;
1350
+ }
1351
+ } catch {
1352
+ }
1353
+ if (!(0, import_nostr_js_sdk.isChatMessage)(pm)) return;
1354
+ let content = pm.content;
1355
+ let senderNametag;
1356
+ try {
1357
+ const parsed = JSON.parse(content);
1358
+ if (typeof parsed === "object" && parsed.text !== void 0) {
1359
+ content = parsed.text;
1360
+ senderNametag = parsed.senderNametag || void 0;
1361
+ }
1362
+ } catch {
1363
+ }
1364
+ const message = {
1365
+ id: event.id,
1366
+ senderTransportPubkey: pm.senderPubkey,
1367
+ senderNametag,
1368
+ content,
1369
+ timestamp: pm.timestamp * 1e3,
1370
+ encrypted: true
1371
+ };
1372
+ entry.adapter.dispatchMessage(message);
1373
+ return;
1374
+ } catch {
1375
+ continue;
1376
+ }
1377
+ }
1378
+ logger.debug("Mux", "Gift wrap could not be decrypted by any address");
1379
+ }
1380
+ /**
1381
+ * Dispatch a wallet event (non-gift-wrap) to the correct address adapter.
1382
+ */
1383
+ async dispatchWalletEvent(entry, event) {
1384
+ switch (event.kind) {
1385
+ case EVENT_KINDS.DIRECT_MESSAGE:
1386
+ break;
1387
+ case EVENT_KINDS.TOKEN_TRANSFER:
1388
+ await this.handleTokenTransfer(entry, event);
1389
+ break;
1390
+ case EVENT_KINDS.PAYMENT_REQUEST:
1391
+ await this.handlePaymentRequest(entry, event);
1392
+ break;
1393
+ case EVENT_KINDS.PAYMENT_REQUEST_RESPONSE:
1394
+ await this.handlePaymentRequestResponse(entry, event);
1395
+ break;
1396
+ }
1397
+ if (event.created_at) {
1398
+ this.updateLastEventTimestamp(entry, event.created_at);
1399
+ }
1400
+ }
1401
+ async handleTokenTransfer(entry, event) {
1402
+ try {
1403
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1404
+ const payload = JSON.parse(content);
1405
+ const transfer = {
1406
+ id: event.id,
1407
+ senderTransportPubkey: event.pubkey,
1408
+ payload,
1409
+ timestamp: event.created_at * 1e3
1410
+ };
1411
+ entry.adapter.dispatchTokenTransfer(transfer);
1412
+ } catch (err) {
1413
+ logger.debug("Mux", `Token transfer decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1414
+ }
1415
+ }
1416
+ async handlePaymentRequest(entry, event) {
1417
+ try {
1418
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1419
+ const requestData = JSON.parse(content);
1420
+ const request = {
1421
+ id: event.id,
1422
+ senderTransportPubkey: event.pubkey,
1423
+ request: {
1424
+ requestId: requestData.requestId,
1425
+ amount: requestData.amount,
1426
+ coinId: requestData.coinId,
1427
+ message: requestData.message,
1428
+ recipientNametag: requestData.recipientNametag,
1429
+ metadata: requestData.metadata
1430
+ },
1431
+ timestamp: event.created_at * 1e3
1432
+ };
1433
+ entry.adapter.dispatchPaymentRequest(request);
1434
+ } catch (err) {
1435
+ logger.debug("Mux", `Payment request decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1436
+ }
1437
+ }
1438
+ async handlePaymentRequestResponse(entry, event) {
1439
+ try {
1440
+ const content = await this.decryptContent(entry, event.content, event.pubkey);
1441
+ const responseData = JSON.parse(content);
1442
+ const response = {
1443
+ id: event.id,
1444
+ responderTransportPubkey: event.pubkey,
1445
+ response: {
1446
+ requestId: responseData.requestId,
1447
+ responseType: responseData.responseType,
1448
+ message: responseData.message,
1449
+ transferId: responseData.transferId
1450
+ },
1451
+ timestamp: event.created_at * 1e3
1452
+ };
1453
+ entry.adapter.dispatchPaymentRequestResponse(response);
1454
+ } catch (err) {
1455
+ logger.debug("Mux", `Payment response decrypt failed for address ${entry.index}:`, err?.message?.slice(0, 50));
1456
+ }
1457
+ }
1458
+ // ===========================================================================
1459
+ // Crypto Helpers
1460
+ // ===========================================================================
1461
+ async decryptContent(entry, content, senderPubkey) {
1462
+ const decrypted = await import_nostr_js_sdk.NIP04.decryptHex(
1463
+ content,
1464
+ entry.keyManager.getPrivateKeyHex(),
1465
+ senderPubkey
1466
+ );
1467
+ return this.stripContentPrefix(decrypted);
1468
+ }
1469
+ stripContentPrefix(content) {
1470
+ const prefixes = ["payment_request:", "token_transfer:", "payment_response:"];
1471
+ for (const prefix of prefixes) {
1472
+ if (content.startsWith(prefix)) return content.slice(prefix.length);
1473
+ }
1474
+ return content;
1475
+ }
1476
+ // ===========================================================================
1477
+ // Sending (called by adapters)
1478
+ // ===========================================================================
1479
+ /**
1480
+ * Create an encrypted event using a specific address's keyManager.
1481
+ * Used by AddressTransportAdapter for sending.
1482
+ */
1483
+ async createAndPublishEncryptedEvent(addressIndex, kind, content, tags) {
1484
+ const entry = this.addresses.get(addressIndex);
1485
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1486
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1487
+ const recipientTag = tags.find((t) => t[0] === "p");
1488
+ if (!recipientTag?.[1]) throw new SphereError("No recipient pubkey in tags", "VALIDATION_ERROR");
1489
+ const encrypted = await import_nostr_js_sdk.NIP04.encryptHex(
1490
+ content,
1491
+ entry.keyManager.getPrivateKeyHex(),
1492
+ recipientTag[1]
1493
+ );
1494
+ const signedEvent = import_nostr_js_sdk.Event.create(entry.keyManager, { kind, content: encrypted, tags });
1495
+ const nostrEvent = import_nostr_js_sdk.Event.fromJSON({
1496
+ id: signedEvent.id,
1497
+ kind: signedEvent.kind,
1498
+ content: signedEvent.content,
1499
+ tags: signedEvent.tags,
1500
+ pubkey: signedEvent.pubkey,
1501
+ created_at: signedEvent.created_at,
1502
+ sig: signedEvent.sig
1503
+ });
1504
+ await this.nostrClient.publishEvent(nostrEvent);
1505
+ return signedEvent.id;
1506
+ }
1507
+ /**
1508
+ * Create and publish a NIP-17 gift wrap message for a specific address.
1509
+ */
1510
+ async sendGiftWrap(addressIndex, recipientPubkey, content) {
1511
+ const entry = this.addresses.get(addressIndex);
1512
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1513
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1514
+ const nostrRecipient = recipientPubkey.length === 66 && (recipientPubkey.startsWith("02") || recipientPubkey.startsWith("03")) ? recipientPubkey.slice(2) : recipientPubkey;
1515
+ const giftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(entry.keyManager, nostrRecipient, content);
1516
+ const giftWrapEvent = import_nostr_js_sdk.Event.fromJSON(giftWrap);
1517
+ await this.nostrClient.publishEvent(giftWrapEvent);
1518
+ const selfPubkey = entry.keyManager.getPublicKeyHex();
1519
+ const senderNametag = entry.identity.nametag;
1520
+ const selfWrapContent = JSON.stringify({
1521
+ selfWrap: true,
1522
+ originalId: giftWrap.id,
1523
+ recipientPubkey,
1524
+ senderNametag,
1525
+ text: content
1526
+ });
1527
+ const selfGiftWrap = import_nostr_js_sdk.NIP17.createGiftWrap(entry.keyManager, selfPubkey, selfWrapContent);
1528
+ const selfGiftWrapEvent = import_nostr_js_sdk.Event.fromJSON(selfGiftWrap);
1529
+ this.nostrClient.publishEvent(selfGiftWrapEvent).catch((err) => {
1530
+ logger.debug("Mux", "Self-wrap publish failed:", err);
1531
+ });
1532
+ return giftWrap.id;
1533
+ }
1534
+ /**
1535
+ * Publish a raw event (e.g., identity binding, broadcast).
1536
+ */
1537
+ async publishRawEvent(addressIndex, kind, content, tags) {
1538
+ const entry = this.addresses.get(addressIndex);
1539
+ if (!entry) throw new SphereError("Address not registered in mux", "NOT_INITIALIZED");
1540
+ if (!this.nostrClient) throw new SphereError("Not connected", "NOT_INITIALIZED");
1541
+ const signedEvent = import_nostr_js_sdk.Event.create(entry.keyManager, { kind, content, tags });
1542
+ const nostrEvent = import_nostr_js_sdk.Event.fromJSON({
1543
+ id: signedEvent.id,
1544
+ kind: signedEvent.kind,
1545
+ content: signedEvent.content,
1546
+ tags: signedEvent.tags,
1547
+ pubkey: signedEvent.pubkey,
1548
+ created_at: signedEvent.created_at,
1549
+ sig: signedEvent.sig
1550
+ });
1551
+ await this.nostrClient.publishEvent(nostrEvent);
1552
+ return signedEvent.id;
1553
+ }
1554
+ // ===========================================================================
1555
+ // Resolve Methods (delegates to inner — these are stateless relay queries)
1556
+ // ===========================================================================
1557
+ /**
1558
+ * Get the NostrClient for resolve operations.
1559
+ * Adapters use this for resolve*, publishIdentityBinding, etc.
1560
+ */
1561
+ getNostrClient() {
1562
+ return this.nostrClient;
1563
+ }
1564
+ /**
1565
+ * Get keyManager for a specific address (used by adapters for resolve/binding).
1566
+ */
1567
+ getKeyManager(addressIndex) {
1568
+ return this.addresses.get(addressIndex)?.keyManager ?? null;
1569
+ }
1570
+ /**
1571
+ * Get identity for a specific address.
1572
+ */
1573
+ getIdentity(addressIndex) {
1574
+ return this.addresses.get(addressIndex)?.identity ?? null;
1575
+ }
1576
+ // ===========================================================================
1577
+ // Event timestamp persistence
1578
+ // ===========================================================================
1579
+ updateLastEventTimestamp(entry, createdAt) {
1580
+ if (!this.storage) return;
1581
+ if (createdAt <= entry.lastEventTs) return;
1582
+ entry.lastEventTs = createdAt;
1583
+ const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${entry.nostrPubkey.slice(0, 16)}`;
1584
+ this.storage.set(storageKey, createdAt.toString()).catch((err) => {
1585
+ logger.debug("Mux", "Failed to save last event timestamp:", err);
1586
+ });
1587
+ }
1588
+ // ===========================================================================
1589
+ // Mux-level event system
1590
+ // ===========================================================================
1591
+ onTransportEvent(callback) {
1592
+ this.eventCallbacks.add(callback);
1593
+ return () => this.eventCallbacks.delete(callback);
1594
+ }
1595
+ onChatReady(handler) {
1596
+ if (this.chatEoseFired) {
1597
+ try {
1598
+ handler();
1599
+ } catch {
1600
+ }
1601
+ return () => {
1602
+ };
1603
+ }
1604
+ this.chatEoseHandlers.push(handler);
1605
+ return () => {
1606
+ const idx = this.chatEoseHandlers.indexOf(handler);
1607
+ if (idx >= 0) this.chatEoseHandlers.splice(idx, 1);
1608
+ };
1609
+ }
1610
+ emitEvent(event) {
1611
+ for (const cb of this.eventCallbacks) {
1612
+ try {
1613
+ cb(event);
1614
+ } catch {
1615
+ }
1616
+ }
1617
+ for (const entry of this.addresses.values()) {
1618
+ entry.adapter.emitTransportEvent(event);
1619
+ }
1620
+ }
1621
+ // ===========================================================================
1622
+ // Dedup Management
1623
+ // ===========================================================================
1624
+ /**
1625
+ * Clear processed event IDs (e.g., on address change or periodic cleanup).
1626
+ */
1627
+ clearProcessedEvents() {
1628
+ this.processedEventIds.clear();
1629
+ }
1630
+ /**
1631
+ * Get the storage adapter (for adapters that need it).
1632
+ */
1633
+ getStorage() {
1634
+ return this.storage;
1635
+ }
1636
+ /**
1637
+ * Get the UUID generator.
1638
+ */
1639
+ getUUIDGenerator() {
1640
+ return this.config.generateUUID;
1641
+ }
1642
+ };
1643
+ var AddressTransportAdapter = class {
1644
+ id;
1645
+ name;
1646
+ type = "p2p";
1647
+ description;
1648
+ mux;
1649
+ addressIndex;
1650
+ identity;
1651
+ resolveDelegate;
1652
+ // Per-address handler sets
1653
+ messageHandlers = /* @__PURE__ */ new Set();
1654
+ transferHandlers = /* @__PURE__ */ new Set();
1655
+ paymentRequestHandlers = /* @__PURE__ */ new Set();
1656
+ paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
1657
+ readReceiptHandlers = /* @__PURE__ */ new Set();
1658
+ typingIndicatorHandlers = /* @__PURE__ */ new Set();
1659
+ composingHandlers = /* @__PURE__ */ new Set();
1660
+ instantSplitBundleHandlers = /* @__PURE__ */ new Set();
1661
+ broadcastHandlers = /* @__PURE__ */ new Map();
1662
+ eventCallbacks = /* @__PURE__ */ new Set();
1663
+ pendingMessages = [];
1664
+ chatEoseHandlers = [];
1665
+ constructor(mux, addressIndex, identity, resolveDelegate) {
1666
+ this.mux = mux;
1667
+ this.addressIndex = addressIndex;
1668
+ this.identity = identity;
1669
+ this.resolveDelegate = resolveDelegate ?? null;
1670
+ this.id = `nostr-addr-${addressIndex}`;
1671
+ this.name = `Nostr Transport (address ${addressIndex})`;
1672
+ this.description = `P2P messaging for address index ${addressIndex}`;
1673
+ }
1674
+ // ===========================================================================
1675
+ // BaseProvider — delegates to mux
1676
+ // ===========================================================================
1677
+ async connect() {
1678
+ await this.mux.connect();
1679
+ }
1680
+ async disconnect() {
1681
+ }
1682
+ isConnected() {
1683
+ return this.mux.isConnected();
1684
+ }
1685
+ getStatus() {
1686
+ return this.mux.getStatus();
1687
+ }
1688
+ // ===========================================================================
1689
+ // Identity (no-op — mux manages identity via addAddress)
1690
+ // ===========================================================================
1691
+ async setIdentity(identity) {
1692
+ this.identity = identity;
1693
+ await this.mux.addAddress(this.addressIndex, identity);
1694
+ }
1695
+ // ===========================================================================
1696
+ // Sending — delegates to mux with this address's keyManager
1697
+ // ===========================================================================
1698
+ async sendMessage(recipientPubkey, content) {
1699
+ const senderNametag = this.identity.nametag;
1700
+ const wrappedContent = senderNametag ? JSON.stringify({ senderNametag, text: content }) : content;
1701
+ return this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, wrappedContent);
1702
+ }
1703
+ async sendTokenTransfer(recipientPubkey, payload) {
1704
+ const content = "token_transfer:" + JSON.stringify(payload);
1705
+ const uniqueD = `token-transfer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1706
+ return this.mux.createAndPublishEncryptedEvent(
1707
+ this.addressIndex,
1708
+ EVENT_KINDS.TOKEN_TRANSFER,
1709
+ content,
1710
+ [["p", recipientPubkey], ["d", uniqueD], ["type", "token_transfer"]]
1711
+ );
1712
+ }
1713
+ async sendPaymentRequest(recipientPubkey, payload) {
1714
+ const requestId2 = this.mux.getUUIDGenerator()();
1715
+ const amount = typeof payload.amount === "bigint" ? payload.amount.toString() : payload.amount;
1716
+ const requestContent = {
1717
+ requestId: requestId2,
1718
+ amount,
1719
+ coinId: payload.coinId,
1720
+ message: payload.message,
1721
+ recipientNametag: payload.recipientNametag,
1722
+ deadline: Date.now() + 5 * 60 * 1e3
1723
+ };
1724
+ const content = "payment_request:" + JSON.stringify(requestContent);
1725
+ const tags = [
1726
+ ["p", recipientPubkey],
1727
+ ["type", "payment_request"],
1728
+ ["amount", amount]
1729
+ ];
1730
+ if (payload.recipientNametag) {
1731
+ tags.push(["recipient", payload.recipientNametag]);
1732
+ }
1733
+ return this.mux.createAndPublishEncryptedEvent(
1734
+ this.addressIndex,
1735
+ EVENT_KINDS.PAYMENT_REQUEST,
1736
+ content,
1737
+ tags
1738
+ );
1739
+ }
1740
+ async sendPaymentRequestResponse(recipientPubkey, response) {
1741
+ const content = "payment_response:" + JSON.stringify(response);
1742
+ return this.mux.createAndPublishEncryptedEvent(
1743
+ this.addressIndex,
1744
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE,
1745
+ content,
1746
+ [["p", recipientPubkey], ["type", "payment_response"]]
1747
+ );
1748
+ }
1749
+ async sendReadReceipt(recipientPubkey, messageEventId) {
1750
+ const content = JSON.stringify({ type: "read_receipt", messageEventId });
1751
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1752
+ }
1753
+ async sendTypingIndicator(recipientPubkey) {
1754
+ const content = JSON.stringify({
1755
+ type: "typing",
1756
+ senderNametag: this.identity.nametag
1757
+ });
1758
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1759
+ }
1760
+ async sendComposingIndicator(recipientPubkey, content) {
1761
+ await this.mux.sendGiftWrap(this.addressIndex, recipientPubkey, content);
1762
+ }
1763
+ async sendInstantSplitBundle(recipientPubkey, bundle) {
1764
+ const content = "token_transfer:" + JSON.stringify({
1765
+ type: "instant_split",
1766
+ ...bundle
1767
+ });
1768
+ const uniqueD = `instant-split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1769
+ return this.mux.createAndPublishEncryptedEvent(
1770
+ this.addressIndex,
1771
+ EVENT_KINDS.TOKEN_TRANSFER,
1772
+ content,
1773
+ [["p", recipientPubkey], ["d", uniqueD], ["type", "instant_split"]]
1774
+ );
1775
+ }
1776
+ // ===========================================================================
1777
+ // Subscription handlers — per-address
1778
+ // ===========================================================================
1779
+ onMessage(handler) {
1780
+ this.messageHandlers.add(handler);
1781
+ if (this.pendingMessages.length > 0) {
1782
+ const pending2 = this.pendingMessages;
1783
+ this.pendingMessages = [];
1784
+ for (const msg of pending2) {
1785
+ try {
1786
+ handler(msg);
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ }
1791
+ return () => this.messageHandlers.delete(handler);
1792
+ }
1793
+ onTokenTransfer(handler) {
1794
+ this.transferHandlers.add(handler);
1795
+ return () => this.transferHandlers.delete(handler);
1796
+ }
1797
+ onPaymentRequest(handler) {
1798
+ this.paymentRequestHandlers.add(handler);
1799
+ return () => this.paymentRequestHandlers.delete(handler);
1800
+ }
1801
+ onPaymentRequestResponse(handler) {
1802
+ this.paymentRequestResponseHandlers.add(handler);
1803
+ return () => this.paymentRequestResponseHandlers.delete(handler);
1804
+ }
1805
+ onReadReceipt(handler) {
1806
+ this.readReceiptHandlers.add(handler);
1807
+ return () => this.readReceiptHandlers.delete(handler);
1808
+ }
1809
+ onTypingIndicator(handler) {
1810
+ this.typingIndicatorHandlers.add(handler);
1811
+ return () => this.typingIndicatorHandlers.delete(handler);
1812
+ }
1813
+ onComposing(handler) {
1814
+ this.composingHandlers.add(handler);
1815
+ return () => this.composingHandlers.delete(handler);
1816
+ }
1817
+ onInstantSplitReceived(handler) {
1818
+ this.instantSplitBundleHandlers.add(handler);
1819
+ return () => this.instantSplitBundleHandlers.delete(handler);
1820
+ }
1821
+ subscribeToBroadcast(tags, handler) {
1822
+ const key = tags.sort().join(":");
1823
+ if (!this.broadcastHandlers.has(key)) {
1824
+ this.broadcastHandlers.set(key, /* @__PURE__ */ new Set());
1825
+ }
1826
+ this.broadcastHandlers.get(key).add(handler);
1827
+ return () => this.broadcastHandlers.get(key)?.delete(handler);
1828
+ }
1829
+ async publishBroadcast(content, tags) {
1830
+ const eventTags = tags ? tags.map((t) => ["t", t]) : [];
1831
+ return this.mux.publishRawEvent(this.addressIndex, 30023, content, eventTags);
1832
+ }
1833
+ // ===========================================================================
1834
+ // Resolve methods — delegate to original NostrTransportProvider
1835
+ // These are stateless relay queries, shared across all addresses
1836
+ // ===========================================================================
1837
+ async resolve(identifier) {
1838
+ return this.resolveDelegate?.resolve?.(identifier) ?? null;
1839
+ }
1840
+ async resolveNametag(nametag) {
1841
+ return this.resolveDelegate?.resolveNametag?.(nametag) ?? null;
1842
+ }
1843
+ async resolveNametagInfo(nametag) {
1844
+ return this.resolveDelegate?.resolveNametagInfo?.(nametag) ?? null;
1845
+ }
1846
+ async resolveAddressInfo(address) {
1847
+ return this.resolveDelegate?.resolveAddressInfo?.(address) ?? null;
1848
+ }
1849
+ async resolveTransportPubkeyInfo(transportPubkey) {
1850
+ return this.resolveDelegate?.resolveTransportPubkeyInfo?.(transportPubkey) ?? null;
1851
+ }
1852
+ async discoverAddresses(transportPubkeys) {
1853
+ return this.resolveDelegate?.discoverAddresses?.(transportPubkeys) ?? [];
1854
+ }
1855
+ async recoverNametag() {
1856
+ return this.resolveDelegate?.recoverNametag?.() ?? null;
1857
+ }
1858
+ async publishIdentityBinding(chainPubkey, l1Address, directAddress, nametag) {
1859
+ return this.resolveDelegate?.publishIdentityBinding?.(chainPubkey, l1Address, directAddress, nametag) ?? false;
1860
+ }
1861
+ // ===========================================================================
1862
+ // Relay Management — delegates to mux
1863
+ // ===========================================================================
1864
+ getRelays() {
1865
+ return this.mux.getRelays();
1866
+ }
1867
+ getConnectedRelays() {
1868
+ return this.mux.getConnectedRelays();
1869
+ }
1870
+ async addRelay(relayUrl) {
1871
+ return this.mux.addRelay(relayUrl);
1872
+ }
1873
+ async removeRelay(relayUrl) {
1874
+ return this.mux.removeRelay(relayUrl);
1875
+ }
1876
+ hasRelay(relayUrl) {
1877
+ return this.mux.hasRelay(relayUrl);
1878
+ }
1879
+ isRelayConnected(relayUrl) {
1880
+ return this.mux.isRelayConnected(relayUrl);
1881
+ }
1882
+ setFallbackSince(sinceSeconds) {
1883
+ this.mux.setFallbackSince(this.addressIndex, sinceSeconds);
1884
+ }
1885
+ async fetchPendingEvents() {
1886
+ }
1887
+ onChatReady(handler) {
1888
+ return this.mux.onChatReady(handler);
1889
+ }
1890
+ // ===========================================================================
1891
+ // Dispatch methods — called by MultiAddressTransportMux to route events
1892
+ // ===========================================================================
1893
+ dispatchMessage(message) {
1894
+ if (this.messageHandlers.size === 0) {
1895
+ this.pendingMessages.push(message);
1896
+ return;
1897
+ }
1898
+ for (const handler of this.messageHandlers) {
1899
+ try {
1900
+ handler(message);
1901
+ } catch (e) {
1902
+ logger.debug("MuxAdapter", "Message handler error:", e);
1903
+ }
1904
+ }
1905
+ }
1906
+ dispatchTokenTransfer(transfer) {
1907
+ for (const handler of this.transferHandlers) {
1908
+ try {
1909
+ handler(transfer);
1910
+ } catch (e) {
1911
+ logger.debug("MuxAdapter", "Transfer handler error:", e);
1912
+ }
1913
+ }
1914
+ }
1915
+ dispatchPaymentRequest(request) {
1916
+ for (const handler of this.paymentRequestHandlers) {
1917
+ try {
1918
+ handler(request);
1919
+ } catch (e) {
1920
+ logger.debug("MuxAdapter", "Payment request handler error:", e);
1921
+ }
1922
+ }
1923
+ }
1924
+ dispatchPaymentRequestResponse(response) {
1925
+ for (const handler of this.paymentRequestResponseHandlers) {
1926
+ try {
1927
+ handler(response);
1928
+ } catch (e) {
1929
+ logger.debug("MuxAdapter", "Payment response handler error:", e);
1930
+ }
1931
+ }
1932
+ }
1933
+ dispatchReadReceipt(receipt) {
1934
+ for (const handler of this.readReceiptHandlers) {
1935
+ try {
1936
+ handler(receipt);
1937
+ } catch (e) {
1938
+ logger.debug("MuxAdapter", "Read receipt handler error:", e);
1939
+ }
1940
+ }
1941
+ }
1942
+ dispatchTypingIndicator(indicator) {
1943
+ for (const handler of this.typingIndicatorHandlers) {
1944
+ try {
1945
+ handler(indicator);
1946
+ } catch (e) {
1947
+ logger.debug("MuxAdapter", "Typing handler error:", e);
1948
+ }
1949
+ }
1950
+ }
1951
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1952
+ dispatchComposingIndicator(indicator) {
1953
+ for (const handler of this.composingHandlers) {
1954
+ try {
1955
+ handler(indicator);
1956
+ } catch (e) {
1957
+ logger.debug("MuxAdapter", "Composing handler error:", e);
1958
+ }
1959
+ }
1960
+ }
1961
+ dispatchInstantSplitBundle(bundle) {
1962
+ for (const handler of this.instantSplitBundleHandlers) {
1963
+ try {
1964
+ handler(bundle);
1965
+ } catch (e) {
1966
+ logger.debug("MuxAdapter", "Instant split handler error:", e);
1967
+ }
1968
+ }
1969
+ }
1970
+ emitTransportEvent(event) {
1971
+ for (const cb of this.eventCallbacks) {
1972
+ try {
1973
+ cb(event);
1974
+ } catch {
1975
+ }
1976
+ }
1977
+ }
1978
+ };
1979
+
858
1980
  // modules/payments/L1PaymentsModule.ts
859
1981
  init_errors();
860
1982
  init_constants();
@@ -2656,7 +3778,7 @@ var import_MintCommitment = require("@unicitylabs/state-transition-sdk/lib/trans
2656
3778
  var import_HashAlgorithm2 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
2657
3779
  var import_UnmaskedPredicate2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
2658
3780
  var import_InclusionProofUtils2 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
2659
- var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
3781
+ var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
2660
3782
  var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
2661
3783
  var NametagMinter = class {
2662
3784
  client;
@@ -2680,7 +3802,7 @@ var NametagMinter = class {
2680
3802
  async isNametagAvailable(nametag) {
2681
3803
  try {
2682
3804
  const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
2683
- const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
3805
+ const cleanNametag = (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
2684
3806
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
2685
3807
  const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
2686
3808
  return !isMinted;
@@ -2698,7 +3820,7 @@ var NametagMinter = class {
2698
3820
  */
2699
3821
  async mintNametag(nametag, ownerAddress) {
2700
3822
  const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
2701
- const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
3823
+ const cleanNametag = (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
2702
3824
  this.log(`Starting mint for nametag: ${cleanNametag}`);
2703
3825
  try {
2704
3826
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
@@ -4718,6 +5840,15 @@ var PaymentsModule = class _PaymentsModule {
4718
5840
  this.unsubscribePaymentRequests = null;
4719
5841
  this.unsubscribePaymentRequestResponses?.();
4720
5842
  this.unsubscribePaymentRequestResponses = null;
5843
+ this.stopProofPolling();
5844
+ this.proofPollingJobs.clear();
5845
+ this.stopResolveUnconfirmedPolling();
5846
+ this.unsubscribeStorageEvents();
5847
+ for (const [, resolver] of this.pendingResponseResolvers) {
5848
+ clearTimeout(resolver.timeout);
5849
+ resolver.reject(new Error("Address switched"));
5850
+ }
5851
+ this.pendingResponseResolvers.clear();
4721
5852
  this.tokens.clear();
4722
5853
  this.pendingTransfers.clear();
4723
5854
  this.tombstones = [];
@@ -4766,6 +5897,13 @@ var PaymentsModule = class _PaymentsModule {
4766
5897
  try {
4767
5898
  const result = await provider.load();
4768
5899
  if (result.success && result.data) {
5900
+ const loadedMeta = result.data?._meta;
5901
+ const currentL1 = this.deps.identity.l1Address;
5902
+ const currentChain = this.deps.identity.chainPubkey;
5903
+ if (loadedMeta?.address && currentL1 && loadedMeta.address !== currentL1 && loadedMeta.address !== currentChain) {
5904
+ logger.warn("Payments", `Load: rejecting data from provider ${id} \u2014 address mismatch (got=${loadedMeta.address.slice(0, 20)}... expected=${currentL1.slice(0, 20)}...)`);
5905
+ continue;
5906
+ }
4769
5907
  this.loadFromStorageData(result.data);
4770
5908
  const txfData = result.data;
4771
5909
  if (txfData._history && txfData._history.length > 0) {
@@ -4847,6 +5985,11 @@ var PaymentsModule = class _PaymentsModule {
4847
5985
  */
4848
5986
  async send(request) {
4849
5987
  this.ensureInitialized();
5988
+ let resolveSendTracker;
5989
+ const sendTracker = new Promise((r) => {
5990
+ resolveSendTracker = r;
5991
+ });
5992
+ this.pendingBackgroundTasks.push(sendTracker);
4850
5993
  const result = {
4851
5994
  id: crypto.randomUUID(),
4852
5995
  status: "pending",
@@ -5134,6 +6277,8 @@ var PaymentsModule = class _PaymentsModule {
5134
6277
  }
5135
6278
  this.deps.emitEvent("transfer:failed", result);
5136
6279
  throw error;
6280
+ } finally {
6281
+ resolveSendTracker();
5137
6282
  }
5138
6283
  }
5139
6284
  /**
@@ -6146,9 +7291,12 @@ var PaymentsModule = class _PaymentsModule {
6146
7291
  * Call this before process exit to ensure all tokens are saved.
6147
7292
  */
6148
7293
  async waitForPendingOperations() {
7294
+ logger.debug("Payments", `waitForPendingOperations: ${this.pendingBackgroundTasks.length} pending tasks`);
6149
7295
  if (this.pendingBackgroundTasks.length > 0) {
7296
+ logger.debug("Payments", "waitForPendingOperations: waiting...");
6150
7297
  await Promise.allSettled(this.pendingBackgroundTasks);
6151
7298
  this.pendingBackgroundTasks = [];
7299
+ logger.debug("Payments", "waitForPendingOperations: all tasks completed");
6152
7300
  }
6153
7301
  }
6154
7302
  /**
@@ -7390,6 +8538,13 @@ var PaymentsModule = class _PaymentsModule {
7390
8538
  try {
7391
8539
  const result = await provider.sync(localData);
7392
8540
  if (result.success && result.merged) {
8541
+ const mergedMeta = result.merged?._meta;
8542
+ const currentL1 = this.deps.identity.l1Address;
8543
+ const currentChain = this.deps.identity.chainPubkey;
8544
+ if (mergedMeta?.address && currentL1 && mergedMeta.address !== currentL1 && mergedMeta.address !== currentChain) {
8545
+ logger.warn("Payments", `Sync: rejecting data from provider ${providerId} \u2014 address mismatch (got=${mergedMeta.address.slice(0, 20)}... expected=${currentL1.slice(0, 20)}...)`);
8546
+ continue;
8547
+ }
7393
8548
  const savedTokens = new Map(this.tokens);
7394
8549
  this.loadFromStorageData(result.merged);
7395
8550
  let restoredCount = 0;
@@ -8818,7 +9973,7 @@ function createCommunicationsModule(config) {
8818
9973
  }
8819
9974
 
8820
9975
  // modules/groupchat/GroupChatModule.ts
8821
- var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
9976
+ var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
8822
9977
  init_logger();
8823
9978
  init_errors();
8824
9979
  init_constants();
@@ -8836,7 +9991,7 @@ var GroupVisibility = {
8836
9991
 
8837
9992
  // modules/groupchat/GroupChatModule.ts
8838
9993
  function createNip29Filter(data) {
8839
- return new import_nostr_js_sdk2.Filter(data);
9994
+ return new import_nostr_js_sdk3.Filter(data);
8840
9995
  }
8841
9996
  var GroupChatModule = class {
8842
9997
  config;
@@ -8885,7 +10040,7 @@ var GroupChatModule = class {
8885
10040
  }
8886
10041
  this.deps = deps;
8887
10042
  const secretKey = Buffer.from(deps.identity.privateKey, "hex");
8888
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10043
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
8889
10044
  }
8890
10045
  async load() {
8891
10046
  this.ensureInitialized();
@@ -9020,7 +10175,7 @@ var GroupChatModule = class {
9020
10175
  }
9021
10176
  this.subscriptionIds = [];
9022
10177
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
9023
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10178
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
9024
10179
  if (this.groups.size === 0) {
9025
10180
  await this.restoreJoinedGroups();
9026
10181
  } else {
@@ -9032,13 +10187,13 @@ var GroupChatModule = class {
9032
10187
  this.ensureInitialized();
9033
10188
  if (!this.keyManager) {
9034
10189
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
9035
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10190
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
9036
10191
  }
9037
10192
  const primaryRelay = this.config.relays[0];
9038
10193
  if (primaryRelay) {
9039
10194
  await this.checkAndClearOnRelayChange(primaryRelay);
9040
10195
  }
9041
- this.client = new import_nostr_js_sdk2.NostrClient(this.keyManager);
10196
+ this.client = new import_nostr_js_sdk3.NostrClient(this.keyManager);
9042
10197
  try {
9043
10198
  await this.client.connect(...this.config.relays);
9044
10199
  this.connected = true;
@@ -9297,7 +10452,7 @@ var GroupChatModule = class {
9297
10452
  if (!myPubkey) return [];
9298
10453
  const groupIdsWithMembership = /* @__PURE__ */ new Set();
9299
10454
  await this.oneshotSubscription(
9300
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10455
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9301
10456
  {
9302
10457
  onEvent: (event) => {
9303
10458
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9348,7 +10503,7 @@ var GroupChatModule = class {
9348
10503
  const memberCountsMap = /* @__PURE__ */ new Map();
9349
10504
  await Promise.all([
9350
10505
  this.oneshotSubscription(
9351
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
10506
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
9352
10507
  {
9353
10508
  onEvent: (event) => {
9354
10509
  const group = this.parseGroupMetadata(event);
@@ -9366,7 +10521,7 @@ var GroupChatModule = class {
9366
10521
  }
9367
10522
  ),
9368
10523
  this.oneshotSubscription(
9369
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10524
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9370
10525
  {
9371
10526
  onEvent: (event) => {
9372
10527
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9869,7 +11024,7 @@ var GroupChatModule = class {
9869
11024
  if (!this.client) return /* @__PURE__ */ new Set();
9870
11025
  const adminPubkeys = /* @__PURE__ */ new Set();
9871
11026
  return this.oneshotSubscription(
9872
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
11027
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
9873
11028
  {
9874
11029
  onEvent: (event) => {
9875
11030
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9891,7 +11046,7 @@ var GroupChatModule = class {
9891
11046
  if (!this.client) return null;
9892
11047
  let result = null;
9893
11048
  return this.oneshotSubscription(
9894
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
11049
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
9895
11050
  {
9896
11051
  onEvent: (event) => {
9897
11052
  if (!result) result = this.parseGroupMetadata(event);
@@ -9928,7 +11083,7 @@ var GroupChatModule = class {
9928
11083
  if (!this.client) return [];
9929
11084
  const members = [];
9930
11085
  return this.oneshotSubscription(
9931
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
11086
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
9932
11087
  {
9933
11088
  onEvent: (event) => {
9934
11089
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9949,7 +11104,7 @@ var GroupChatModule = class {
9949
11104
  if (!this.client) return [];
9950
11105
  const adminPubkeys = [];
9951
11106
  return this.oneshotSubscription(
9952
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
11107
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
9953
11108
  {
9954
11109
  onEvent: (event) => {
9955
11110
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -13703,9 +14858,9 @@ var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign
13703
14858
  var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
13704
14859
  var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
13705
14860
  var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
13706
- var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
14861
+ var import_nostr_js_sdk4 = require("@unicitylabs/nostr-js-sdk");
13707
14862
  function isValidNametag(nametag) {
13708
- if ((0, import_nostr_js_sdk3.isPhoneNumber)(nametag)) return true;
14863
+ if ((0, import_nostr_js_sdk4.isPhoneNumber)(nametag)) return true;
13709
14864
  return /^[a-z0-9_-]{3,20}$/.test(nametag);
13710
14865
  }
13711
14866
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
@@ -13749,11 +14904,18 @@ var Sphere = class _Sphere {
13749
14904
  _transport;
13750
14905
  _oracle;
13751
14906
  _priceProvider;
13752
- // Modules
14907
+ // Modules (single-instance — backward compat, delegates to active address)
13753
14908
  _payments;
13754
14909
  _communications;
13755
14910
  _groupChat = null;
13756
14911
  _market = null;
14912
+ // Per-address module instances (Phase 2: independent parallel operation)
14913
+ _addressModules = /* @__PURE__ */ new Map();
14914
+ _transportMux = null;
14915
+ // Stored configs for creating per-address modules
14916
+ _l1Config;
14917
+ _groupChatConfig;
14918
+ _marketConfig;
13757
14919
  // Events
13758
14920
  eventHandlers = /* @__PURE__ */ new Map();
13759
14921
  // Provider management
@@ -13771,6 +14933,9 @@ var Sphere = class _Sphere {
13771
14933
  if (tokenStorage) {
13772
14934
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
13773
14935
  }
14936
+ this._l1Config = l1Config;
14937
+ this._groupChatConfig = groupChatConfig;
14938
+ this._marketConfig = marketConfig;
13774
14939
  this._payments = createPaymentsModule({ l1: l1Config });
13775
14940
  this._communications = createCommunicationsModule();
13776
14941
  this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
@@ -15029,7 +16194,7 @@ var Sphere = class _Sphere {
15029
16194
  nametags.set(0, newNametag);
15030
16195
  }
15031
16196
  const nametag = this._addressNametags.get(addressId)?.get(0);
15032
- this._identity = {
16197
+ const newIdentity = {
15033
16198
  privateKey: addressInfo.privateKey,
15034
16199
  chainPubkey: addressInfo.publicKey,
15035
16200
  l1Address: addressInfo.address,
@@ -15037,20 +16202,53 @@ var Sphere = class _Sphere {
15037
16202
  ipnsName: "12D3KooW" + ipnsHash,
15038
16203
  nametag
15039
16204
  };
16205
+ if (!this._addressModules.has(index)) {
16206
+ logger.debug("Sphere", `switchToAddress(${index}): creating per-address modules (lazy init)`);
16207
+ const addressTokenProviders = /* @__PURE__ */ new Map();
16208
+ for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
16209
+ if (provider.createForAddress) {
16210
+ const newProvider = provider.createForAddress();
16211
+ newProvider.setIdentity(newIdentity);
16212
+ await newProvider.initialize();
16213
+ addressTokenProviders.set(providerId, newProvider);
16214
+ } else {
16215
+ logger.warn("Sphere", `Token storage provider ${providerId} does not support createForAddress, reusing shared instance`);
16216
+ addressTokenProviders.set(providerId, provider);
16217
+ }
16218
+ }
16219
+ await this.initializeAddressModules(index, newIdentity, addressTokenProviders);
16220
+ } else {
16221
+ const moduleSet = this._addressModules.get(index);
16222
+ if (nametag !== moduleSet.identity.nametag) {
16223
+ moduleSet.identity = newIdentity;
16224
+ const addressTransport = moduleSet.transportAdapter ?? this._transport;
16225
+ moduleSet.payments.initialize({
16226
+ identity: newIdentity,
16227
+ storage: this._storage,
16228
+ tokenStorageProviders: moduleSet.tokenStorageProviders,
16229
+ transport: addressTransport,
16230
+ oracle: this._oracle,
16231
+ emitEvent: this.emitEvent.bind(this),
16232
+ chainCode: this._masterKey?.chainCode || void 0,
16233
+ price: this._priceProvider ?? void 0
16234
+ });
16235
+ }
16236
+ }
16237
+ this._identity = newIdentity;
15040
16238
  this._currentAddressIndex = index;
15041
16239
  await this._updateCachedProxyAddress();
16240
+ const activeModules = this._addressModules.get(index);
16241
+ this._payments = activeModules.payments;
16242
+ this._communications = activeModules.communications;
16243
+ this._groupChat = activeModules.groupChat;
16244
+ this._market = activeModules.market;
15042
16245
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
15043
16246
  this._storage.setIdentity(this._identity);
15044
- await this._transport.setIdentity(this._identity);
15045
- logger.debug("Sphere", `switchToAddress(${index}): re-initializing ${this._tokenStorageProviders.size} token storage provider(s)`);
15046
- for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
15047
- logger.debug("Sphere", `switchToAddress(${index}): shutdown provider=${providerId}`);
15048
- await provider.shutdown();
15049
- provider.setIdentity(this._identity);
15050
- logger.debug("Sphere", `switchToAddress(${index}): initialize provider=${providerId}`);
15051
- await provider.initialize();
16247
+ if (this._transport.setFallbackSince) {
16248
+ const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
16249
+ this._transport.setFallbackSince(fallbackTs);
15052
16250
  }
15053
- await this.reinitializeModulesForNewAddress();
16251
+ await this._transport.setIdentity(this._identity);
15054
16252
  this.emitEvent("identity:changed", {
15055
16253
  l1Address: this._identity.l1Address,
15056
16254
  directAddress: this._identity.directAddress,
@@ -15105,42 +16303,104 @@ var Sphere = class _Sphere {
15105
16303
  }
15106
16304
  }
15107
16305
  /**
15108
- * Re-initialize modules after address switch
16306
+ * Create a new set of per-address modules for the given index.
16307
+ * Each address gets its own PaymentsModule, CommunicationsModule, etc.
16308
+ * Modules are fully independent — they have their own token storage,
16309
+ * and can sync/finalize/split in background regardless of active address.
16310
+ *
16311
+ * @param index - HD address index
16312
+ * @param identity - Full identity for this address
16313
+ * @param tokenStorageProviders - Token storage providers for this address
15109
16314
  */
15110
- async reinitializeModulesForNewAddress() {
16315
+ async initializeAddressModules(index, identity, tokenStorageProviders) {
15111
16316
  const emitEvent = this.emitEvent.bind(this);
15112
- this._payments.initialize({
15113
- identity: this._identity,
16317
+ const adapter = await this.ensureTransportMux(index, identity);
16318
+ const addressTransport = adapter ?? this._transport;
16319
+ const payments = createPaymentsModule({ l1: this._l1Config });
16320
+ const communications = createCommunicationsModule();
16321
+ const groupChat = this._groupChatConfig ? createGroupChatModule(this._groupChatConfig) : null;
16322
+ const market = this._marketConfig ? createMarketModule(this._marketConfig) : null;
16323
+ payments.initialize({
16324
+ identity,
15114
16325
  storage: this._storage,
15115
- tokenStorageProviders: this._tokenStorageProviders,
15116
- transport: this._transport,
16326
+ tokenStorageProviders,
16327
+ transport: addressTransport,
15117
16328
  oracle: this._oracle,
15118
16329
  emitEvent,
15119
16330
  chainCode: this._masterKey?.chainCode || void 0,
15120
16331
  price: this._priceProvider ?? void 0
15121
16332
  });
15122
- this._communications.initialize({
15123
- identity: this._identity,
16333
+ communications.initialize({
16334
+ identity,
15124
16335
  storage: this._storage,
15125
- transport: this._transport,
16336
+ transport: addressTransport,
15126
16337
  emitEvent
15127
16338
  });
15128
- this._groupChat?.initialize({
15129
- identity: this._identity,
16339
+ groupChat?.initialize({
16340
+ identity,
15130
16341
  storage: this._storage,
15131
16342
  emitEvent
15132
16343
  });
15133
- this._market?.initialize({
15134
- identity: this._identity,
16344
+ market?.initialize({
16345
+ identity,
15135
16346
  emitEvent
15136
16347
  });
15137
- await this._payments.load();
15138
- await this._communications.load();
15139
- await this._groupChat?.load();
15140
- await this._market?.load();
15141
- this._payments.sync().catch((err) => {
15142
- logger.warn("Sphere", "Post-switch sync failed:", err);
16348
+ await payments.load();
16349
+ await communications.load();
16350
+ await groupChat?.load();
16351
+ await market?.load();
16352
+ const moduleSet = {
16353
+ index,
16354
+ identity,
16355
+ payments,
16356
+ communications,
16357
+ groupChat,
16358
+ market,
16359
+ transportAdapter: adapter,
16360
+ tokenStorageProviders: new Map(tokenStorageProviders),
16361
+ initialized: true
16362
+ };
16363
+ this._addressModules.set(index, moduleSet);
16364
+ logger.debug("Sphere", `Initialized per-address modules for address ${index} (transport: ${adapter ? "mux adapter" : "primary"})`);
16365
+ payments.sync().catch((err) => {
16366
+ logger.warn("Sphere", `Post-init sync failed for address ${index}:`, err);
15143
16367
  });
16368
+ return moduleSet;
16369
+ }
16370
+ /**
16371
+ * Ensure the transport multiplexer exists and register an address.
16372
+ * Creates the mux on first call. Returns an AddressTransportAdapter
16373
+ * that routes events for this address independently.
16374
+ * @returns AddressTransportAdapter or null if transport is not Nostr-based
16375
+ */
16376
+ async ensureTransportMux(index, identity) {
16377
+ const transport = this._transport;
16378
+ if (typeof transport.getWebSocketFactory !== "function" || typeof transport.getConfiguredRelays !== "function") {
16379
+ logger.debug("Sphere", "Transport does not support mux interface, skipping");
16380
+ return null;
16381
+ }
16382
+ const nostrTransport = transport;
16383
+ if (!this._transportMux) {
16384
+ this._transportMux = new MultiAddressTransportMux({
16385
+ relays: nostrTransport.getConfiguredRelays(),
16386
+ createWebSocket: nostrTransport.getWebSocketFactory(),
16387
+ storage: nostrTransport.getStorageAdapter() ?? void 0
16388
+ });
16389
+ await this._transportMux.connect();
16390
+ if (typeof nostrTransport.suppressSubscriptions === "function") {
16391
+ nostrTransport.suppressSubscriptions();
16392
+ }
16393
+ logger.debug("Sphere", "Transport mux created and connected");
16394
+ }
16395
+ const adapter = await this._transportMux.addAddress(index, identity, this._transport);
16396
+ return adapter;
16397
+ }
16398
+ /**
16399
+ * Get per-address modules for any address index (creates lazily if needed).
16400
+ * This allows accessing any address's modules without switching.
16401
+ */
16402
+ getAddressPayments(index) {
16403
+ return this._addressModules.get(index)?.payments;
15144
16404
  }
15145
16405
  /**
15146
16406
  * Derive address at a specific index
@@ -16061,17 +17321,40 @@ var Sphere = class _Sphere {
16061
17321
  */
16062
17322
  cleanNametag(raw) {
16063
17323
  const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
16064
- return (0, import_nostr_js_sdk3.normalizeNametag)(stripped);
17324
+ return (0, import_nostr_js_sdk4.normalizeNametag)(stripped);
16065
17325
  }
16066
17326
  // ===========================================================================
16067
17327
  // Public Methods - Lifecycle
16068
17328
  // ===========================================================================
16069
17329
  async destroy() {
16070
17330
  this.cleanupProviderEventSubscriptions();
17331
+ for (const [idx, moduleSet] of this._addressModules.entries()) {
17332
+ try {
17333
+ moduleSet.payments.destroy();
17334
+ moduleSet.communications.destroy();
17335
+ moduleSet.groupChat?.destroy();
17336
+ moduleSet.market?.destroy();
17337
+ for (const provider of moduleSet.tokenStorageProviders.values()) {
17338
+ try {
17339
+ await provider.shutdown();
17340
+ } catch {
17341
+ }
17342
+ }
17343
+ moduleSet.tokenStorageProviders.clear();
17344
+ logger.debug("Sphere", `Destroyed modules for address ${idx}`);
17345
+ } catch (err) {
17346
+ logger.warn("Sphere", `Error destroying modules for address ${idx}:`, err);
17347
+ }
17348
+ }
17349
+ this._addressModules.clear();
16071
17350
  this._payments.destroy();
16072
17351
  this._communications.destroy();
16073
17352
  this._groupChat?.destroy();
16074
17353
  this._market?.destroy();
17354
+ if (this._transportMux) {
17355
+ await this._transportMux.disconnect();
17356
+ this._transportMux = null;
17357
+ }
16075
17358
  await this._transport.disconnect();
16076
17359
  await this._storage.disconnect();
16077
17360
  await this._oracle.disconnect();
@@ -16266,6 +17549,9 @@ var Sphere = class _Sphere {
16266
17549
  // ===========================================================================
16267
17550
  async initializeProviders() {
16268
17551
  this._storage.setIdentity(this._identity);
17552
+ if (this._transport.setFallbackSince) {
17553
+ this._transport.setFallbackSince(Math.floor(Date.now() / 1e3) - 86400);
17554
+ }
16269
17555
  await this._transport.setIdentity(this._identity);
16270
17556
  for (const provider of this._tokenStorageProviders.values()) {
16271
17557
  provider.setIdentity(this._identity);
@@ -16356,11 +17642,13 @@ var Sphere = class _Sphere {
16356
17642
  }
16357
17643
  async initializeModules() {
16358
17644
  const emitEvent = this.emitEvent.bind(this);
17645
+ const adapter = await this.ensureTransportMux(this._currentAddressIndex, this._identity);
17646
+ const moduleTransport = adapter ?? this._transport;
16359
17647
  this._payments.initialize({
16360
17648
  identity: this._identity,
16361
17649
  storage: this._storage,
16362
17650
  tokenStorageProviders: this._tokenStorageProviders,
16363
- transport: this._transport,
17651
+ transport: moduleTransport,
16364
17652
  oracle: this._oracle,
16365
17653
  emitEvent,
16366
17654
  // Pass chain code for L1 HD derivation
@@ -16371,7 +17659,7 @@ var Sphere = class _Sphere {
16371
17659
  this._communications.initialize({
16372
17660
  identity: this._identity,
16373
17661
  storage: this._storage,
16374
- transport: this._transport,
17662
+ transport: moduleTransport,
16375
17663
  emitEvent
16376
17664
  });
16377
17665
  this._groupChat?.initialize({
@@ -16387,6 +17675,17 @@ var Sphere = class _Sphere {
16387
17675
  await this._communications.load();
16388
17676
  await this._groupChat?.load();
16389
17677
  await this._market?.load();
17678
+ this._addressModules.set(this._currentAddressIndex, {
17679
+ index: this._currentAddressIndex,
17680
+ identity: this._identity,
17681
+ payments: this._payments,
17682
+ communications: this._communications,
17683
+ groupChat: this._groupChat,
17684
+ market: this._market,
17685
+ transportAdapter: adapter,
17686
+ tokenStorageProviders: new Map(this._tokenStorageProviders),
17687
+ initialized: true
17688
+ });
16390
17689
  }
16391
17690
  // ===========================================================================
16392
17691
  // Private: Helpers