@tezos-x/octez.connect-dapp 5.0.3 → 5.0.5

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.
@@ -46,6 +46,7 @@ class DappP2PTransport extends octez_connect_transport_matrix_1.P2PTransport {
46
46
  return __awaiter(this, void 0, void 0, function* () {
47
47
  logger.log('stopListeningForNewPeers');
48
48
  this.newPeerListener = undefined;
49
+ this.client.clearPendingPairings();
49
50
  });
50
51
  }
51
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DappP2PTransport.js","sourceRoot":"","sources":["../../../src/transports/DappP2PTransport.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sEAMqC;AACrC,oEAAoD;AACpD,4FAAsE;AAGtE,MAAM,MAAM,GAAG,IAAI,2BAAM,CAAC,kBAAkB,CAAC,CAAA;AAE7C;;;;GAIG;AACH,MAAa,gBAAiB,SAAQ,6CAGrC;IACC,YACE,IAAY,EACZ,OAAgB,EAChB,OAAgB,EAChB,WAA8B,EAC9B,OAAgB,EAChB,MAAe;QAEf,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,gCAAU,CAAC,wBAAwB,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IAClG,CAAC;IAEY,wBAAwB;;YACnC,OAAO,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAO,IAAI,EAAE,EAAE;gBACxD,MAAM,CAAC,GAAG,CAAC,kBAAkB,EAAE,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;gBAE/D,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAExB,IAAI,CAAC,YAAY,GAAG,qCAAe,CAAC,SAAS,CAAA;gBAE7C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;oBAC1B,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA,CAAC,uCAAuC;gBAC1E,CAAC;YACH,CAAC,CAAA,CAAC,CAAA;QACJ,CAAC;KAAA;IAEY,gBAAgB,CAC3B,eAA2D;;YAE3D,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;YAC9B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACxC,CAAC;KAAA;IAEY,wBAAwB;;YACnC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YACtC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA;QAClC,CAAC;KAAA;CACF;AAzCD,4CAyCC","sourcesContent":["import {\n Storage,\n StorageKey,\n TransportStatus,\n ExtendedP2PPairingResponse,\n NodeDistributions\n} from '@tezos-x/octez.connect-types'\nimport { Logger } from '@tezos-x/octez.connect-core'\nimport { P2PTransport } from '@tezos-x/octez.connect-transport-matrix'\nimport { KeyPair } from '@tezos-x/octez.connect-utils'\n\nconst logger = new Logger('DappP2PTransport')\n\n/**\n * @internalapi\n *\n *\n */\nexport class DappP2PTransport extends P2PTransport<\n ExtendedP2PPairingResponse,\n StorageKey.TRANSPORT_P2P_PEERS_DAPP\n> {\n constructor(\n name: string,\n keyPair: KeyPair,\n storage: Storage,\n matrixNodes: NodeDistributions,\n iconUrl?: string,\n appUrl?: string\n ) {\n super(name, keyPair, storage, matrixNodes, StorageKey.TRANSPORT_P2P_PEERS_DAPP, iconUrl, appUrl)\n }\n\n public async startOpenChannelListener(): Promise<void> {\n return this.client.listenForChannelOpening(async (peer) => {\n logger.log('listenForNewPeer', `new publicKey`, peer.publicKey)\n\n await this.addPeer(peer)\n\n this._isConnected = TransportStatus.CONNECTED\n\n if (this.newPeerListener) {\n this.newPeerListener(peer)\n this.newPeerListener = undefined // TODO: Remove this once we use the id\n }\n })\n }\n\n public async listenForNewPeer(\n newPeerListener: (peer: ExtendedP2PPairingResponse) => void\n ): Promise<void> {\n logger.log('listenForNewPeer')\n this.newPeerListener = newPeerListener\n }\n\n public async stopListeningForNewPeers(): Promise<void> {\n logger.log('stopListeningForNewPeers')\n this.newPeerListener = undefined\n }\n}\n"]}
1
+ {"version":3,"file":"DappP2PTransport.js","sourceRoot":"","sources":["../../../src/transports/DappP2PTransport.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sEAMqC;AACrC,oEAAoD;AACpD,4FAAsE;AAGtE,MAAM,MAAM,GAAG,IAAI,2BAAM,CAAC,kBAAkB,CAAC,CAAA;AAE7C;;;;GAIG;AACH,MAAa,gBAAiB,SAAQ,6CAGrC;IACC,YACE,IAAY,EACZ,OAAgB,EAChB,OAAgB,EAChB,WAA8B,EAC9B,OAAgB,EAChB,MAAe;QAEf,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,gCAAU,CAAC,wBAAwB,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IAClG,CAAC;IAEY,wBAAwB;;YACnC,OAAO,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAO,IAAI,EAAE,EAAE;gBACxD,MAAM,CAAC,GAAG,CAAC,kBAAkB,EAAE,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;gBAE/D,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAExB,IAAI,CAAC,YAAY,GAAG,qCAAe,CAAC,SAAS,CAAA;gBAE7C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;oBAC1B,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA,CAAC,uCAAuC;gBAC1E,CAAC;YACH,CAAC,CAAA,CAAC,CAAA;QACJ,CAAC;KAAA;IAEY,gBAAgB,CAC3B,eAA2D;;YAE3D,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;YAC9B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACxC,CAAC;KAAA;IAEY,wBAAwB;;YACnC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YACtC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA;YAChC,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAA;QACpC,CAAC;KAAA;CACF;AA1CD,4CA0CC","sourcesContent":["import {\n Storage,\n StorageKey,\n TransportStatus,\n ExtendedP2PPairingResponse,\n NodeDistributions\n} from '@tezos-x/octez.connect-types'\nimport { Logger } from '@tezos-x/octez.connect-core'\nimport { P2PTransport } from '@tezos-x/octez.connect-transport-matrix'\nimport { KeyPair } from '@tezos-x/octez.connect-utils'\n\nconst logger = new Logger('DappP2PTransport')\n\n/**\n * @internalapi\n *\n *\n */\nexport class DappP2PTransport extends P2PTransport<\n ExtendedP2PPairingResponse,\n StorageKey.TRANSPORT_P2P_PEERS_DAPP\n> {\n constructor(\n name: string,\n keyPair: KeyPair,\n storage: Storage,\n matrixNodes: NodeDistributions,\n iconUrl?: string,\n appUrl?: string\n ) {\n super(name, keyPair, storage, matrixNodes, StorageKey.TRANSPORT_P2P_PEERS_DAPP, iconUrl, appUrl)\n }\n\n public async startOpenChannelListener(): Promise<void> {\n return this.client.listenForChannelOpening(async (peer) => {\n logger.log('listenForNewPeer', `new publicKey`, peer.publicKey)\n\n await this.addPeer(peer)\n\n this._isConnected = TransportStatus.CONNECTED\n\n if (this.newPeerListener) {\n this.newPeerListener(peer)\n this.newPeerListener = undefined // TODO: Remove this once we use the id\n }\n })\n }\n\n public async listenForNewPeer(\n newPeerListener: (peer: ExtendedP2PPairingResponse) => void\n ): Promise<void> {\n logger.log('listenForNewPeer')\n this.newPeerListener = newPeerListener\n }\n\n public async stopListeningForNewPeers(): Promise<void> {\n logger.log('stopListeningForNewPeers')\n this.newPeerListener = undefined\n this.client.clearPendingPairings()\n }\n}\n"]}
@@ -46,6 +46,7 @@ class DappPostMessageTransport extends octez_connect_transport_postmessage_1.Pos
46
46
  return __awaiter(this, void 0, void 0, function* () {
47
47
  logger.log('stopListeningForNewPeers');
48
48
  this.newPeerListener = undefined;
49
+ this.client.clearPendingPairings();
49
50
  });
50
51
  }
51
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DappPostMessageTransport.js","sourceRoot":"","sources":["../../../src/transports/DappPostMessageTransport.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sEAKqC;AACrC,oEAAoD;AACpD,sGAAmF;AAGnF,MAAM,MAAM,GAAG,IAAI,2BAAM,CAAC,0BAA0B,CAAC,CAAA;AAErD;;;;GAIG;AACH,MAAa,wBAAyB,SAAQ,0DAG7C;IACC,YAAY,IAAY,EAAE,OAAgB,EAAE,OAAgB;QAC1D,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAU,CAAC,gCAAgC,CAAC,CAAA;IAC5E,CAAC;IAEY,wBAAwB;;YACnC,OAAO,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAO,IAAwC,EAAE,EAAE;gBAC5F,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,qCAAqC,EAAE,IAAI,CAAC,CAAA;gBAElE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAExB,IAAI,CAAC,YAAY,GAAG,qCAAe,CAAC,SAAS,CAAA;gBAE7C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;oBAC1B,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA,CAAC,uCAAuC;gBAC1E,CAAC;YACH,CAAC,CAAA,CAAC,CAAA;QACJ,CAAC;KAAA;IAEY,gBAAgB,CAC3B,eAAmE;;YAEnE,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;YAC9B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACxC,CAAC;KAAA;IAEY,wBAAwB;;YACnC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YACtC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA;QAClC,CAAC;KAAA;CACF;AAlCD,4DAkCC","sourcesContent":["import {\n TransportStatus,\n StorageKey,\n Storage,\n ExtendedPostMessagePairingResponse\n} from '@tezos-x/octez.connect-types'\nimport { Logger } from '@tezos-x/octez.connect-core'\nimport { PostMessageTransport } from '@tezos-x/octez.connect-transport-postmessage'\nimport { KeyPair } from '@tezos-x/octez.connect-utils'\n\nconst logger = new Logger('DappPostMessageTransport')\n\n/**\n * @internalapi\n *\n *\n */\nexport class DappPostMessageTransport extends PostMessageTransport<\n ExtendedPostMessagePairingResponse,\n StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP\n> {\n constructor(name: string, keyPair: KeyPair, storage: Storage) {\n super(name, keyPair, storage, StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP)\n }\n\n public async startOpenChannelListener(): Promise<void> {\n return this.client.listenForChannelOpening(async (peer: ExtendedPostMessagePairingResponse) => {\n logger.log('connect', `received PostMessagePairingResponse`, peer)\n\n await this.addPeer(peer)\n\n this._isConnected = TransportStatus.CONNECTED\n\n if (this.newPeerListener) {\n this.newPeerListener(peer)\n this.newPeerListener = undefined // TODO: Remove this once we use the id\n }\n })\n }\n\n public async listenForNewPeer(\n newPeerListener: (peer: ExtendedPostMessagePairingResponse) => void\n ): Promise<void> {\n logger.log('listenForNewPeer')\n this.newPeerListener = newPeerListener\n }\n\n public async stopListeningForNewPeers(): Promise<void> {\n logger.log('stopListeningForNewPeers')\n this.newPeerListener = undefined\n }\n}\n"]}
1
+ {"version":3,"file":"DappPostMessageTransport.js","sourceRoot":"","sources":["../../../src/transports/DappPostMessageTransport.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sEAKqC;AACrC,oEAAoD;AACpD,sGAAmF;AAGnF,MAAM,MAAM,GAAG,IAAI,2BAAM,CAAC,0BAA0B,CAAC,CAAA;AAErD;;;;GAIG;AACH,MAAa,wBAAyB,SAAQ,0DAG7C;IACC,YAAY,IAAY,EAAE,OAAgB,EAAE,OAAgB;QAC1D,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAU,CAAC,gCAAgC,CAAC,CAAA;IAC5E,CAAC;IAEY,wBAAwB;;YACnC,OAAO,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAO,IAAwC,EAAE,EAAE;gBAC5F,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,qCAAqC,EAAE,IAAI,CAAC,CAAA;gBAElE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAExB,IAAI,CAAC,YAAY,GAAG,qCAAe,CAAC,SAAS,CAAA;gBAE7C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;oBAC1B,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA,CAAC,uCAAuC;gBAC1E,CAAC;YACH,CAAC,CAAA,CAAC,CAAA;QACJ,CAAC;KAAA;IAEY,gBAAgB,CAC3B,eAAmE;;YAEnE,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;YAC9B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACxC,CAAC;KAAA;IAEY,wBAAwB;;YACnC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YACtC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA;YAChC,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAA;QACpC,CAAC;KAAA;CACF;AAnCD,4DAmCC","sourcesContent":["import {\n TransportStatus,\n StorageKey,\n Storage,\n ExtendedPostMessagePairingResponse\n} from '@tezos-x/octez.connect-types'\nimport { Logger } from '@tezos-x/octez.connect-core'\nimport { PostMessageTransport } from '@tezos-x/octez.connect-transport-postmessage'\nimport { KeyPair } from '@tezos-x/octez.connect-utils'\n\nconst logger = new Logger('DappPostMessageTransport')\n\n/**\n * @internalapi\n *\n *\n */\nexport class DappPostMessageTransport extends PostMessageTransport<\n ExtendedPostMessagePairingResponse,\n StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP\n> {\n constructor(name: string, keyPair: KeyPair, storage: Storage) {\n super(name, keyPair, storage, StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP)\n }\n\n public async startOpenChannelListener(): Promise<void> {\n return this.client.listenForChannelOpening(async (peer: ExtendedPostMessagePairingResponse) => {\n logger.log('connect', `received PostMessagePairingResponse`, peer)\n\n await this.addPeer(peer)\n\n this._isConnected = TransportStatus.CONNECTED\n\n if (this.newPeerListener) {\n this.newPeerListener(peer)\n this.newPeerListener = undefined // TODO: Remove this once we use the id\n }\n })\n }\n\n public async listenForNewPeer(\n newPeerListener: (peer: ExtendedPostMessagePairingResponse) => void\n ): Promise<void> {\n logger.log('listenForNewPeer')\n this.newPeerListener = newPeerListener\n }\n\n public async stopListeningForNewPeers(): Promise<void> {\n logger.log('stopListeningForNewPeers')\n this.newPeerListener = undefined\n this.client.clearPendingPairings()\n }\n}\n"]}
@@ -88,6 +88,18 @@ export declare class DAppClient extends Client {
88
88
  private prepareRequest;
89
89
  private createStateSnapshot;
90
90
  private initUserID;
91
+ /**
92
+ * Disconnect the postMessage and P2P transports and drop all three instances.
93
+ *
94
+ * A dropped transport is not garbage. Its communication client keeps its
95
+ * `window` handlers (postMessage) or its matrix sync (P2P) until it is told to
96
+ * stop, so every path that used to just forget the instances left one more
97
+ * live client behind per connection. Every such path goes through here.
98
+ *
99
+ * WalletConnect is left to each call site: its session is shared across tabs
100
+ * and only the leader may close it, which the callers already handle.
101
+ */
102
+ private dropTransports;
91
103
  initInternalTransports(): Promise<void>;
92
104
  private initEvents;
93
105
  private onRelayerError;
@@ -302,5 +314,18 @@ export declare class DAppClient extends Client {
302
314
  private addOpenRequest;
303
315
  private sendNotificationWithAccessToken;
304
316
  private buildAccountInfosFromV4Fanout;
317
+ /**
318
+ * Is this message from the wallet behind the active account? Over P2P the
319
+ * senderId is already bound to the decrypting key (see handleResponse).
320
+ */
321
+ private isActiveWallet;
322
+ /** Is this message from a wallet we are paired with on this connection? */
323
+ private isPairedWallet;
324
+ /**
325
+ * A wallet-initiated disconnect. Strangers are ignored; a saved but inactive
326
+ * wallet only removes its own records; only the active wallet (or any paired
327
+ * wallet when no account is active) ends the session.
328
+ */
329
+ private onUnsolicitedDisconnect;
305
330
  private onNewAccount;
306
331
  }
@@ -182,11 +182,25 @@ export class DAppClient extends Client {
182
182
  // normalized back to those same flat shapes — the wire dialect stays
183
183
  // invisible to integrators. Non-Tezos wrapped payloads keep the
184
184
  // pass-through of the generic permissionRequest/request API.
185
- const isWrapped = usesWrappedMessages(wireMessage.version);
185
+ // The wrapped dialect is identified by its payload, not by the absence of
186
+ // a flat marker: `wrapBeaconMessage` only ever emits
187
+ // { id, version, senderId, message }. Routing on the version alone dropped
188
+ // every flat message stamped with the wrapped dialect's number -- which is
189
+ // how a wallet built outside this SDK (or on <= 4.8.6, echoing the peer's
190
+ // version) sends its `disconnect` -- so handleDisconnect never ran and the
191
+ // dApp kept the account connected until the user disconnected a second
192
+ // time (#52). Testing for `.message` positively routes that message down
193
+ // the flat path, and still reads a non-conformant wallet's wrapped
194
+ // envelope as wrapped even when it carries a redundant top-level `type`.
195
+ const hasWrappedPayload = Boolean(wireMessage.message);
196
+ const isWrapped = usesWrappedMessages(wireMessage.version) && hasWrappedPayload;
186
197
  // Issue #33: a V3-versioned message can arrive without its wrapped payload.
187
- // Drop it safely instead of dereferencing an undefined payload, which would
188
- // throw an unhandled rejection inside the transport subscription callback.
189
- if (isWrapped && !wireMessage.message) {
198
+ // With no top-level `type` either it carries nothing to route on, so drop
199
+ // it instead of dereferencing an undefined payload, which would throw an
200
+ // unhandled rejection inside the transport subscription callback.
201
+ if (usesWrappedMessages(wireMessage.version) &&
202
+ !hasWrappedPayload &&
203
+ !('type' in wireMessage)) {
190
204
  logger.log('handleResponse', 'Received wrapped message with undefined payload; dropping', wireMessage);
191
205
  return;
192
206
  }
@@ -195,6 +209,13 @@ export class DAppClient extends Client {
195
209
  : wireMessage;
196
210
  const message = normalized ?? wireMessage;
197
211
  const typedMessage = normalized ?? wireMessage.message;
212
+ if ((connectionInfo.origin === Origin.P2P &&
213
+ message.senderId !== (await getSenderId(connectionInfo.id))) ||
214
+ (connectionInfo.origin === Origin.EXTENSION &&
215
+ !(await this.isPairedWallet(message.senderId, connectionInfo)))) {
216
+ logger.warn('handleResponse', 'dropping message: senderId does not match its sender');
217
+ return;
218
+ }
198
219
  let appMetadata = normalized
199
220
  ? typedMessage.appMetadata
200
221
  : typedMessage.blockchainData?.appMetadata;
@@ -242,7 +263,8 @@ export class DAppClient extends Client {
242
263
  logger.log('handleResponse', 'Received message', message, connectionInfo);
243
264
  logger.log('### message ###', JSON.stringify(message));
244
265
  logger.log('### connectionInfo ###', connectionInfo);
245
- const handleDisconnect = async () => {
266
+ // endSession=false: a saved but inactive wallet removes only its own records.
267
+ const handleDisconnect = async (endSession = true) => {
246
268
  this.analytics.track('event', 'DAppClient', 'Disconnect received from Wallet');
247
269
  const relevantTransport = connectionInfo.origin === Origin.P2P
248
270
  ? this.p2pTransport
@@ -257,11 +279,12 @@ export class DAppClient extends Client {
257
279
  }
258
280
  }
259
281
  await this.removeAccountsForPeerIds([message.senderId]);
282
+ if (!endSession) {
283
+ return;
284
+ }
260
285
  await this.events.emit(BeaconEvent.CHANNEL_CLOSED);
261
286
  // Reset transport state so next requestPermissions() shows pairing modal
262
- this.postMessageTransport = undefined;
263
- this.p2pTransport = undefined;
264
- this.walletConnectTransport = undefined;
287
+ await this.dropTransports();
265
288
  await this.setTransport(undefined);
266
289
  await this.setActivePeer(undefined);
267
290
  };
@@ -308,10 +331,15 @@ export class DAppClient extends Client {
308
331
  }
309
332
  else {
310
333
  if (typedMessage.type === BeaconMessageType.Disconnect) {
311
- await handleDisconnect();
334
+ await this.onUnsolicitedDisconnect(message.senderId, connectionInfo, handleDisconnect);
312
335
  }
313
336
  else if (typedMessage.type === BeaconMessageType.ChangeAccountRequest) {
314
- await this.onNewAccount(typedMessage, connectionInfo);
337
+ if (await this.isActiveWallet(message.senderId, connectionInfo)) {
338
+ await this.onNewAccount(typedMessage, connectionInfo);
339
+ }
340
+ else {
341
+ logger.warn('handleResponse', 'ignoring change account from a non-active wallet');
342
+ }
315
343
  }
316
344
  }
317
345
  if (this._transport.isResolved()) {
@@ -444,6 +472,28 @@ export class DAppClient extends Client {
444
472
  this.userId = await generateGUID();
445
473
  this.storage.set(StorageKey.USER_ID, this.userId);
446
474
  }
475
+ /**
476
+ * Disconnect the postMessage and P2P transports and drop all three instances.
477
+ *
478
+ * A dropped transport is not garbage. Its communication client keeps its
479
+ * `window` handlers (postMessage) or its matrix sync (P2P) until it is told to
480
+ * stop, so every path that used to just forget the instances left one more
481
+ * live client behind per connection. Every such path goes through here.
482
+ *
483
+ * WalletConnect is left to each call site: its session is shared across tabs
484
+ * and only the leader may close it, which the callers already handle.
485
+ */
486
+ async dropTransports() {
487
+ const dropped = [this.postMessageTransport, this.p2pTransport].filter((transport) => transport !== undefined);
488
+ this.postMessageTransport = undefined;
489
+ this.p2pTransport = undefined;
490
+ this.walletConnectTransport = undefined;
491
+ // Best effort: a transport that fails to disconnect is still dropped, and
492
+ // the others are still disconnected.
493
+ await Promise.all(dropped.map((transport) => transport.disconnect().catch((error) => {
494
+ logger.warn('dropTransports', error instanceof Error ? error.message : String(error));
495
+ })));
496
+ }
447
497
  async initInternalTransports() {
448
498
  const seed = await this.storage.get(StorageKey.BEACON_SDK_SECRET_SEED);
449
499
  if (!seed) {
@@ -542,6 +592,7 @@ export class DAppClient extends Client {
542
592
  */
543
593
  async destroy() {
544
594
  await this.createStateSnapshot();
595
+ await this.dropTransports();
545
596
  await super.destroy();
546
597
  }
547
598
  async init(transport, substratePairing) {
@@ -669,13 +720,19 @@ export class DAppClient extends Client {
669
720
  });
670
721
  const abortHandler = async () => {
671
722
  logger.log('init', 'ABORTED');
723
+ // Popup closed: replies to the QR code / request it showed no longer count.
724
+ stopListening();
672
725
  this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'abort'));
673
726
  await Promise.all([
674
- postMessageTransport.disconnect(),
675
- // p2pTransport.disconnect(), do not abort connection manually
676
- walletConnectTransport?.disconnect()
727
+ walletConnectTransport?.disconnect(),
728
+ // Disconnects postMessage and P2P -- `postMessageTransport` above
729
+ // is this.postMessageTransport, so dropTransports() already owns
730
+ // it -- and forgets all three. It also stops the P2P client
731
+ // started for the pairing QR: if its start is still pending,
732
+ // P2PCommunicationClient.start() notices the stop once the login
733
+ // completes and shuts it down.
734
+ this.dropTransports()
677
735
  ]);
678
- this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
679
736
  this._activeAccount.isResolved() && this.clearActiveAccount();
680
737
  this.events.emit(BeaconEvent.PAIR_ABORTED).catch((emitError) => console.warn(emitError));
681
738
  if (this._initPromiseReject) {
@@ -751,11 +808,8 @@ export class DAppClient extends Client {
751
808
  this.storage.set(StorageKey.ACTIVE_ACCOUNT, undefined);
752
809
  emit && this.events.emit(BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
753
810
  !emit && this.hideUI(['alert']);
754
- await Promise.all([
755
- this.postMessageTransport?.disconnect(),
756
- this.walletConnectTransport?.disconnect()
757
- ]);
758
- this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
811
+ const walletConnectTransport = this.walletConnectTransport;
812
+ await Promise.all([walletConnectTransport?.disconnect(), this.dropTransports()]);
759
813
  await this.setActivePeer(undefined);
760
814
  await this.setTransport(undefined);
761
815
  this._initPromise = undefined;
@@ -787,7 +841,7 @@ export class DAppClient extends Client {
787
841
  if (!this.debounceSetActiveAccount && transport instanceof WalletConnectTransport) {
788
842
  this.debounceSetActiveAccount = true;
789
843
  this._initPromise = undefined;
790
- this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
844
+ await this.dropTransports();
791
845
  if (this.multiTabChannel.isLeader() || isMobileOS(window)) {
792
846
  await transport.disconnect();
793
847
  this.openRequestsOtherTabs.clear();
@@ -1750,9 +1804,7 @@ export class DAppClient extends Client {
1750
1804
  if (request.type === BeaconMessageType.PermissionRequest &&
1751
1805
  (await this.getActiveAccount()) === undefined) {
1752
1806
  this._initPromise = undefined;
1753
- this.postMessageTransport = undefined;
1754
- this.p2pTransport = undefined;
1755
- this.walletConnectTransport = undefined;
1807
+ await this.dropTransports();
1756
1808
  await this.setTransport();
1757
1809
  await this.setActivePeer();
1758
1810
  }
@@ -2359,22 +2411,11 @@ export class DAppClient extends Client {
2359
2411
  }
2360
2412
  await this.clearActiveAccount();
2361
2413
  if (!(transport instanceof WalletConnectTransport)) {
2362
- try {
2363
- await transport.disconnect();
2364
- }
2365
- catch (disconnectError) {
2366
- const message = disconnectError instanceof Error ? disconnectError.message : String(disconnectError);
2367
- if (typeof message === 'string' && message.includes('Syncing stopped manually')) {
2368
- logger.log('disconnect', 'Matrix sync stopped manually');
2369
- }
2370
- else {
2371
- throw disconnectError;
2372
- }
2373
- }
2414
+ await transport.disconnect();
2374
2415
  }
2375
- this.postMessageTransport = undefined;
2376
- this.p2pTransport = undefined;
2377
- this.walletConnectTransport = undefined;
2416
+ // The active transport is already disconnected above; this stops the
2417
+ // other one (a second disconnect is a no-op) and drops the instances.
2418
+ await this.dropTransports();
2378
2419
  await this.setTransport();
2379
2420
  this._initPromise = undefined;
2380
2421
  this._initPromiseReject = undefined;
@@ -2500,6 +2541,54 @@ export class DAppClient extends Client {
2500
2541
  };
2501
2542
  }));
2502
2543
  }
2544
+ /**
2545
+ * Is this message from the wallet behind the active account? Over P2P the
2546
+ * senderId is already bound to the decrypting key (see handleResponse).
2547
+ */
2548
+ async isActiveWallet(senderId, connectionInfo) {
2549
+ if (connectionInfo.origin === Origin.WALLETCONNECT) {
2550
+ return true;
2551
+ }
2552
+ await this.activeAccountLoaded;
2553
+ const active = await this.getActiveAccount();
2554
+ return Boolean(active &&
2555
+ active.senderId === senderId &&
2556
+ active.origin.type === connectionInfo.origin &&
2557
+ active.origin.id === connectionInfo.id);
2558
+ }
2559
+ /** Is this message from a wallet we are paired with on this connection? */
2560
+ async isPairedWallet(senderId, connectionInfo) {
2561
+ if (connectionInfo.origin === Origin.P2P) {
2562
+ const peers = (await this.storage.get(StorageKey.TRANSPORT_P2P_PEERS_DAPP)) ?? [];
2563
+ return peers.some((peer) => peer.publicKey === connectionInfo.id);
2564
+ }
2565
+ if (connectionInfo.origin === Origin.EXTENSION) {
2566
+ const peers = (await this.storage.get(StorageKey.TRANSPORT_POSTMESSAGE_PEERS_DAPP)) ?? [];
2567
+ for (const peer of peers) {
2568
+ if (peer.extensionId === connectionInfo.id &&
2569
+ (await getSenderId(peer.publicKey)) === senderId) {
2570
+ return true;
2571
+ }
2572
+ }
2573
+ return false;
2574
+ }
2575
+ return true; // WalletConnect: unchanged, see isActiveWallet
2576
+ }
2577
+ /**
2578
+ * A wallet-initiated disconnect. Strangers are ignored; a saved but inactive
2579
+ * wallet only removes its own records; only the active wallet (or any paired
2580
+ * wallet when no account is active) ends the session.
2581
+ */
2582
+ async onUnsolicitedDisconnect(senderId, connectionInfo, handleDisconnect) {
2583
+ if (!(await this.isPairedWallet(senderId, connectionInfo))) {
2584
+ logger.warn('handleResponse', 'ignoring disconnect from an unknown wallet');
2585
+ return;
2586
+ }
2587
+ await this.activeAccountLoaded;
2588
+ const active = await this.getActiveAccount();
2589
+ const endSession = !active || (await this.isActiveWallet(senderId, connectionInfo));
2590
+ await handleDisconnect(endSession);
2591
+ }
2503
2592
  async onNewAccount(message, connectionInfo) {
2504
2593
  // TODO: Migration code. Remove sometime after 1.0.0 release.
2505
2594
  const tempPK = message.publicKey || message.pubkey || message.pubKey;
@@ -2508,6 +2597,15 @@ export class DAppClient extends Client {
2508
2597
  throw new Error('PublicKey or Address must be defined');
2509
2598
  }
2510
2599
  const address = message.address ?? (await getAddressFromPublicKey(publicKey));
2600
+ if (connectionInfo.origin !== Origin.WALLETCONNECT &&
2601
+ publicKey &&
2602
+ message.address &&
2603
+ message.walletType !== 'abstracted_account') {
2604
+ const derived = await getAddressFromPublicKey(publicKey).catch(() => undefined);
2605
+ if (derived && derived !== message.address) {
2606
+ throw new Error(`Address "${message.address}" does not match its public key`);
2607
+ }
2608
+ }
2511
2609
  if (!isValidAddress(address)) {
2512
2610
  throw new Error(`Invalid address: "${address}"`);
2513
2611
  }