@unicitylabs/sphere-sdk 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -8401,6 +9556,12 @@ var CommunicationsModule = class {
8401
9556
  this.unsubscribeComposing = deps.transport.onComposing?.((indicator) => {
8402
9557
  this.handleComposingIndicator(indicator);
8403
9558
  }) ?? null;
9559
+ if (deps.transport.onChatReady) {
9560
+ deps.transport.onChatReady(() => {
9561
+ const conversations = this.getConversations();
9562
+ deps.emitEvent("communications:ready", { conversationCount: conversations.size });
9563
+ });
9564
+ }
8404
9565
  }
8405
9566
  /**
8406
9567
  * Load messages from storage.
@@ -8812,7 +9973,7 @@ function createCommunicationsModule(config) {
8812
9973
  }
8813
9974
 
8814
9975
  // modules/groupchat/GroupChatModule.ts
8815
- var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
9976
+ var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
8816
9977
  init_logger();
8817
9978
  init_errors();
8818
9979
  init_constants();
@@ -8830,7 +9991,7 @@ var GroupVisibility = {
8830
9991
 
8831
9992
  // modules/groupchat/GroupChatModule.ts
8832
9993
  function createNip29Filter(data) {
8833
- return new import_nostr_js_sdk2.Filter(data);
9994
+ return new import_nostr_js_sdk3.Filter(data);
8834
9995
  }
8835
9996
  var GroupChatModule = class {
8836
9997
  config;
@@ -8879,7 +10040,7 @@ var GroupChatModule = class {
8879
10040
  }
8880
10041
  this.deps = deps;
8881
10042
  const secretKey = Buffer.from(deps.identity.privateKey, "hex");
8882
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10043
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
8883
10044
  }
8884
10045
  async load() {
8885
10046
  this.ensureInitialized();
@@ -9014,7 +10175,7 @@ var GroupChatModule = class {
9014
10175
  }
9015
10176
  this.subscriptionIds = [];
9016
10177
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
9017
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10178
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
9018
10179
  if (this.groups.size === 0) {
9019
10180
  await this.restoreJoinedGroups();
9020
10181
  } else {
@@ -9026,13 +10187,13 @@ var GroupChatModule = class {
9026
10187
  this.ensureInitialized();
9027
10188
  if (!this.keyManager) {
9028
10189
  const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
9029
- this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
10190
+ this.keyManager = import_nostr_js_sdk3.NostrKeyManager.fromPrivateKey(secretKey);
9030
10191
  }
9031
10192
  const primaryRelay = this.config.relays[0];
9032
10193
  if (primaryRelay) {
9033
10194
  await this.checkAndClearOnRelayChange(primaryRelay);
9034
10195
  }
9035
- this.client = new import_nostr_js_sdk2.NostrClient(this.keyManager);
10196
+ this.client = new import_nostr_js_sdk3.NostrClient(this.keyManager);
9036
10197
  try {
9037
10198
  await this.client.connect(...this.config.relays);
9038
10199
  this.connected = true;
@@ -9043,6 +10204,7 @@ var GroupChatModule = class {
9043
10204
  await this.subscribeToJoinedGroups();
9044
10205
  }
9045
10206
  this.deps.emitEvent("groupchat:connection", { connected: true });
10207
+ this.deps.emitEvent("groupchat:ready", { groupCount: this.groups.size });
9046
10208
  } catch (error) {
9047
10209
  logger.error("GroupChat", "Failed to connect to relays", error);
9048
10210
  this.deps.emitEvent("groupchat:connection", { connected: false });
@@ -9290,7 +10452,7 @@ var GroupChatModule = class {
9290
10452
  if (!myPubkey) return [];
9291
10453
  const groupIdsWithMembership = /* @__PURE__ */ new Set();
9292
10454
  await this.oneshotSubscription(
9293
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10455
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9294
10456
  {
9295
10457
  onEvent: (event) => {
9296
10458
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9341,7 +10503,7 @@ var GroupChatModule = class {
9341
10503
  const memberCountsMap = /* @__PURE__ */ new Map();
9342
10504
  await Promise.all([
9343
10505
  this.oneshotSubscription(
9344
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
10506
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
9345
10507
  {
9346
10508
  onEvent: (event) => {
9347
10509
  const group = this.parseGroupMetadata(event);
@@ -9359,7 +10521,7 @@ var GroupChatModule = class {
9359
10521
  }
9360
10522
  ),
9361
10523
  this.oneshotSubscription(
9362
- new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
10524
+ new import_nostr_js_sdk3.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
9363
10525
  {
9364
10526
  onEvent: (event) => {
9365
10527
  const groupId = this.getGroupIdFromMetadataEvent(event);
@@ -9677,6 +10839,19 @@ var GroupChatModule = class {
9677
10839
  getMessages(groupId) {
9678
10840
  return (this.messages.get(groupId) || []).sort((a, b) => a.timestamp - b.timestamp);
9679
10841
  }
10842
+ getMessagesPage(groupId, options) {
10843
+ const limit = options?.limit ?? 20;
10844
+ const before = options?.before ?? Infinity;
10845
+ const groupMessages = this.messages.get(groupId) ?? [];
10846
+ const filtered = groupMessages.filter((m) => m.timestamp < before).sort((a, b) => b.timestamp - a.timestamp);
10847
+ const page = filtered.slice(0, limit);
10848
+ return {
10849
+ messages: page.reverse(),
10850
+ // chronological order
10851
+ hasMore: filtered.length > limit,
10852
+ oldestTimestamp: page.length > 0 ? page[0].timestamp : null
10853
+ };
10854
+ }
9680
10855
  getMembers(groupId) {
9681
10856
  return (this.members.get(groupId) || []).sort((a, b) => a.joinedAt - b.joinedAt);
9682
10857
  }
@@ -9849,7 +11024,7 @@ var GroupChatModule = class {
9849
11024
  if (!this.client) return /* @__PURE__ */ new Set();
9850
11025
  const adminPubkeys = /* @__PURE__ */ new Set();
9851
11026
  return this.oneshotSubscription(
9852
- 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": ["", "_"] }),
9853
11028
  {
9854
11029
  onEvent: (event) => {
9855
11030
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9871,7 +11046,7 @@ var GroupChatModule = class {
9871
11046
  if (!this.client) return null;
9872
11047
  let result = null;
9873
11048
  return this.oneshotSubscription(
9874
- 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] }),
9875
11050
  {
9876
11051
  onEvent: (event) => {
9877
11052
  if (!result) result = this.parseGroupMetadata(event);
@@ -9908,7 +11083,7 @@ var GroupChatModule = class {
9908
11083
  if (!this.client) return [];
9909
11084
  const members = [];
9910
11085
  return this.oneshotSubscription(
9911
- 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] }),
9912
11087
  {
9913
11088
  onEvent: (event) => {
9914
11089
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -9929,7 +11104,7 @@ var GroupChatModule = class {
9929
11104
  if (!this.client) return [];
9930
11105
  const adminPubkeys = [];
9931
11106
  return this.oneshotSubscription(
9932
- 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] }),
9933
11108
  {
9934
11109
  onEvent: (event) => {
9935
11110
  const pTags = event.tags.filter((t) => t[0] === "p");
@@ -13683,9 +14858,9 @@ var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign
13683
14858
  var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
13684
14859
  var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
13685
14860
  var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
13686
- var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
14861
+ var import_nostr_js_sdk4 = require("@unicitylabs/nostr-js-sdk");
13687
14862
  function isValidNametag(nametag) {
13688
- if ((0, import_nostr_js_sdk3.isPhoneNumber)(nametag)) return true;
14863
+ if ((0, import_nostr_js_sdk4.isPhoneNumber)(nametag)) return true;
13689
14864
  return /^[a-z0-9_-]{3,20}$/.test(nametag);
13690
14865
  }
13691
14866
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
@@ -13729,11 +14904,18 @@ var Sphere = class _Sphere {
13729
14904
  _transport;
13730
14905
  _oracle;
13731
14906
  _priceProvider;
13732
- // Modules
14907
+ // Modules (single-instance — backward compat, delegates to active address)
13733
14908
  _payments;
13734
14909
  _communications;
13735
14910
  _groupChat = null;
13736
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;
13737
14919
  // Events
13738
14920
  eventHandlers = /* @__PURE__ */ new Map();
13739
14921
  // Provider management
@@ -13751,6 +14933,9 @@ var Sphere = class _Sphere {
13751
14933
  if (tokenStorage) {
13752
14934
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
13753
14935
  }
14936
+ this._l1Config = l1Config;
14937
+ this._groupChatConfig = groupChatConfig;
14938
+ this._marketConfig = marketConfig;
13754
14939
  this._payments = createPaymentsModule({ l1: l1Config });
13755
14940
  this._communications = createCommunicationsModule();
13756
14941
  this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
@@ -15009,7 +16194,7 @@ var Sphere = class _Sphere {
15009
16194
  nametags.set(0, newNametag);
15010
16195
  }
15011
16196
  const nametag = this._addressNametags.get(addressId)?.get(0);
15012
- this._identity = {
16197
+ const newIdentity = {
15013
16198
  privateKey: addressInfo.privateKey,
15014
16199
  chainPubkey: addressInfo.publicKey,
15015
16200
  l1Address: addressInfo.address,
@@ -15017,20 +16202,53 @@ var Sphere = class _Sphere {
15017
16202
  ipnsName: "12D3KooW" + ipnsHash,
15018
16203
  nametag
15019
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;
15020
16238
  this._currentAddressIndex = index;
15021
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;
15022
16245
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
15023
16246
  this._storage.setIdentity(this._identity);
15024
- await this._transport.setIdentity(this._identity);
15025
- logger.debug("Sphere", `switchToAddress(${index}): re-initializing ${this._tokenStorageProviders.size} token storage provider(s)`);
15026
- for (const [providerId, provider] of this._tokenStorageProviders.entries()) {
15027
- logger.debug("Sphere", `switchToAddress(${index}): shutdown provider=${providerId}`);
15028
- await provider.shutdown();
15029
- provider.setIdentity(this._identity);
15030
- logger.debug("Sphere", `switchToAddress(${index}): initialize provider=${providerId}`);
15031
- await provider.initialize();
16247
+ if (this._transport.setFallbackSince) {
16248
+ const fallbackTs = Math.floor(Date.now() / 1e3) - 86400;
16249
+ this._transport.setFallbackSince(fallbackTs);
15032
16250
  }
15033
- await this.reinitializeModulesForNewAddress();
16251
+ await this._transport.setIdentity(this._identity);
15034
16252
  this.emitEvent("identity:changed", {
15035
16253
  l1Address: this._identity.l1Address,
15036
16254
  directAddress: this._identity.directAddress,
@@ -15085,42 +16303,104 @@ var Sphere = class _Sphere {
15085
16303
  }
15086
16304
  }
15087
16305
  /**
15088
- * 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
15089
16314
  */
15090
- async reinitializeModulesForNewAddress() {
16315
+ async initializeAddressModules(index, identity, tokenStorageProviders) {
15091
16316
  const emitEvent = this.emitEvent.bind(this);
15092
- this._payments.initialize({
15093
- 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,
15094
16325
  storage: this._storage,
15095
- tokenStorageProviders: this._tokenStorageProviders,
15096
- transport: this._transport,
16326
+ tokenStorageProviders,
16327
+ transport: addressTransport,
15097
16328
  oracle: this._oracle,
15098
16329
  emitEvent,
15099
16330
  chainCode: this._masterKey?.chainCode || void 0,
15100
16331
  price: this._priceProvider ?? void 0
15101
16332
  });
15102
- this._communications.initialize({
15103
- identity: this._identity,
16333
+ communications.initialize({
16334
+ identity,
15104
16335
  storage: this._storage,
15105
- transport: this._transport,
16336
+ transport: addressTransport,
15106
16337
  emitEvent
15107
16338
  });
15108
- this._groupChat?.initialize({
15109
- identity: this._identity,
16339
+ groupChat?.initialize({
16340
+ identity,
15110
16341
  storage: this._storage,
15111
16342
  emitEvent
15112
16343
  });
15113
- this._market?.initialize({
15114
- identity: this._identity,
16344
+ market?.initialize({
16345
+ identity,
15115
16346
  emitEvent
15116
16347
  });
15117
- await this._payments.load();
15118
- await this._communications.load();
15119
- await this._groupChat?.load();
15120
- await this._market?.load();
15121
- this._payments.sync().catch((err) => {
15122
- 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);
15123
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;
15124
16404
  }
15125
16405
  /**
15126
16406
  * Derive address at a specific index
@@ -16041,17 +17321,40 @@ var Sphere = class _Sphere {
16041
17321
  */
16042
17322
  cleanNametag(raw) {
16043
17323
  const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
16044
- return (0, import_nostr_js_sdk3.normalizeNametag)(stripped);
17324
+ return (0, import_nostr_js_sdk4.normalizeNametag)(stripped);
16045
17325
  }
16046
17326
  // ===========================================================================
16047
17327
  // Public Methods - Lifecycle
16048
17328
  // ===========================================================================
16049
17329
  async destroy() {
16050
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();
16051
17350
  this._payments.destroy();
16052
17351
  this._communications.destroy();
16053
17352
  this._groupChat?.destroy();
16054
17353
  this._market?.destroy();
17354
+ if (this._transportMux) {
17355
+ await this._transportMux.disconnect();
17356
+ this._transportMux = null;
17357
+ }
16055
17358
  await this._transport.disconnect();
16056
17359
  await this._storage.disconnect();
16057
17360
  await this._oracle.disconnect();
@@ -16246,6 +17549,9 @@ var Sphere = class _Sphere {
16246
17549
  // ===========================================================================
16247
17550
  async initializeProviders() {
16248
17551
  this._storage.setIdentity(this._identity);
17552
+ if (this._transport.setFallbackSince) {
17553
+ this._transport.setFallbackSince(Math.floor(Date.now() / 1e3) - 86400);
17554
+ }
16249
17555
  await this._transport.setIdentity(this._identity);
16250
17556
  for (const provider of this._tokenStorageProviders.values()) {
16251
17557
  provider.setIdentity(this._identity);
@@ -16336,11 +17642,13 @@ var Sphere = class _Sphere {
16336
17642
  }
16337
17643
  async initializeModules() {
16338
17644
  const emitEvent = this.emitEvent.bind(this);
17645
+ const adapter = await this.ensureTransportMux(this._currentAddressIndex, this._identity);
17646
+ const moduleTransport = adapter ?? this._transport;
16339
17647
  this._payments.initialize({
16340
17648
  identity: this._identity,
16341
17649
  storage: this._storage,
16342
17650
  tokenStorageProviders: this._tokenStorageProviders,
16343
- transport: this._transport,
17651
+ transport: moduleTransport,
16344
17652
  oracle: this._oracle,
16345
17653
  emitEvent,
16346
17654
  // Pass chain code for L1 HD derivation
@@ -16351,7 +17659,7 @@ var Sphere = class _Sphere {
16351
17659
  this._communications.initialize({
16352
17660
  identity: this._identity,
16353
17661
  storage: this._storage,
16354
- transport: this._transport,
17662
+ transport: moduleTransport,
16355
17663
  emitEvent
16356
17664
  });
16357
17665
  this._groupChat?.initialize({
@@ -16367,6 +17675,17 @@ var Sphere = class _Sphere {
16367
17675
  await this._communications.load();
16368
17676
  await this._groupChat?.load();
16369
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
+ });
16370
17689
  }
16371
17690
  // ===========================================================================
16372
17691
  // Private: Helpers