@tezos-x/octez.connect-dapp 4.8.4 → 4.8.6

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.
@@ -24,6 +24,23 @@ const octez_connect_transport_postmessage_1 = require("@tezos-x/octez.connect-tr
24
24
  const octez_connect_ui_1 = require("@tezos-x/octez.connect-ui");
25
25
  const octez_connect_transport_walletconnect_1 = require("@tezos-x/octez.connect-transport-walletconnect");
26
26
  const logger = new octez_connect_core_1.Logger('DAppClient');
27
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
28
+ const SESSION_UPDATE_REQUEST_ID = 'session_update';
29
+ const createLazyPromise = (factory) => {
30
+ let promise;
31
+ const getPromise = () => {
32
+ if (!promise) {
33
+ promise = factory();
34
+ }
35
+ return promise;
36
+ };
37
+ const lazyPromise = {
38
+ then: (onfulfilled, onrejected) => getPromise().then(onfulfilled, onrejected),
39
+ catch: (onrejected) => getPromise().catch(onrejected),
40
+ finally: (onfinally) => getPromise().finally(onfinally !== null && onfinally !== void 0 ? onfinally : undefined)
41
+ };
42
+ return lazyPromise;
43
+ };
27
44
  /**
28
45
  * @publicapi
29
46
  *
@@ -34,7 +51,7 @@ const logger = new octez_connect_core_1.Logger('DAppClient');
34
51
  */
35
52
  class DAppClient extends octez_connect_core_1.Client {
36
53
  constructor(config) {
37
- var _a, _b, _c, _d, _e, _f, _g, _h;
54
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
38
55
  super(Object.assign({ storage: config && config.storage ? config.storage : new octez_connect_core_1.LocalStorage() }, config));
39
56
  this.events = new events_1.BeaconEventHandler();
40
57
  this.isGetActiveAccountHandled = false;
@@ -43,6 +60,8 @@ class DAppClient extends octez_connect_core_1.Client {
43
60
  * A map of requests that are currently "open", meaning we have sent them to a wallet and are still awaiting a response.
44
61
  */
45
62
  this.openRequests = new Map();
63
+ this.acknowledgedRequests = new Set();
64
+ this.openRequestTimeouts = new Map();
46
65
  /**
47
66
  * The currently active account. For all requests that are associated to a specific request (operation request, signing request),
48
67
  * the active account is used to determine the network and destination wallet
@@ -52,44 +71,59 @@ class DAppClient extends octez_connect_core_1.Client {
52
71
  * The currently active peer. This is used to address a peer in case the active account is not set. (Eg. for permission requests)
53
72
  */
54
73
  this._activePeer = new octez_connect_utils_1.ExposedPromise();
55
- this.isInitPending = false;
74
+ this.destroyed = false;
56
75
  this.beaconIDB = new octez_connect_core_1.IndexedDBStorage('beacon', ['bug_report', 'metrics']);
57
76
  this.debounceSetActiveAccount = false;
77
+ // Constructor-time invalid storage recovery can race through multiple async paths.
78
+ // Keep this guard scoped to those startup paths.
79
+ this.hasEmittedInvalidAccountDeactivated = false;
80
+ this._disconnectNotifyPeers = false;
58
81
  this.multiTabChannel = new octez_connect_core_1.MultiTabChannel('octez.connect-sdk-channel', this.onBCMessageHandler.bind(this), this.onElectedLeaderhandler.bind(this));
59
82
  this.blockchains = new Map();
60
83
  this.description = config.description;
61
- this.wcProjectId = ((_a = config.walletConnectOptions) === null || _a === void 0 ? void 0 : _a.projectId) || '24469fd0a06df227b6e5f7dc7de0ff4f';
62
- this.wcRelayUrl = (_b = config.walletConnectOptions) === null || _b === void 0 ? void 0 : _b.relayUrl;
84
+ this.disableWalletConnect = (_a = config.disableWalletConnect) !== null && _a !== void 0 ? _a : false;
85
+ if (!this.disableWalletConnect) {
86
+ this.wcProjectId = ((_b = config.walletConnectOptions) === null || _b === void 0 ? void 0 : _b.projectId) || '24469fd0a06df227b6e5f7dc7de0ff4f';
87
+ this.wcRelayUrl = (_c = config.walletConnectOptions) === null || _c === void 0 ? void 0 : _c.relayUrl;
88
+ }
63
89
  this.featuredWallets = config.featuredWallets;
64
- this.events = new events_1.BeaconEventHandler(config.eventHandlers, (_c = config.disableDefaultEvents) !== null && _c !== void 0 ? _c : false);
65
- this.blockExplorer = (_d = config.blockExplorer) !== null && _d !== void 0 ? _d : new tzkt_blockexplorer_1.TzktBlockExplorer();
66
- this.network = (_e = config.network) !== null && _e !== void 0 ? _e : { type: (_f = config.preferredNetwork) !== null && _f !== void 0 ? _f : octez_connect_types_1.NetworkType.MAINNET };
67
- (0, octez_connect_ui_1.setColorMode)((_g = config.colorMode) !== null && _g !== void 0 ? _g : octez_connect_types_1.ColorMode.LIGHT);
90
+ this.events = new events_1.BeaconEventHandler(config.eventHandlers, (_d = config.disableDefaultEvents) !== null && _d !== void 0 ? _d : false);
91
+ this.blockExplorer = (_e = config.blockExplorer) !== null && _e !== void 0 ? _e : new tzkt_blockexplorer_1.TzktBlockExplorer();
92
+ this.network = (_f = config.network) !== null && _f !== void 0 ? _f : { type: (_g = config.preferredNetwork) !== null && _g !== void 0 ? _g : octez_connect_types_1.NetworkType.MAINNET };
93
+ (0, octez_connect_ui_1.setColorMode)((_h = config.colorMode) !== null && _h !== void 0 ? _h : octez_connect_types_1.ColorMode.LIGHT);
68
94
  this.disclaimerText = config.disclaimerText;
69
- this.errorMessages = (_h = config.errorMessages) !== null && _h !== void 0 ? _h : {};
95
+ this.errorMessages = (_j = config.errorMessages) !== null && _j !== void 0 ? _j : {};
70
96
  this.appMetadataManager = new octez_connect_core_1.AppMetadataManager(this.storage);
71
97
  this.storageValidator = new octez_connect_core_1.StorageValidator(this.storage);
72
98
  this.enableAppSwitching =
73
99
  config.enableAppSwitching === undefined ? true : !!config.enableAppSwitching;
74
100
  this.enableMetrics = config.enableMetrics ? true : false;
101
+ this.requestTimeoutMs = (_k = config.requestTimeoutMs) !== null && _k !== void 0 ? _k : DEFAULT_REQUEST_TIMEOUT_MS;
75
102
  // Subscribe to storage changes and update the active account if it changes on other tabs
76
103
  this.storage.subscribeToStorageChanged((event) => __awaiter(this, void 0, void 0, function* () {
104
+ if (event.oldValue === event.newValue) {
105
+ return;
106
+ }
77
107
  if (event.eventType === 'storageCleared') {
78
- this.setActiveAccount(undefined);
108
+ yield this.setActiveAccount(undefined);
79
109
  return;
80
110
  }
81
111
  if (event.eventType === 'entryModified') {
82
112
  if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT)) {
83
113
  const accountIdentifier = event.newValue;
84
114
  if (!accountIdentifier || accountIdentifier === 'undefined') {
85
- this.setActiveAccount(undefined);
115
+ yield this.setActiveAccount(undefined);
86
116
  }
87
117
  else {
88
118
  const account = yield this.getAccount(accountIdentifier);
89
- this.setActiveAccount(account);
119
+ yield this.setActiveAccount(account);
90
120
  }
91
121
  return;
92
122
  }
123
+ if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ACCOUNTS)) {
124
+ yield this.recoverActiveAccountFromAccountsChange();
125
+ return;
126
+ }
93
127
  if (event.key === this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ENABLE_METRICS)) {
94
128
  this.enableMetrics = !!(yield this.storage.get(octez_connect_types_1.StorageKey.ENABLE_METRICS));
95
129
  return;
@@ -105,31 +139,43 @@ class DAppClient extends octez_connect_core_1.Client {
105
139
  this.activeAccountLoaded = this.storage
106
140
  .get(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT)
107
141
  .then((activeAccountIdentifier) => __awaiter(this, void 0, void 0, function* () {
142
+ const accounts = yield this.accountManager.getAccounts();
143
+ if (activeAccountIdentifier === 'undefined') {
144
+ // Non-normalizing storage backends can still surface legacy sentinel strings directly.
145
+ yield this.deactivateInvalidAccountState('missing_active_account');
146
+ return undefined;
147
+ }
108
148
  if (activeAccountIdentifier) {
109
- const account = yield this.accountManager.getAccount(activeAccountIdentifier);
149
+ const account = accounts.find((storedAccount) => storedAccount.accountIdentifier === activeAccountIdentifier);
150
+ if (!account) {
151
+ yield this.deactivateInvalidAccountState('missing_active_account');
152
+ return undefined;
153
+ }
110
154
  yield this.setActiveAccount(account);
111
155
  return account;
112
156
  }
113
- else {
114
- yield this.setActiveAccount(undefined);
157
+ if (accounts.length > 0 && this.hasStoredActiveAccountPointer()) {
158
+ yield this.deactivateInvalidAccountState('missing_active_account');
115
159
  return undefined;
116
160
  }
161
+ yield this.setActiveAccount(undefined);
162
+ return undefined;
117
163
  }))
118
164
  .catch((storageError) => __awaiter(this, void 0, void 0, function* () {
119
165
  logger.error(storageError);
120
- yield this.resetInvalidState(false);
121
- this.events.emit(events_1.BeaconEvent.INVALID_ACCOUNT_DEACTIVATED);
166
+ yield this.deactivateInvalidAccountState('invalid_active_account_storage');
122
167
  return undefined;
123
168
  }));
124
169
  this.handleResponse = (message, connectionInfo) => __awaiter(this, void 0, void 0, function* () {
125
170
  var _a;
126
- const typedMessage = message.version === '3'
171
+ const isV3WrappedMessage = message.version === '3' && 'message' in message;
172
+ const typedMessage = isV3WrappedMessage
127
173
  ? message.message
128
174
  : message;
129
- let appMetadata = message.version === '3'
175
+ let appMetadata = isV3WrappedMessage
130
176
  ? (_a = typedMessage.blockchainData) === null || _a === void 0 ? void 0 : _a.appMetadata
131
177
  : typedMessage.appMetadata;
132
- if (!appMetadata && message.version === '3') {
178
+ if (!appMetadata && isV3WrappedMessage) {
133
179
  const storedMetadata = yield Promise.all([
134
180
  this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_P2P_PEERS_DAPP),
135
181
  this.storage.get(octez_connect_types_1.StorageKey.TRANSPORT_WALLETCONNECT_PEERS_DAPP),
@@ -158,8 +204,11 @@ class DAppClient extends octez_connect_core_1.Client {
158
204
  },
159
205
  id: message.id
160
206
  });
161
- if (typedMessage.type !== octez_connect_types_1.BeaconMessageType.Acknowledge) {
162
- this.openRequestsOtherTabs.delete(message.id);
207
+ if (typedMessage.type === octez_connect_types_1.BeaconMessageType.Acknowledge) {
208
+ this.clearOpenRequestTimeout(message.id);
209
+ }
210
+ else {
211
+ this.clearOpenRequest(message.id);
163
212
  }
164
213
  return;
165
214
  }
@@ -187,15 +236,19 @@ class DAppClient extends octez_connect_core_1.Client {
187
236
  yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
188
237
  });
189
238
  if (openRequest && typedMessage.type === octez_connect_types_1.BeaconMessageType.Acknowledge) {
190
- this.analytics.track('event', 'DAppClient', 'Acknowledge received from Wallet');
191
- logger.log('handleResponse', `acknowledge message received for ${message.id}`);
192
- this.events
193
- .emit(events_1.BeaconEvent.ACKNOWLEDGE_RECEIVED, {
194
- message: typedMessage,
195
- extraInfo: {},
196
- walletInfo: yield this.getWalletInfo()
197
- })
198
- .catch(console.error);
239
+ if (!this.acknowledgedRequests.has(message.id)) {
240
+ this.acknowledgedRequests.add(message.id);
241
+ this.clearOpenRequestTimeout(message.id);
242
+ this.analytics.track('event', 'DAppClient', 'Acknowledge received from Wallet');
243
+ logger.log('handleResponse', `acknowledge message received for ${message.id}`);
244
+ this.events
245
+ .emit(events_1.BeaconEvent.ACKNOWLEDGE_RECEIVED, {
246
+ message: typedMessage,
247
+ extraInfo: {},
248
+ walletInfo: yield this.getWalletInfo()
249
+ })
250
+ .catch((emitError) => logger.error('handleResponse', emitError));
251
+ }
199
252
  }
200
253
  else if (openRequest) {
201
254
  if (typedMessage.type === octez_connect_types_1.BeaconMessageType.PermissionResponse && appMetadata) {
@@ -207,7 +260,8 @@ class DAppClient extends octez_connect_core_1.Client {
207
260
  else {
208
261
  openRequest.resolve({ message, connectionInfo });
209
262
  }
210
- this.openRequests.delete(typedMessage.id);
263
+ this.clearOpenRequest(message.id);
264
+ this.acknowledgedRequests.delete(message.id);
211
265
  }
212
266
  else {
213
267
  if (typedMessage.type === octez_connect_types_1.BeaconMessageType.Disconnect) {
@@ -216,16 +270,23 @@ class DAppClient extends octez_connect_core_1.Client {
216
270
  else if (typedMessage.type === octez_connect_types_1.BeaconMessageType.ChangeAccountRequest) {
217
271
  yield this.onNewAccount(typedMessage, connectionInfo);
218
272
  }
273
+ else {
274
+ logger.warn('handleResponse', 'received response for unknown or closed request', {
275
+ id: message.id,
276
+ type: typedMessage.type,
277
+ connectionInfo
278
+ });
279
+ }
219
280
  }
220
281
  if (this._transport.isResolved()) {
221
282
  const transport = yield this.transport;
222
283
  if (transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport &&
223
- !this.openRequests.has('session_update')) {
224
- this.openRequests.set('session_update', new octez_connect_utils_1.ExposedPromise());
284
+ !this.openRequests.has(SESSION_UPDATE_REQUEST_ID)) {
285
+ this.openRequests.set(SESSION_UPDATE_REQUEST_ID, new octez_connect_utils_1.ExposedPromise());
225
286
  }
226
287
  }
227
288
  });
228
- this.storageValidator
289
+ this.storageValidated = this.storageValidator
229
290
  .validate()
230
291
  .then((isValid) => __awaiter(this, void 0, void 0, function* () {
231
292
  var _a, _b;
@@ -242,15 +303,22 @@ class DAppClient extends octez_connect_core_1.Client {
242
303
  url: info.deeplink
243
304
  });
244
305
  const nowValid = yield this.storageValidator.validate();
245
- if (!nowValid) {
246
- this.resetInvalidState(false);
306
+ if (!nowValid && account) {
307
+ yield this.deactivateInvalidAccountState('storage_validation_failed');
308
+ }
309
+ else if (!nowValid) {
310
+ yield this.resetInvalidState(false);
247
311
  }
248
312
  }
249
313
  if (account && account.origin.type !== 'p2p') {
250
- this.init();
314
+ // Fire-and-forget eager init for stored sessions. init() can now
315
+ // reject (e.g. when an abort path settles _initPromise), so swallow
316
+ // here; downstream callers of init() handle rejection on their own.
317
+ this.init().catch((initError) => { var _a; return logger.error('eager init failed', (_a = initError === null || initError === void 0 ? void 0 : initError.message) !== null && _a !== void 0 ? _a : initError); });
251
318
  }
252
319
  }))
253
320
  .catch((err) => logger.error(err.message));
321
+ this.retainStorageValidationPromise();
254
322
  this.sendMetrics('enable-metrics?' + this.addQueryParam('version', octez_connect_core_1.SDK_VERSION), undefined, (res) => {
255
323
  if (!res.ok) {
256
324
  res.status === 426
@@ -328,11 +396,42 @@ class DAppClient extends octez_connect_core_1.Client {
328
396
  const transport = (yield this._transport.promise);
329
397
  yield transport.waitForResolution();
330
398
  this.openRequestsOtherTabs.add(message.id);
331
- isV3
399
+ const requestPromise = isV3
332
400
  ? this.makeRequestV3(message.data, message.id)
333
401
  : this.makeRequest(message.data, false, message.id);
402
+ requestPromise.catch((requestError) => {
403
+ const errorResponse = this.buildDelegatedRequestError(message.id, requestError);
404
+ this.multiTabChannel.postMessage({
405
+ type: 'RESPONSE',
406
+ data: {
407
+ message: errorResponse,
408
+ connectionInfo: {
409
+ origin: octez_connect_types_1.Origin.WALLETCONNECT,
410
+ id: ''
411
+ }
412
+ },
413
+ id: message.id
414
+ });
415
+ this.clearOpenRequest(message.id);
416
+ });
334
417
  });
335
418
  }
419
+ buildDelegatedRequestError(id, error) {
420
+ if (typeof error === 'object' &&
421
+ error !== null &&
422
+ 'type' in error &&
423
+ 'errorType' in error &&
424
+ error.type === octez_connect_types_1.BeaconMessageType.Error) {
425
+ return Object.assign(Object.assign({}, error), { id });
426
+ }
427
+ return {
428
+ id,
429
+ type: octez_connect_types_1.BeaconMessageType.Error,
430
+ errorType: octez_connect_types_1.BeaconErrorType.UNKNOWN_ERROR,
431
+ senderId: '',
432
+ version: '2'
433
+ };
434
+ }
336
435
  createStateSnapshot() {
337
436
  return __awaiter(this, void 0, void 0, function* () {
338
437
  if (!localStorage || !this.enableMetrics) {
@@ -363,10 +462,7 @@ class DAppClient extends octez_connect_core_1.Client {
363
462
  initInternalTransports() {
364
463
  return __awaiter(this, void 0, void 0, function* () {
365
464
  var _a, _b;
366
- const seed = yield this.storage.get(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED);
367
- if (!seed) {
368
- throw new Error('Secret seed not found');
369
- }
465
+ const seed = yield this.getOrCreateSDKSecretSeed();
370
466
  const keyPair = yield (0, octez_connect_utils_1.getKeypairFromSeed)(seed);
371
467
  if (this.postMessageTransport || this.p2pTransport || this.walletConnectTransport) {
372
468
  return;
@@ -375,6 +471,11 @@ class DAppClient extends octez_connect_core_1.Client {
375
471
  yield this.addListener(this.postMessageTransport);
376
472
  this.p2pTransport = new DappP2PTransport_1.DappP2PTransport(this.name, keyPair, this.storage, this.matrixNodes, this.iconUrl, this.appUrl);
377
473
  yield this.addListener(this.p2pTransport);
474
+ // Skip WalletConnect entirely when disabled (e.g. inside a Firefox MV3 content-script
475
+ // compartment where the WC provider cannot run). postMessage + P2P are unaffected.
476
+ if (this.disableWalletConnect) {
477
+ return;
478
+ }
378
479
  const wcOptions = {
379
480
  projectId: this.wcProjectId,
380
481
  relayUrl: this.wcRelayUrl,
@@ -393,6 +494,41 @@ class DAppClient extends octez_connect_core_1.Client {
393
494
  yield this.addListener(this.walletConnectTransport);
394
495
  });
395
496
  }
497
+ getOrCreateSDKSecretSeed() {
498
+ return __awaiter(this, void 0, void 0, function* () {
499
+ if (this._sdkSecretSeedPromise) {
500
+ return this._sdkSecretSeedPromise;
501
+ }
502
+ this._sdkSecretSeedPromise = this.loadOrCreateSDKSecretSeed()
503
+ .finally(() => {
504
+ this._sdkSecretSeedPromise = undefined;
505
+ })
506
+ .catch((error) => {
507
+ throw error;
508
+ });
509
+ return this._sdkSecretSeedPromise;
510
+ });
511
+ }
512
+ loadOrCreateSDKSecretSeed() {
513
+ return __awaiter(this, void 0, void 0, function* () {
514
+ const existingSeed = yield this.storage.get(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED);
515
+ if (this.isValidSDKSecretSeed(existingSeed)) {
516
+ return existingSeed;
517
+ }
518
+ yield this.storage.delete(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED);
519
+ this._keyPair = new octez_connect_utils_1.ExposedPromise();
520
+ this._beaconId = new octez_connect_utils_1.ExposedPromise();
521
+ yield this.initSDK();
522
+ const restoredSeed = yield this.storage.get(octez_connect_types_1.StorageKey.BEACON_SDK_SECRET_SEED);
523
+ if (this.isValidSDKSecretSeed(restoredSeed)) {
524
+ return restoredSeed;
525
+ }
526
+ throw new Error('Secret seed not found');
527
+ });
528
+ }
529
+ isValidSDKSecretSeed(seed) {
530
+ return typeof seed === 'string' && seed !== '' && seed !== 'undefined' && seed !== 'null';
531
+ }
396
532
  initEvents() {
397
533
  if (!this.walletConnectTransport) {
398
534
  return;
@@ -410,6 +546,7 @@ class DAppClient extends octez_connect_core_1.Client {
410
546
  }
411
547
  wcToastHandler(status) {
412
548
  return __awaiter(this, void 0, void 0, function* () {
549
+ var _a;
413
550
  const walletInfo = yield (() => __awaiter(this, void 0, void 0, function* () {
414
551
  try {
415
552
  return yield this.getWalletInfo();
@@ -427,22 +564,28 @@ class DAppClient extends octez_connect_core_1.Client {
427
564
  });
428
565
  }
429
566
  else {
430
- this.events.emit(events_1.BeaconEvent.PERMISSION_REQUEST_ERROR, {
431
- errorResponse: { errorType: octez_connect_types_1.BeaconErrorType.ABORTED_ERROR },
432
- walletInfo
433
- });
567
+ // The WC transport reports session-proposal rejection through this event
568
+ // when there are no active listeners yet (no peer paired). Reject the
569
+ // in-flight init promise so `await dapp.requestPermissions()` unwinds
570
+ // through requestPermissions' catch -> handleRequestError, which both
571
+ // emits PERMISSION_REQUEST_ERROR and resets transport state. Without
572
+ // this, the dapp wedges with a pending `_initPromise`.
573
+ (_a = this._initReject) === null || _a === void 0 ? void 0 : _a.call(this, this.createAbortedError());
434
574
  }
435
575
  });
436
576
  }
437
577
  channelClosedHandler(type) {
438
578
  return __awaiter(this, void 0, void 0, function* () {
579
+ if (!this._transport.isResolved()) {
580
+ yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
581
+ return;
582
+ }
439
583
  const transport = yield this.transport;
440
584
  if (transport.type !== type) {
441
585
  return;
442
586
  }
443
587
  yield this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED);
444
- this.setActiveAccount(undefined);
445
- yield this.disconnect();
588
+ yield this.disconnect({ notifyPeers: false });
446
589
  });
447
590
  }
448
591
  /**
@@ -457,16 +600,81 @@ class DAppClient extends octez_connect_core_1.Client {
457
600
  destroy: { get: () => super.destroy }
458
601
  });
459
602
  return __awaiter(this, void 0, void 0, function* () {
603
+ this.destroyed = true;
460
604
  yield this.createStateSnapshot();
605
+ yield this.destroyInternalTransports();
461
606
  yield _super.destroy.call(this);
462
607
  });
463
608
  }
609
+ destroyInternalTransports() {
610
+ return __awaiter(this, void 0, void 0, function* () {
611
+ this.abortPendingInit();
612
+ const transports = [];
613
+ if (this.postMessageTransport) {
614
+ transports.push(this.postMessageTransport);
615
+ }
616
+ if (this.p2pTransport) {
617
+ transports.push(this.p2pTransport);
618
+ }
619
+ if (this.walletConnectTransport) {
620
+ transports.push(this.walletConnectTransport);
621
+ }
622
+ if (transports.length > 0) {
623
+ yield this.cleanup(transports);
624
+ yield Promise.all(transports.map((transport) => __awaiter(this, void 0, void 0, function* () {
625
+ yield transport.disconnect();
626
+ if ('doClientCleanup' in transport) {
627
+ yield transport.doClientCleanup();
628
+ }
629
+ })));
630
+ this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
631
+ yield this.setTransport(undefined);
632
+ }
633
+ yield this.multiTabChannel.close();
634
+ });
635
+ }
636
+ abortPendingInit() {
637
+ var _a;
638
+ (_a = this._initReject) === null || _a === void 0 ? void 0 : _a.call(this, this.createAbortedError());
639
+ this._initPromise = undefined;
640
+ this._initReject = undefined;
641
+ this._initSubstratePairing = undefined;
642
+ this._requestPermissionsPromise = undefined;
643
+ this._requestPermissionsKey = undefined;
644
+ }
645
+ createAbortedError(id = '') {
646
+ return {
647
+ type: octez_connect_types_1.BeaconMessageType.Error,
648
+ id,
649
+ senderId: '',
650
+ version: '2',
651
+ errorType: octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
652
+ };
653
+ }
654
+ /**
655
+ * @internal
656
+ */
657
+ isDestroyed() {
658
+ return this.destroyed;
659
+ }
660
+ assertNotDestroyed() {
661
+ if (this.destroyed) {
662
+ throw new Error('DAppClient has been destroyed and cannot be used again.');
663
+ }
664
+ }
464
665
  init(transport, substratePairing) {
465
666
  const _super = Object.create(null, {
466
667
  init: { get: () => super.init }
467
668
  });
468
669
  return __awaiter(this, void 0, void 0, function* () {
670
+ this.assertNotDestroyed();
671
+ const requestedSubstratePairing = substratePairing === true;
469
672
  if (this._initPromise) {
673
+ if (!transport &&
674
+ this._initReject &&
675
+ this._initSubstratePairing !== requestedSubstratePairing) {
676
+ throw new Error('Cannot start a permission request with different pairing options while another pairing is pending');
677
+ }
470
678
  return this._initPromise;
471
679
  }
472
680
  try {
@@ -475,163 +683,260 @@ class DAppClient extends octez_connect_core_1.Client {
475
683
  catch (_a) {
476
684
  //
477
685
  }
478
- this._initPromise = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
479
- if (transport) {
480
- yield this.addListener(transport);
481
- resolve(yield _super.init.call(this, transport));
482
- }
483
- else if (this._transport.isSettled()) {
484
- yield (yield this.transport).connect();
485
- resolve(yield _super.init.call(this, yield this.transport));
486
- }
487
- else {
488
- const activeAccount = yield this.getActiveAccount();
489
- const stopListening = () => {
490
- if (this.postMessageTransport) {
491
- this.postMessageTransport.stopListeningForNewPeers().catch(console.error);
492
- }
493
- if (this.p2pTransport) {
494
- this.p2pTransport.stopListeningForNewPeers().catch(console.error);
495
- }
496
- if (this.walletConnectTransport) {
497
- this.walletConnectTransport.stopListeningForNewPeers().catch(console.error);
498
- }
499
- };
500
- yield this.initInternalTransports();
501
- if (!this.postMessageTransport || !this.p2pTransport || !this.walletConnectTransport) {
502
- return;
686
+ this._initPromise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
687
+ this._initSubstratePairing = requestedSubstratePairing;
688
+ // Capture reject so abort paths (modal close, transport-level rejection)
689
+ // can unwedge a pending `await this.init()`. _initReject is cleared by
690
+ // the .finally below whether the promise resolves or rejects, so the
691
+ // field accurately means "currently rejectable" rather than "most recent
692
+ // rejector".
693
+ this._initReject = (reason) => {
694
+ this._initPromise = undefined;
695
+ reject(reason);
696
+ };
697
+ // The body uses `new Promise(async ...)` (an anti-pattern: unhandled
698
+ // throws are swallowed). Wrap it so any unexpected throw rejects the
699
+ // promise instead of stranding `await this.init()` callers forever.
700
+ // A proper structural fix would replace the async-executor entirely
701
+ // with chained promises; left as follow-up to keep this PR focused.
702
+ try {
703
+ if (transport) {
704
+ yield this.addListener(transport);
705
+ resolve(yield _super.init.call(this, transport));
503
706
  }
504
- this.postMessageTransport.connect().then().catch(console.error);
505
- if (activeAccount && activeAccount.origin) {
506
- const origin = activeAccount.origin.type;
507
- // Select the transport that matches the active account
508
- if (origin === octez_connect_types_1.Origin.EXTENSION) {
509
- resolve(yield _super.init.call(this, this.postMessageTransport));
510
- }
511
- else if (origin === octez_connect_types_1.Origin.P2P) {
512
- resolve(yield _super.init.call(this, this.p2pTransport));
513
- }
514
- else if (origin === octez_connect_types_1.Origin.WALLETCONNECT) {
515
- resolve(yield _super.init.call(this, this.walletConnectTransport));
516
- }
707
+ else if (this._transport.isSettled()) {
708
+ yield (yield this.transport).connect();
709
+ resolve(yield _super.init.call(this, yield this.transport));
517
710
  }
518
711
  else {
519
- const p2pTransport = this.p2pTransport;
520
- const postMessageTransport = this.postMessageTransport;
521
- const walletConnectTransport = this.walletConnectTransport;
522
- postMessageTransport
523
- .listenForNewPeer((peer) => {
524
- logger.log('init', 'postmessage transport peer connected', peer);
525
- this.analytics.track('event', 'DAppClient', 'Extension connected', {
526
- peerName: peer.name
527
- });
528
- this.events
529
- .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
530
- .catch((emitError) => console.warn(emitError));
531
- this.setActivePeer(peer).catch(console.error);
532
- this.setTransport(this.postMessageTransport).catch(console.error);
533
- stopListening();
534
- resolve(octez_connect_types_1.TransportType.POST_MESSAGE);
535
- })
536
- .catch(console.error);
537
- p2pTransport
538
- .listenForNewPeer((peer) => {
539
- logger.log('init', 'p2p transport peer connected', peer);
540
- this.analytics.track('event', 'DAppClient', 'octez.connect Wallet connected', {
541
- peerName: peer.name
712
+ const activeAccount = yield this.getActiveAccount();
713
+ const stopListening = () => {
714
+ const onError = (err) => logger.error('stopListeningForNewPeers', err);
715
+ if (this.postMessageTransport) {
716
+ this.postMessageTransport.stopListeningForNewPeers().catch(onError);
717
+ }
718
+ if (this.p2pTransport) {
719
+ this.p2pTransport.stopListeningForNewPeers().catch(onError);
720
+ }
721
+ if (this.walletConnectTransport) {
722
+ this.walletConnectTransport.stopListeningForNewPeers().catch(onError);
723
+ }
724
+ };
725
+ yield this.initInternalTransports();
726
+ if (!this.postMessageTransport ||
727
+ !this.p2pTransport ||
728
+ (!this.disableWalletConnect && !this.walletConnectTransport)) {
729
+ // Bare return here used to strand the promise forever -- no UI is
730
+ // emitted, no peer ever pairs, no abort path fires. Reject explicitly
731
+ // so callers see a deterministic error. (WalletConnect is exempt when
732
+ // disableWalletConnect is set -- its transport is intentionally absent.)
733
+ const noTransportError = {
734
+ type: octez_connect_types_1.BeaconMessageType.Error,
735
+ id: '',
736
+ senderId: '',
737
+ version: '2',
738
+ errorType: octez_connect_types_1.BeaconErrorType.UNKNOWN_ERROR
739
+ };
740
+ reject(noTransportError);
741
+ return;
742
+ }
743
+ this.postMessageTransport.connect().then().catch(console.error);
744
+ if (activeAccount && activeAccount.origin) {
745
+ const origin = activeAccount.origin.type;
746
+ // Select the transport that matches the active account
747
+ if (origin === octez_connect_types_1.Origin.EXTENSION) {
748
+ resolve(yield _super.init.call(this, this.postMessageTransport));
749
+ }
750
+ else if (origin === octez_connect_types_1.Origin.P2P) {
751
+ resolve(yield _super.init.call(this, this.p2pTransport));
752
+ }
753
+ else if (origin === octez_connect_types_1.Origin.WALLETCONNECT && this.walletConnectTransport) {
754
+ resolve(yield _super.init.call(this, this.walletConnectTransport));
755
+ }
756
+ }
757
+ else {
758
+ const p2pTransport = this.p2pTransport;
759
+ const postMessageTransport = this.postMessageTransport;
760
+ const walletConnectTransport = this.walletConnectTransport;
761
+ postMessageTransport
762
+ .listenForNewPeer((peer) => {
763
+ logger.log('init', 'postmessage transport peer connected', peer);
764
+ this.analytics.track('event', 'DAppClient', 'Extension connected', {
765
+ peerName: peer.name
766
+ });
767
+ this.events
768
+ .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
769
+ .catch((emitError) => console.warn(emitError));
770
+ this.setActivePeer(peer).catch(console.error);
771
+ this.setTransport(this.postMessageTransport).catch(console.error);
772
+ stopListening();
773
+ resolve(octez_connect_types_1.TransportType.POST_MESSAGE);
774
+ })
775
+ .catch(console.error);
776
+ p2pTransport
777
+ .listenForNewPeer((peer) => {
778
+ logger.log('init', 'p2p transport peer connected', peer);
779
+ this.analytics.track('event', 'DAppClient', 'octez.connect Wallet connected', {
780
+ peerName: peer.name
781
+ });
782
+ this.events
783
+ .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
784
+ .catch((emitError) => console.warn(emitError));
785
+ this.setActivePeer(peer).catch(console.error);
786
+ this.setTransport(this.p2pTransport).catch(console.error);
787
+ stopListening();
788
+ resolve(octez_connect_types_1.TransportType.P2P);
789
+ })
790
+ .catch(console.error);
791
+ walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.listenForNewPeer((peer) => {
792
+ logger.log('init', 'walletconnect transport peer connected', peer);
793
+ this.analytics.track('event', 'DAppClient', 'WalletConnect Wallet connected', {
794
+ peerName: peer.name
795
+ });
796
+ this.events
797
+ .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
798
+ .catch((emitError) => console.warn(emitError));
799
+ this.setActivePeer(peer).catch(console.error);
800
+ this.setTransport(this.walletConnectTransport).catch(console.error);
801
+ stopListening();
802
+ resolve(octez_connect_types_1.TransportType.WALLETCONNECT);
803
+ }).catch(console.error);
804
+ octez_connect_transport_postmessage_1.PostMessageTransport.getAvailableExtensions()
805
+ .then((extensions) => __awaiter(this, void 0, void 0, function* () {
806
+ this.analytics.track('event', 'DAppClient', 'Extensions detected', { extensions });
807
+ }))
808
+ .catch((error) => {
809
+ this._initPromise = undefined;
810
+ console.error(error);
542
811
  });
543
- this.events
544
- .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
545
- .catch((emitError) => console.warn(emitError));
546
- this.setActivePeer(peer).catch(console.error);
547
- this.setTransport(this.p2pTransport).catch(console.error);
548
- stopListening();
549
- resolve(octez_connect_types_1.TransportType.P2P);
550
- })
551
- .catch(console.error);
552
- walletConnectTransport
553
- .listenForNewPeer((peer) => {
554
- logger.log('init', 'walletconnect transport peer connected', peer);
555
- this.analytics.track('event', 'DAppClient', 'WalletConnect Wallet connected', {
556
- peerName: peer.name
812
+ const abortHandler = () => __awaiter(this, void 0, void 0, function* () {
813
+ var _a;
814
+ logger.log('init', 'ABORTED');
815
+ this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'abort'));
816
+ yield Promise.all([
817
+ postMessageTransport.disconnect(),
818
+ // p2pTransport.disconnect(), do not abort connection manually
819
+ walletConnectTransport === null || walletConnectTransport === void 0 ? void 0 : walletConnectTransport.disconnect()
820
+ ]);
821
+ this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
822
+ this._activeAccount.isResolved() && this.clearActiveAccount();
823
+ // Reject _initPromise so any awaiter (makeRequest -> requestPermissions)
824
+ // unwinds via handleRequestError instead of hanging. _initReject
825
+ // also clears _initPromise as a side effect.
826
+ (_a = this._initReject) === null || _a === void 0 ? void 0 : _a.call(this, this.createAbortedError());
557
827
  });
828
+ const serializer = new octez_connect_core_1.Serializer();
829
+ const p2pPeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
830
+ try {
831
+ yield p2pTransport.connect();
832
+ }
833
+ catch (err) {
834
+ logger.error(err);
835
+ yield this.hideUI(['alert']); // hide pairing alert
836
+ setTimeout(() => this.events.emit(events_1.BeaconEvent.GENERIC_ERROR, err.message), 1000);
837
+ abortHandler().catch((abortErr) => logger.warn('init', 'abortHandler', abortErr));
838
+ resolve('');
839
+ return;
840
+ }
841
+ resolve(yield serializer.serialize(yield p2pTransport.getPairingRequestInfo()));
842
+ }));
843
+ const walletConnectPeerInfo = walletConnectTransport
844
+ ? createLazyPromise(() => walletConnectTransport
845
+ .getPairingRequestInfo()
846
+ .then((pairingRequestInfo) => pairingRequestInfo.uri)
847
+ .catch((error) => {
848
+ logger.warn('init', 'walletconnect pairing request failed', error);
849
+ return '';
850
+ }))
851
+ : undefined;
852
+ const postmessagePeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
853
+ resolve(yield serializer.serialize(yield postMessageTransport.getPairingRequestInfo()));
854
+ }));
558
855
  this.events
559
- .emit(events_1.BeaconEvent.PAIR_SUCCESS, peer)
856
+ .emit(events_1.BeaconEvent.PAIR_INIT, Object.assign(Object.assign({ p2pPeerInfo,
857
+ postmessagePeerInfo }, (walletConnectPeerInfo ? { walletConnectPeerInfo } : {})), { networkType: this.network.type, abortedHandler: abortHandler.bind(this), disclaimerText: this.disclaimerText, analytics: this.analytics, featuredWallets: this.featuredWallets, substratePairing }))
560
858
  .catch((emitError) => console.warn(emitError));
561
- this.setActivePeer(peer).catch(console.error);
562
- this.setTransport(this.walletConnectTransport).catch(console.error);
563
- stopListening();
564
- resolve(octez_connect_types_1.TransportType.WALLETCONNECT);
565
- })
566
- .catch(console.error);
567
- octez_connect_transport_postmessage_1.PostMessageTransport.getAvailableExtensions()
568
- .then((extensions) => __awaiter(this, void 0, void 0, function* () {
569
- this.analytics.track('event', 'DAppClient', 'Extensions detected', { extensions });
570
- }))
571
- .catch((error) => {
572
- this._initPromise = undefined;
573
- console.error(error);
574
- });
575
- const abortHandler = () => __awaiter(this, void 0, void 0, function* () {
576
- logger.log('init', 'ABORTED');
577
- this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'abort'));
578
- yield Promise.all([
579
- postMessageTransport.disconnect(),
580
- // p2pTransport.disconnect(), do not abort connection manually
581
- walletConnectTransport.disconnect()
582
- ]);
583
- this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
584
- this._activeAccount.isResolved() && this.clearActiveAccount();
585
- this._initPromise = undefined;
586
- });
587
- const serializer = new octez_connect_core_1.Serializer();
588
- const p2pPeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
589
- try {
590
- yield p2pTransport.connect();
591
- }
592
- catch (err) {
593
- logger.error(err);
594
- yield this.hideUI(['alert']); // hide pairing alert
595
- setTimeout(() => this.events.emit(events_1.BeaconEvent.GENERIC_ERROR, err.message), 1000);
596
- abortHandler();
597
- resolve('');
598
- return;
599
- }
600
- resolve(yield serializer.serialize(yield p2pTransport.getPairingRequestInfo()));
601
- }));
602
- const walletConnectPeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
603
- resolve((yield walletConnectTransport.getPairingRequestInfo()).uri);
604
- }));
605
- const postmessagePeerInfo = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
606
- resolve(yield serializer.serialize(yield postMessageTransport.getPairingRequestInfo()));
607
- }));
608
- this.events
609
- .emit(events_1.BeaconEvent.PAIR_INIT, {
610
- p2pPeerInfo,
611
- postmessagePeerInfo,
612
- walletConnectPeerInfo,
613
- networkType: this.network.type,
614
- abortedHandler: abortHandler.bind(this),
615
- disclaimerText: this.disclaimerText,
616
- analytics: this.analytics,
617
- featuredWallets: this.featuredWallets,
618
- substratePairing
619
- })
620
- .catch((emitError) => console.warn(emitError));
859
+ }
621
860
  }
622
861
  }
862
+ catch (err) {
863
+ // An async-executor throw would otherwise be silently swallowed,
864
+ // leaving _initPromise pending and every awaiter stranded.
865
+ reject(err);
866
+ }
623
867
  }));
868
+ // Drop the _initReject reference whether init resolved or rejected, so the
869
+ // field is never stale. Using an explicit helper keeps the dangling
870
+ // observer chain out of statement position (which the lint rules forbid).
871
+ this.clearInitRejectOnSettle(this._initPromise);
624
872
  return this._initPromise;
625
873
  });
626
874
  }
875
+ /**
876
+ * Attach a settle observer to {@link _initPromise} that drops
877
+ * {@link _initReject} once the promise settles (resolved or rejected).
878
+ * Errors on the observer chain are swallowed; only the original
879
+ * {@link _initPromise} surfaces them to its awaiters.
880
+ */
881
+ clearInitRejectOnSettle(initPromise) {
882
+ initPromise
883
+ .finally(() => {
884
+ this._initReject = undefined;
885
+ this._initSubstratePairing = undefined;
886
+ })
887
+ .catch(() => {
888
+ // observer-only; original promise's rejection is delivered to awaiters
889
+ });
890
+ }
627
891
  /**
628
892
  * Returns the active account
629
893
  */
630
894
  getActiveAccount() {
631
895
  return __awaiter(this, void 0, void 0, function* () {
896
+ this.assertNotDestroyed();
632
897
  return this._activeAccount.promise;
633
898
  });
634
899
  }
900
+ retainStorageValidationPromise() {
901
+ // Constructor validation is intentionally fire-and-forget, but retaining
902
+ // the promise gives tests a deterministic hook without changing public API.
903
+ this.storageValidated.catch((err) => logger.error(err.message));
904
+ }
905
+ deactivateInvalidAccountState(reason) {
906
+ return __awaiter(this, void 0, void 0, function* () {
907
+ if (this.hasEmittedInvalidAccountDeactivated) {
908
+ return;
909
+ }
910
+ this.hasEmittedInvalidAccountDeactivated = true;
911
+ logger.log('deactivateInvalidAccountState', reason);
912
+ if (reason === 'missing_active_account') {
913
+ yield this.repairMissingActiveAccount();
914
+ }
915
+ else {
916
+ yield this.resetInvalidState(false);
917
+ }
918
+ yield this.events.emit(events_1.BeaconEvent.INVALID_ACCOUNT_DEACTIVATED, { reason });
919
+ });
920
+ }
921
+ repairMissingActiveAccount() {
922
+ return __awaiter(this, void 0, void 0, function* () {
923
+ this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(undefined);
924
+ yield this.storage.delete(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT);
925
+ yield this.setActivePeer(undefined);
926
+ });
927
+ }
928
+ hasStoredActiveAccountPointer() {
929
+ // LocalStorage-specific repair: that backend is where literal sentinel strings were produced.
930
+ if (typeof localStorage === 'undefined') {
931
+ return false;
932
+ }
933
+ try {
934
+ return localStorage.getItem(this.storage.getPrefixedKey(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT)) !== null;
935
+ }
936
+ catch (_a) {
937
+ return false;
938
+ }
939
+ }
635
940
  isInvalidState(account) {
636
941
  return __awaiter(this, void 0, void 0, function* () {
637
942
  const activeAccount = yield this._activeAccount.promise;
@@ -643,11 +948,15 @@ class DAppClient extends octez_connect_core_1.Client {
643
948
  resetInvalidState() {
644
949
  return __awaiter(this, arguments, void 0, function* (emit = true) {
645
950
  var _a, _b;
646
- this.accountManager.removeAllAccounts();
951
+ yield this.accountManager.removeAllAccounts();
647
952
  this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(undefined);
648
- this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, undefined);
649
- emit && this.events.emit(events_1.BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
650
- !emit && this.hideUI(['alert']);
953
+ yield this.storage.delete(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT);
954
+ if (emit) {
955
+ yield this.events.emit(events_1.BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
956
+ }
957
+ else {
958
+ yield this.hideUI(['alert']);
959
+ }
651
960
  yield Promise.all([
652
961
  (_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
653
962
  (_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
@@ -666,7 +975,8 @@ class DAppClient extends octez_connect_core_1.Client {
666
975
  setActiveAccount(account) {
667
976
  return __awaiter(this, void 0, void 0, function* () {
668
977
  var _a;
669
- if (!this.isGetActiveAccountHandled) {
978
+ const activeAccountAlreadyCleared = !account && (yield this.isActiveAccountAlreadyCleared());
979
+ if (!activeAccountAlreadyCleared && !this.isGetActiveAccountHandled) {
670
980
  console.warn(`An active account has been received, but no active subscription was found for BeaconEvent.ACTIVE_ACCOUNT_SET.`);
671
981
  }
672
982
  if (account && this._activeAccount.isSettled() && (yield this.isInvalidState(account))) {
@@ -678,9 +988,9 @@ class DAppClient extends octez_connect_core_1.Client {
678
988
  }
679
989
  // when I'm resetting the activeAccount
680
990
  if (!account && this._activeAccount.isResolved() && (yield this.getActiveAccount())) {
681
- const transport = yield this.transport;
991
+ const transport = this._transport.isResolved() ? yield this.transport : undefined;
682
992
  const activeAccount = yield this.getActiveAccount();
683
- if (!transport || !activeAccount) {
993
+ if (!activeAccount) {
684
994
  return;
685
995
  }
686
996
  if (!this.debounceSetActiveAccount && transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport) {
@@ -696,22 +1006,15 @@ class DAppClient extends octez_connect_core_1.Client {
696
1006
  type: 'DISCONNECT'
697
1007
  });
698
1008
  }
699
- Array.from(this.openRequests.entries())
700
- .filter(([id, _promise]) => id !== 'session_update')
701
- .forEach(([id, promise]) => {
702
- promise.reject({
703
- type: octez_connect_types_1.BeaconMessageType.Error,
704
- errorType: octez_connect_types_1.BeaconErrorType.ABORTED_ERROR,
705
- id,
706
- senderId: '',
707
- version: '2'
708
- });
709
- });
710
- this.openRequests.clear();
1009
+ this.abortOpenRequests();
711
1010
  this.debounceSetActiveAccount = false;
712
1011
  }
713
1012
  }
714
- if (this._activeAccount.isSettled()) {
1013
+ if (activeAccountAlreadyCleared) {
1014
+ // Keep peer and transport cleanup below idempotent, but do not write storage or emit
1015
+ // another ACTIVE_ACCOUNT_SET(undefined) for an already-empty account.
1016
+ }
1017
+ else if (this._activeAccount.isSettled()) {
715
1018
  // If the promise has already been resolved we need to create a new one.
716
1019
  this._activeAccount = octez_connect_utils_1.ExposedPromise.resolve(account);
717
1020
  }
@@ -752,11 +1055,42 @@ class DAppClient extends octez_connect_core_1.Client {
752
1055
  yield this.setActivePeer(undefined);
753
1056
  yield this.setTransport(undefined);
754
1057
  }
755
- yield this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, account ? account.accountIdentifier : undefined);
756
- yield this.events.emit(events_1.BeaconEvent.ACTIVE_ACCOUNT_SET, account);
1058
+ if (account) {
1059
+ yield this.storage.set(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT, account.accountIdentifier);
1060
+ }
1061
+ else if (!activeAccountAlreadyCleared) {
1062
+ yield this.storage.delete(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT);
1063
+ }
1064
+ if (!activeAccountAlreadyCleared) {
1065
+ yield this.events.emit(events_1.BeaconEvent.ACTIVE_ACCOUNT_SET, account);
1066
+ }
757
1067
  return;
758
1068
  });
759
1069
  }
1070
+ isActiveAccountAlreadyCleared() {
1071
+ return __awaiter(this, void 0, void 0, function* () {
1072
+ return this._activeAccount.isResolved() && !(yield this.getActiveAccount());
1073
+ });
1074
+ }
1075
+ recoverActiveAccountFromAccountsChange() {
1076
+ return __awaiter(this, void 0, void 0, function* () {
1077
+ const activeAccountIdentifier = yield this.storage.get(octez_connect_types_1.StorageKey.ACTIVE_ACCOUNT);
1078
+ if (!activeAccountIdentifier || activeAccountIdentifier === 'undefined') {
1079
+ return;
1080
+ }
1081
+ if (this._activeAccount.isResolved()) {
1082
+ const activeAccount = yield this.getActiveAccount();
1083
+ if ((activeAccount === null || activeAccount === void 0 ? void 0 : activeAccount.accountIdentifier) === activeAccountIdentifier) {
1084
+ return;
1085
+ }
1086
+ }
1087
+ const account = yield this.getAccount(activeAccountIdentifier);
1088
+ if (!account) {
1089
+ return;
1090
+ }
1091
+ yield this.setActiveAccount(account);
1092
+ });
1093
+ }
760
1094
  /**
761
1095
  * Clear the active account
762
1096
  */
@@ -846,32 +1180,16 @@ class DAppClient extends octez_connect_core_1.Client {
846
1180
  };
847
1181
  });
848
1182
  }
849
- updateMetricsStorage(payload) {
850
- return __awaiter(this, void 0, void 0, function* () {
851
- const queue = yield this.beaconIDB.getAllKeys('metrics');
852
- if (queue.length >= 1000) {
853
- const key = queue.shift();
854
- this.beaconIDB.delete(key.toString(), 'metrics');
855
- }
856
- this.beaconIDB.set(String(Date.now()), payload, 'metrics');
857
- });
858
- }
859
- sendMetrics(uri, options, thenHandler, catchHandler) {
860
- if (!this.enableMetrics && uri === 'performance-metrics/save') {
861
- options && this.updateMetricsStorage(options.body);
862
- }
863
- if (!this.enableMetrics) {
864
- return;
865
- }
866
- fetch(`${octez_connect_core_1.BACKEND_URL}/${uri}`, options)
867
- .then((res) => thenHandler && thenHandler(res))
868
- .catch((err) => {
869
- console.warn('Network error encountered. Metrics sharing have been automatically disabled.');
870
- logger.error(err.message);
871
- this.enableMetrics = false; // in the event of a network error, stop sending metrics
872
- catchHandler && catchHandler(err);
873
- });
1183
+ // Metrics phone home to a Papers-controlled endpoint that the ECAD fork
1184
+ // does not control or ingest. The disabled path also queued payloads into
1185
+ // IndexedDB before the DB was ready, surfacing as unhandledRejection.
1186
+ // Hard-disabled here; full removal (option, storage key, IDB store,
1187
+ // BACKEND_URL constant, all call sites) tracked as a follow-up.
1188
+ /* eslint-disable @typescript-eslint/no-unused-vars -- hard-disabled stub keeps its signature for call-site type-safety; params are intentionally unused pending full removal. */
1189
+ sendMetrics(_uri, _options, _thenHandler, _catchHandler) {
1190
+ return;
874
1191
  }
1192
+ /* eslint-enable @typescript-eslint/no-unused-vars */
875
1193
  checkMakeRequest() {
876
1194
  return __awaiter(this, void 0, void 0, function* () {
877
1195
  const isResolved = this._transport.isResolved();
@@ -931,14 +1249,19 @@ class DAppClient extends octez_connect_core_1.Client {
931
1249
  * Remove all peers and all accounts that have been connected through those peers
932
1250
  */
933
1251
  removeAllPeers() {
934
- return __awaiter(this, arguments, void 0, function* (sendDisconnectToPeers = false) {
935
- const transport = yield this.transport;
1252
+ return __awaiter(this, arguments, void 0, function* (sendDisconnectToPeers = false, options = {}) {
1253
+ const transport = (yield this.transport);
936
1254
  const peers = yield transport.getPeers();
937
- const removePeerResult = transport.removeAllPeers();
1255
+ const removePeerResult = yield transport.removeAllPeers();
938
1256
  yield this.removeAccountsForPeers(peers);
939
1257
  if (sendDisconnectToPeers) {
940
1258
  const disconnectPromises = peers.map((peer) => this.sendDisconnectToPeer(peer, transport));
941
- yield Promise.all(disconnectPromises);
1259
+ if (options.ignoreDisconnectErrors) {
1260
+ yield Promise.allSettled(disconnectPromises);
1261
+ }
1262
+ else {
1263
+ yield Promise.all(disconnectPromises);
1264
+ }
942
1265
  }
943
1266
  return removePeerResult;
944
1267
  });
@@ -1044,13 +1367,15 @@ class DAppClient extends octez_connect_core_1.Client {
1044
1367
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
1045
1368
  const logId = `makeRequestV3 ${Date.now()}`;
1046
1369
  logger.time(true, logId);
1047
- const { message: response, connectionInfo } = yield this.makeRequestV3(request).catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1048
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1049
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1050
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1051
- logger.time(false, logId);
1052
- throw yield this.handleRequestError(request, requestError);
1053
- }));
1370
+ let resolved;
1371
+ try {
1372
+ resolved = yield this.makeRequestV3(request);
1373
+ }
1374
+ catch (requestError) {
1375
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1376
+ throw requestError;
1377
+ }
1378
+ const { message: response, connectionInfo } = resolved;
1054
1379
  logger.time(false, logId);
1055
1380
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
1056
1381
  logger.log('RESPONSE V3', response, connectionInfo);
@@ -1112,14 +1437,18 @@ class DAppClient extends octez_connect_core_1.Client {
1112
1437
  const res = (yield this.checkMakeRequest())
1113
1438
  ? this.makeRequestV3(request)
1114
1439
  : this.makeRequestBC(request);
1115
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1116
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1117
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1118
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1119
- logger.time(false, logId);
1120
- throw yield this.handleRequestError(request, requestError);
1121
- }));
1122
- const { message: response, connectionInfo } = (yield res);
1440
+ let resolved;
1441
+ try {
1442
+ resolved = yield res;
1443
+ }
1444
+ catch (requestError) {
1445
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1446
+ throw requestError;
1447
+ }
1448
+ if (!resolved) {
1449
+ throw new Error('Internal error: makeRequest returned no result');
1450
+ }
1451
+ const { message: response, connectionInfo } = resolved;
1123
1452
  logger.time(false, logId);
1124
1453
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
1125
1454
  yield blockchain.handleResponse({
@@ -1145,16 +1474,36 @@ class DAppClient extends octez_connect_core_1.Client {
1145
1474
  */
1146
1475
  requestPermissions(input) {
1147
1476
  return __awaiter(this, void 0, void 0, function* () {
1148
- if (input === null || input === void 0 ? void 0 : input.network) {
1149
- throw new Error('[BEACON] the "network" property is no longer accepted in input. Please provide it when instantiating DAppClient.');
1477
+ this.assertNotDestroyed();
1478
+ const scopes = this.getPermissionRequestScopes(input);
1479
+ const requestPermissionsKey = this.getPermissionRequestKey(scopes);
1480
+ if (this._requestPermissionsPromise) {
1481
+ if (this._requestPermissionsKey !== requestPermissionsKey) {
1482
+ throw new Error('Cannot start a permission request with different scopes while another permission request is pending');
1483
+ }
1484
+ return this._requestPermissionsPromise;
1485
+ }
1486
+ const requestPermissionsPromise = this.requestPermissionsInternal(scopes);
1487
+ this._requestPermissionsPromise = requestPermissionsPromise;
1488
+ this._requestPermissionsKey = requestPermissionsKey;
1489
+ try {
1490
+ return yield requestPermissionsPromise;
1150
1491
  }
1492
+ finally {
1493
+ if (this._requestPermissionsPromise === requestPermissionsPromise) {
1494
+ this._requestPermissionsPromise = undefined;
1495
+ this._requestPermissionsKey = undefined;
1496
+ }
1497
+ }
1498
+ });
1499
+ }
1500
+ requestPermissionsInternal(scopes) {
1501
+ return __awaiter(this, void 0, void 0, function* () {
1151
1502
  const request = {
1152
1503
  appMetadata: yield this.getOwnAppMetadata(),
1153
1504
  type: octez_connect_types_1.BeaconMessageType.PermissionRequest,
1154
1505
  network: this.network,
1155
- scopes: input && input.scopes
1156
- ? input.scopes
1157
- : [octez_connect_types_1.PermissionScope.OPERATION_REQUEST, octez_connect_types_1.PermissionScope.SIGN]
1506
+ scopes
1158
1507
  };
1159
1508
  this.analytics.track('event', 'DAppClient', 'Permission requested');
1160
1509
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'start'));
@@ -1163,14 +1512,18 @@ class DAppClient extends octez_connect_core_1.Client {
1163
1512
  const res = (yield this.checkMakeRequest()) || !(yield this.getActiveAccount())
1164
1513
  ? this.makeRequest(request, undefined, undefined)
1165
1514
  : this.makeRequestBC(request);
1166
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1167
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1168
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1169
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1170
- logger.time(false, logId);
1171
- throw yield this.handleRequestError(request, requestError);
1172
- }));
1173
- const { message, connectionInfo } = (yield res);
1515
+ let resolved;
1516
+ try {
1517
+ resolved = yield res;
1518
+ }
1519
+ catch (requestError) {
1520
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1521
+ throw requestError;
1522
+ }
1523
+ if (!resolved) {
1524
+ throw new Error('Internal error: makeRequest returned no result');
1525
+ }
1526
+ const { message, connectionInfo } = resolved;
1174
1527
  logger.time(false, logId);
1175
1528
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('connect', 'success'));
1176
1529
  logger.log('requestPermissions', '######## MESSAGE #######');
@@ -1193,6 +1546,17 @@ class DAppClient extends octez_connect_core_1.Client {
1193
1546
  return output;
1194
1547
  });
1195
1548
  }
1549
+ getPermissionRequestScopes(input) {
1550
+ if (input && 'network' in input) {
1551
+ throw new Error('[BEACON] the "network" property is no longer accepted in input. Please provide it when instantiating DAppClient.');
1552
+ }
1553
+ return input && input.scopes
1554
+ ? input.scopes
1555
+ : [octez_connect_types_1.PermissionScope.OPERATION_REQUEST, octez_connect_types_1.PermissionScope.SIGN];
1556
+ }
1557
+ getPermissionRequestKey(scopes) {
1558
+ return [...scopes].sort().join('|');
1559
+ }
1196
1560
  /**
1197
1561
  * Send a proof of event request to the wallet. The wallet will either accept or decline the challenge.
1198
1562
  * If it is accepted, the challenge will be stored, meaning that even if the user refresh the page, the DAppClient will keep checking if the challenge has been fulfilled.
@@ -1220,14 +1584,18 @@ class DAppClient extends octez_connect_core_1.Client {
1220
1584
  const res = (yield this.checkMakeRequest())
1221
1585
  ? this.makeRequest(request)
1222
1586
  : this.makeRequestBC(request);
1223
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1224
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1225
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1226
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1227
- logger.time(false, logId);
1228
- throw yield this.handleRequestError(request, requestError);
1229
- }));
1230
- const { message, connectionInfo } = (yield res);
1587
+ let resolved;
1588
+ try {
1589
+ resolved = yield res;
1590
+ }
1591
+ catch (requestError) {
1592
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1593
+ throw requestError;
1594
+ }
1595
+ if (!resolved) {
1596
+ throw new Error('Internal error: makeRequest returned no result');
1597
+ }
1598
+ const { message, connectionInfo } = resolved;
1231
1599
  logger.time(false, logId);
1232
1600
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
1233
1601
  this.analytics.track('event', 'DAppClient', `Proof of event challenge ${message.isAccepted ? 'accepted' : 'refused'}`, { address: activeAccount.address });
@@ -1264,14 +1632,18 @@ class DAppClient extends octez_connect_core_1.Client {
1264
1632
  const res = (yield this.checkMakeRequest())
1265
1633
  ? this.makeRequest(request)
1266
1634
  : this.makeRequestBC(request);
1267
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1268
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1269
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1270
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1271
- logger.time(false, logId);
1272
- throw yield this.handleRequestError(request, requestError);
1273
- }));
1274
- const { message, connectionInfo } = (yield res);
1635
+ let resolved;
1636
+ try {
1637
+ resolved = yield res;
1638
+ }
1639
+ catch (requestError) {
1640
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1641
+ throw requestError;
1642
+ }
1643
+ if (!resolved) {
1644
+ throw new Error('Internal error: makeRequest returned no result');
1645
+ }
1646
+ const { message, connectionInfo } = resolved;
1275
1647
  logger.time(false, logId);
1276
1648
  this.analytics.track('event', 'DAppClient', `Simulated proof of event challenge ${!message.errorMessage ? 'accepted' : 'refused'}`, { address: activeAccount.address });
1277
1649
  yield this.notifySuccess(request, {
@@ -1333,14 +1705,18 @@ class DAppClient extends octez_connect_core_1.Client {
1333
1705
  const res = (yield this.checkMakeRequest())
1334
1706
  ? this.makeRequest(request)
1335
1707
  : this.makeRequestBC(request);
1336
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1337
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1338
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1339
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1340
- logger.time(false, logId);
1341
- throw yield this.handleRequestError(request, requestError);
1342
- }));
1343
- const { message, connectionInfo } = (yield res);
1708
+ let resolved;
1709
+ try {
1710
+ resolved = yield res;
1711
+ }
1712
+ catch (requestError) {
1713
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1714
+ throw requestError;
1715
+ }
1716
+ if (!resolved) {
1717
+ throw new Error('Internal error: makeRequest returned no result');
1718
+ }
1719
+ const { message, connectionInfo } = resolved;
1344
1720
  logger.time(false, logId);
1345
1721
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
1346
1722
  yield this.notifySuccess(request, {
@@ -1430,14 +1806,18 @@ class DAppClient extends octez_connect_core_1.Client {
1430
1806
  const res = (yield this.checkMakeRequest())
1431
1807
  ? this.makeRequest(request)
1432
1808
  : this.makeRequestBC(request);
1433
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1434
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1435
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1436
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1437
- logger.time(false, logId);
1438
- throw yield this.handleRequestError(request, requestError);
1439
- }));
1440
- const { message, connectionInfo } = (yield res);
1809
+ let resolved;
1810
+ try {
1811
+ resolved = yield res;
1812
+ }
1813
+ catch (requestError) {
1814
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1815
+ throw requestError;
1816
+ }
1817
+ if (!resolved) {
1818
+ throw new Error('Internal error: makeRequest returned no result');
1819
+ }
1820
+ const { message, connectionInfo } = resolved;
1441
1821
  logger.time(false, logId);
1442
1822
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
1443
1823
  yield this.notifySuccess(request, {
@@ -1480,14 +1860,18 @@ class DAppClient extends octez_connect_core_1.Client {
1480
1860
  const res = (yield this.checkMakeRequest())
1481
1861
  ? this.makeRequest(request)
1482
1862
  : this.makeRequestBC(request);
1483
- res.catch((requestError) => __awaiter(this, void 0, void 0, function* () {
1484
- requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR
1485
- ? this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'))
1486
- : this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
1487
- logger.time(false, logId);
1488
- throw yield this.handleRequestError(request, requestError);
1489
- }));
1490
- const { message, connectionInfo } = (yield res);
1863
+ let resolved;
1864
+ try {
1865
+ resolved = yield res;
1866
+ }
1867
+ catch (requestError) {
1868
+ yield this.runRequestErrorSideEffects(request, requestError, logId);
1869
+ throw requestError;
1870
+ }
1871
+ if (!resolved) {
1872
+ throw new Error('Internal error: makeRequest returned no result');
1873
+ }
1874
+ const { message, connectionInfo } = resolved;
1491
1875
  logger.time(false, logId);
1492
1876
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'success'));
1493
1877
  yield this.notifySuccess(request, {
@@ -1561,13 +1945,17 @@ class DAppClient extends octez_connect_core_1.Client {
1561
1945
  */
1562
1946
  removeAccountsForPeers(peersToRemove) {
1563
1947
  return __awaiter(this, void 0, void 0, function* () {
1948
+ if (peersToRemove.length === 0) {
1949
+ return;
1950
+ }
1564
1951
  const peerIdsToRemove = peersToRemove.map((peer) => peer.senderId);
1565
1952
  return this.removeAccountsForPeerIds(peerIdsToRemove);
1566
1953
  });
1567
1954
  }
1568
1955
  removeAccountsForPeerIds(peerIds) {
1569
1956
  return __awaiter(this, void 0, void 0, function* () {
1570
- const accounts = yield this.accountManager.getAccounts();
1957
+ var _a;
1958
+ const accounts = (_a = (yield this.accountManager.getAccounts())) !== null && _a !== void 0 ? _a : [];
1571
1959
  // Remove all accounts with origin of the specified peer
1572
1960
  const accountsToRemove = accounts.filter((account) => peerIds.includes(account.senderId));
1573
1961
  const accountIdentifiersToRemove = accountsToRemove.map((accountInfo) => accountInfo.accountIdentifier);
@@ -1581,6 +1969,46 @@ class DAppClient extends octez_connect_core_1.Client {
1581
1969
  }
1582
1970
  });
1583
1971
  }
1972
+ /**
1973
+ * Run the standard request-error side effects (abort/error metric,
1974
+ * timer stop, {@link handleRequestError}) on a single awaited control
1975
+ * flow so callers can re-throw the original {@link ErrorResponse} without
1976
+ * leaking an UnhandledPromiseRejection.
1977
+ *
1978
+ * Why this exists: every request method historically used a fire-and-
1979
+ * forget `res.catch(handler)` whose handler did `throw await
1980
+ * this.handleRequestError(...)`. The catch returned a detached promise
1981
+ * nobody awaited; `handleRequestError`'s thrown wrapped error became an
1982
+ * unhandled rejection. The bug rarely surfaced before the dapp-init
1983
+ * promise was made rejectable, because pre-pairing rejection paths used
1984
+ * to hang instead of routing through these handlers. Once init started
1985
+ * rejecting reliably, the unhandled rejection started firing on every
1986
+ * WC session-proposal rejection. We swallow the wrapped throw here so
1987
+ * callers can re-throw the original `ErrorResponse`, matching the
1988
+ * post-pairing rejection contract at the openRequest path.
1989
+ *
1990
+ * @param request The request we sent
1991
+ * @param requestError The error we received
1992
+ * @param logId The {@link logger.time} label opened for this request
1993
+ */
1994
+ runRequestErrorSideEffects(request, rawError, logId) {
1995
+ return __awaiter(this, void 0, void 0, function* () {
1996
+ // V3 request shapes (PermissionRequestV3, BlockchainRequestV3) flow through
1997
+ // here too. They aren't part of BeaconRequestInputMessage, but
1998
+ // handleRequestError only inspects `.type` plus optional fields it already
1999
+ // guards on. Catch params are `unknown` under strict TS, same story.
2000
+ const typedRequest = request;
2001
+ const requestError = rawError;
2002
+ if (requestError.errorType === octez_connect_types_1.BeaconErrorType.ABORTED_ERROR) {
2003
+ this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'abort'));
2004
+ }
2005
+ else {
2006
+ this.sendMetrics('performance-metrics/save', yield this.buildPayload('message', 'error'));
2007
+ }
2008
+ logger.time(false, logId);
2009
+ yield this.handleRequestError(typedRequest, requestError).catch(() => undefined);
2010
+ });
2011
+ }
1584
2012
  /**
1585
2013
  * This message handles errors that we receive from the wallet.
1586
2014
  *
@@ -1763,20 +2191,10 @@ class DAppClient extends octez_connect_core_1.Client {
1763
2191
  }
1764
2192
  makeRequest(requestInput, skipResponse, otherTabMessageId) {
1765
2193
  return __awaiter(this, void 0, void 0, function* () {
1766
- var _a, _b, _c;
2194
+ var _a;
1767
2195
  const messageId = otherTabMessageId !== null && otherTabMessageId !== void 0 ? otherTabMessageId : (yield (0, octez_connect_utils_1.generateGUID)());
1768
- if (this._initPromise && this.isInitPending) {
1769
- yield Promise.all([
1770
- (_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
1771
- (_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
1772
- ]);
1773
- this._initPromise = undefined;
1774
- this.hideUI(['toast']);
1775
- }
1776
2196
  logger.log('makeRequest', 'starting');
1777
- this.isInitPending = true;
1778
2197
  yield this.init();
1779
- this.isInitPending = false;
1780
2198
  logger.log('makeRequest', 'after init');
1781
2199
  if (yield this.addRequestAndCheckIfRateLimited()) {
1782
2200
  this.events
@@ -1797,20 +2215,34 @@ class DAppClient extends octez_connect_core_1.Client {
1797
2215
  exposed = new octez_connect_utils_1.ExposedPromise();
1798
2216
  this.addOpenRequest(request.id, exposed);
1799
2217
  }
1800
- const payload = yield new octez_connect_core_1.Serializer().serialize(request);
1801
- const account = yield this.getActiveAccount();
1802
- const peer = yield this.getPeer(account);
1803
- const walletInfo = yield this.getWalletInfo(peer, account);
1804
- logger.log('makeRequest', 'sending message', request);
1805
2218
  try {
1806
- ;
1807
- (yield this.transport).send(payload, peer);
2219
+ const payload = yield new octez_connect_core_1.Serializer().serialize(request);
2220
+ const account = yield this.getActiveAccount();
2221
+ const peer = yield this.getPeer(account);
2222
+ const walletInfo = yield this.getWalletInfo(peer, account);
2223
+ logger.log('makeRequest', 'sending message', request);
2224
+ yield (yield this.transport).send(payload, peer);
1808
2225
  if (request.type !== octez_connect_types_1.BeaconMessageType.PermissionRequest ||
1809
2226
  (this._activeAccount.isResolved() && (yield this._activeAccount.promise))) {
1810
2227
  this.tryToAppSwitch();
1811
2228
  }
2229
+ if (!otherTabMessageId) {
2230
+ this.events
2231
+ .emit(beacon_message_events_1.messageEvents[requestInput.type].sent, {
2232
+ walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_a = walletInfo.name) !== null && _a !== void 0 ? _a : 'Wallet' }),
2233
+ extraInfo: {
2234
+ resetCallback: () => __awaiter(this, void 0, void 0, function* () {
2235
+ yield this.disconnect();
2236
+ })
2237
+ }
2238
+ })
2239
+ .catch((emitError) => logger.warn('makeRequest', emitError));
2240
+ }
1812
2241
  }
1813
2242
  catch (sendError) {
2243
+ if (!skipResponse) {
2244
+ this.clearOpenRequest(request.id);
2245
+ }
1814
2246
  this.events.emit(events_1.BeaconEvent.INTERNAL_ERROR, {
1815
2247
  text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
1816
2248
  buttons: [
@@ -1825,18 +2257,6 @@ class DAppClient extends octez_connect_core_1.Client {
1825
2257
  });
1826
2258
  throw sendError;
1827
2259
  }
1828
- if (!otherTabMessageId) {
1829
- this.events
1830
- .emit(beacon_message_events_1.messageEvents[requestInput.type].sent, {
1831
- walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_c = walletInfo.name) !== null && _c !== void 0 ? _c : 'Wallet' }),
1832
- extraInfo: {
1833
- resetCallback: () => __awaiter(this, void 0, void 0, function* () {
1834
- this.disconnect();
1835
- })
1836
- }
1837
- })
1838
- .catch((emitError) => console.warn(emitError));
1839
- }
1840
2260
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1841
2261
  return exposed === null || exposed === void 0 ? void 0 : exposed.promise; // TODO: fix type
1842
2262
  });
@@ -1851,20 +2271,10 @@ class DAppClient extends octez_connect_core_1.Client {
1851
2271
  */
1852
2272
  makeRequestV3(requestInput, otherTabMessageId) {
1853
2273
  return __awaiter(this, void 0, void 0, function* () {
1854
- var _a, _b, _c;
1855
- if (this._initPromise && this.isInitPending) {
1856
- yield Promise.all([
1857
- (_a = this.postMessageTransport) === null || _a === void 0 ? void 0 : _a.disconnect(),
1858
- (_b = this.walletConnectTransport) === null || _b === void 0 ? void 0 : _b.disconnect()
1859
- ]);
1860
- this._initPromise = undefined;
1861
- this.hideUI(['toast']);
1862
- }
2274
+ var _a;
1863
2275
  const messageId = otherTabMessageId !== null && otherTabMessageId !== void 0 ? otherTabMessageId : (yield (0, octez_connect_utils_1.generateGUID)());
1864
2276
  logger.log('makeRequest', 'starting');
1865
- this.isInitPending = true;
1866
2277
  yield this.init(undefined, true);
1867
- this.isInitPending = false;
1868
2278
  logger.log('makeRequest', 'after init');
1869
2279
  if (yield this.addRequestAndCheckIfRateLimited()) {
1870
2280
  this.events
@@ -1883,20 +2293,31 @@ class DAppClient extends octez_connect_core_1.Client {
1883
2293
  };
1884
2294
  const exposed = new octez_connect_utils_1.ExposedPromise();
1885
2295
  this.addOpenRequest(request.id, exposed);
1886
- const payload = yield new octez_connect_core_1.Serializer().serialize(request);
1887
- const account = yield this.getActiveAccount();
1888
- const peer = yield this.getPeer(account);
1889
- const walletInfo = yield this.getWalletInfo(peer, account);
1890
- logger.log('makeRequest', 'sending message', request);
1891
2296
  try {
1892
- ;
1893
- (yield this.transport).send(payload, peer);
2297
+ const payload = yield new octez_connect_core_1.Serializer().serialize(request);
2298
+ const account = yield this.getActiveAccount();
2299
+ const peer = yield this.getPeer(account);
2300
+ const walletInfo = yield this.getWalletInfo(peer, account);
2301
+ logger.log('makeRequest', 'sending message', request);
2302
+ yield (yield this.transport).send(payload, peer);
1894
2303
  if (request.message.type !== octez_connect_types_1.BeaconMessageType.PermissionRequest ||
1895
2304
  (this._activeAccount.isResolved() && (yield this._activeAccount.promise))) {
1896
2305
  this.tryToAppSwitch();
1897
2306
  }
2307
+ const index = requestInput.type;
2308
+ this.events
2309
+ .emit(beacon_message_events_1.messageEvents[index].sent, {
2310
+ walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_a = walletInfo.name) !== null && _a !== void 0 ? _a : 'Wallet' }),
2311
+ extraInfo: {
2312
+ resetCallback: () => __awaiter(this, void 0, void 0, function* () {
2313
+ yield this.disconnect();
2314
+ })
2315
+ }
2316
+ })
2317
+ .catch((emitError) => logger.warn('makeRequestV3', emitError));
1898
2318
  }
1899
2319
  catch (sendError) {
2320
+ this.clearOpenRequest(request.id);
1900
2321
  this.events.emit(events_1.BeaconEvent.INTERNAL_ERROR, {
1901
2322
  text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
1902
2323
  buttons: [
@@ -1911,17 +2332,6 @@ class DAppClient extends octez_connect_core_1.Client {
1911
2332
  });
1912
2333
  throw sendError;
1913
2334
  }
1914
- const index = requestInput.type;
1915
- this.events
1916
- .emit(beacon_message_events_1.messageEvents[index].sent, {
1917
- walletInfo: Object.assign(Object.assign({}, walletInfo), { name: (_c = walletInfo.name) !== null && _c !== void 0 ? _c : 'Wallet' }),
1918
- extraInfo: {
1919
- resetCallback: () => __awaiter(this, void 0, void 0, function* () {
1920
- this.disconnect();
1921
- })
1922
- }
1923
- })
1924
- .catch((emitError) => console.warn(emitError));
1925
2335
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1926
2336
  return exposed.promise; // TODO: fix type
1927
2337
  });
@@ -1964,27 +2374,149 @@ class DAppClient extends octez_connect_core_1.Client {
1964
2374
  return exposed.promise;
1965
2375
  });
1966
2376
  }
2377
+ /**
2378
+ * Disconnects all resolved transports (postMessage, P2P, WalletConnect), removes their peers,
2379
+ * and clears active account state. After calling, the DAppClient remains usable for a new
2380
+ * requestPermissions().
2381
+ */
1967
2382
  disconnect() {
2383
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
2384
+ const { notifyPeers = true } = options;
2385
+ if (!this._disconnectPromise) {
2386
+ this._disconnectNotifyPeers = false;
2387
+ }
2388
+ this._disconnectNotifyPeers = this._disconnectNotifyPeers || notifyPeers;
2389
+ if (this._disconnectPromise) {
2390
+ return this._disconnectPromise;
2391
+ }
2392
+ this._disconnectPromise = (() => __awaiter(this, void 0, void 0, function* () { return this.disconnectInternal(); }))().finally(() => {
2393
+ this._disconnectPromise = undefined;
2394
+ this._disconnectNotifyPeers = false;
2395
+ });
2396
+ return this._disconnectPromise;
2397
+ });
2398
+ }
2399
+ disconnectInternal() {
1968
2400
  return __awaiter(this, void 0, void 0, function* () {
2401
+ this.abortPendingInit();
1969
2402
  if (!this._transport.isResolved()) {
1970
- throw new Error('No transport available.');
2403
+ const initializedTransports = this.getInitializedTransports();
2404
+ this.abortOpenRequests();
2405
+ yield this.removeAllPeersFromTransports(initializedTransports);
2406
+ yield this.clearActiveAccount();
2407
+ yield this.disconnectResolvedTransports(initializedTransports);
2408
+ this.clearInternalTransportReferences();
2409
+ return;
1971
2410
  }
1972
- const transport = yield this.transport;
2411
+ const transport = (yield this.transport);
1973
2412
  if (transport.connectionStatus === octez_connect_types_1.TransportStatus.NOT_CONNECTED) {
1974
- throw new Error('Not connected.');
2413
+ yield this.clearActiveAccount();
2414
+ this.abortOpenRequests();
2415
+ return;
1975
2416
  }
1976
2417
  yield this.createStateSnapshot();
1977
2418
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('disconnect', 'start'));
2419
+ this.abortOpenRequests();
2420
+ const transports = this.getResolvedTransports(transport);
2421
+ yield this.removeAllPeersFromTransports(transports);
1978
2422
  yield this.clearActiveAccount();
1979
- if (!(transport instanceof octez_connect_transport_walletconnect_1.WalletConnectTransport)) {
1980
- yield transport.disconnect();
1981
- }
1982
- this.postMessageTransport = undefined;
1983
- this.p2pTransport = undefined;
1984
- this.walletConnectTransport = undefined;
2423
+ yield this.disconnectResolvedTransports(transports);
2424
+ this.clearInternalTransportReferences();
1985
2425
  this.sendMetrics('performance-metrics/save', yield this.buildPayload('disconnect', 'success'));
1986
2426
  });
1987
2427
  }
2428
+ getInitializedTransports() {
2429
+ const transports = [];
2430
+ const addTransport = (transport) => {
2431
+ if (transport && !transports.includes(transport)) {
2432
+ transports.push(transport);
2433
+ }
2434
+ };
2435
+ addTransport(this.postMessageTransport);
2436
+ addTransport(this.p2pTransport);
2437
+ addTransport(this.walletConnectTransport);
2438
+ return transports;
2439
+ }
2440
+ getResolvedTransports(selectedTransport) {
2441
+ const transports = this.getInitializedTransports();
2442
+ const addTransport = (transport) => {
2443
+ if (transport && !transports.includes(transport)) {
2444
+ transports.push(transport);
2445
+ }
2446
+ };
2447
+ addTransport(this.postMessageTransport);
2448
+ addTransport(this.p2pTransport);
2449
+ addTransport(this.walletConnectTransport);
2450
+ addTransport(selectedTransport);
2451
+ return transports;
2452
+ }
2453
+ clearInternalTransportReferences() {
2454
+ this.postMessageTransport = undefined;
2455
+ this.p2pTransport = undefined;
2456
+ this.walletConnectTransport = undefined;
2457
+ }
2458
+ removeAllPeersFromTransports(transports) {
2459
+ return __awaiter(this, void 0, void 0, function* () {
2460
+ const peersByTransport = yield Promise.all(transports.map((transport) => __awaiter(this, void 0, void 0, function* () {
2461
+ const transportPeers = yield transport.getPeers();
2462
+ return {
2463
+ transport,
2464
+ peers: transportPeers
2465
+ };
2466
+ })));
2467
+ yield Promise.all(peersByTransport.map(({ transport }) => transport.removeAllPeers()));
2468
+ const peersToRemove = peersByTransport.flatMap(({ peers: transportPeers }) => transportPeers);
2469
+ yield this.removeAccountsForPeers(peersToRemove);
2470
+ // If cleanup already removed peers, a late notify upgrade has nothing left to notify.
2471
+ const sendDisconnectToPeers = this._disconnectNotifyPeers;
2472
+ if (!sendDisconnectToPeers) {
2473
+ return;
2474
+ }
2475
+ yield Promise.allSettled(this.getUniquePeerNotifications(peersByTransport).map(({ transport, peer }) => this.sendDisconnectToPeer(peer, transport)));
2476
+ });
2477
+ }
2478
+ getUniquePeerNotifications(peersByTransport) {
2479
+ const notifications = [];
2480
+ const seen = new Set();
2481
+ peersByTransport.forEach(({ transport, peers: transportPeers }) => {
2482
+ transportPeers.forEach((peer) => {
2483
+ const key = this.getPeerNotificationKey(transport, peer);
2484
+ if (seen.has(key)) {
2485
+ return;
2486
+ }
2487
+ seen.add(key);
2488
+ notifications.push({ transport, peer });
2489
+ });
2490
+ });
2491
+ return notifications;
2492
+ }
2493
+ getPeerNotificationKey(transport, peer) {
2494
+ return [transport.type, peer.publicKey, peer.senderId, peer.id].filter(Boolean).join(':');
2495
+ }
2496
+ disconnectResolvedTransports(transports) {
2497
+ return __awaiter(this, void 0, void 0, function* () {
2498
+ const results = yield Promise.allSettled(transports.map((transport) => __awaiter(this, void 0, void 0, function* () {
2499
+ if (this.isTransportConnected(transport)) {
2500
+ yield transport.disconnect();
2501
+ }
2502
+ if (this.hasClientCleanup(transport)) {
2503
+ yield transport.doClientCleanup();
2504
+ }
2505
+ })));
2506
+ results.forEach((result) => {
2507
+ if (result.status === 'rejected') {
2508
+ logger.warn('disconnectResolvedTransports', result.reason);
2509
+ }
2510
+ });
2511
+ });
2512
+ }
2513
+ hasClientCleanup(transport) {
2514
+ const transportWithClientCleanup = transport;
2515
+ return typeof transportWithClientCleanup.doClientCleanup === 'function';
2516
+ }
2517
+ isTransportConnected(transport) {
2518
+ return transport.connectionStatus !== octez_connect_types_1.TransportStatus.NOT_CONNECTED;
2519
+ }
1988
2520
  /**
1989
2521
  * Adds a requests to the "openRequests" set so we know what messages have already been answered/handled.
1990
2522
  *
@@ -1993,7 +2525,64 @@ class DAppClient extends octez_connect_core_1.Client {
1993
2525
  */
1994
2526
  addOpenRequest(id, promise) {
1995
2527
  logger.log('addOpenRequest', this.name, `adding request ${id} and waiting for answer`);
2528
+ this.clearOpenRequestTimeout(id);
2529
+ this.openRequests.delete(id);
1996
2530
  this.openRequests.set(id, promise);
2531
+ this.addOpenRequestTimeout(id, promise);
2532
+ }
2533
+ addOpenRequestTimeout(id, promise) {
2534
+ if (id === SESSION_UPDATE_REQUEST_ID) {
2535
+ return;
2536
+ }
2537
+ if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
2538
+ return;
2539
+ }
2540
+ const timeout = setTimeout(() => {
2541
+ if (this.openRequests.get(id) !== promise) {
2542
+ return;
2543
+ }
2544
+ const errorResponse = {
2545
+ id,
2546
+ type: octez_connect_types_1.BeaconMessageType.Error,
2547
+ errorType: octez_connect_types_1.BeaconErrorType.PEER_UNREACHABLE,
2548
+ senderId: '',
2549
+ version: '2'
2550
+ };
2551
+ promise.reject(errorResponse);
2552
+ this.clearOpenRequest(id);
2553
+ this.events.emit(events_1.BeaconEvent.CHANNEL_CLOSED).catch((emitError) => {
2554
+ logger.error('addOpenRequestTimeout', emitError);
2555
+ });
2556
+ }, this.requestTimeoutMs);
2557
+ this.openRequestTimeouts.set(id, timeout);
2558
+ }
2559
+ clearOpenRequest(id) {
2560
+ this.clearOpenRequestTimeout(id);
2561
+ this.openRequests.delete(id);
2562
+ this.openRequestsOtherTabs.delete(id);
2563
+ }
2564
+ clearOpenRequestTimeout(id) {
2565
+ const timeout = this.openRequestTimeouts.get(id);
2566
+ if (timeout) {
2567
+ clearTimeout(timeout);
2568
+ this.openRequestTimeouts.delete(id);
2569
+ }
2570
+ }
2571
+ clearAllOpenRequestTimeouts() {
2572
+ this.openRequestTimeouts.forEach((timeout) => clearTimeout(timeout));
2573
+ this.openRequestTimeouts.clear();
2574
+ }
2575
+ abortOpenRequests() {
2576
+ Array.from(this.openRequests.entries())
2577
+ .filter(([id]) => id !== SESSION_UPDATE_REQUEST_ID)
2578
+ .forEach(([id, promise]) => {
2579
+ promise.reject(this.createAbortedError(id));
2580
+ this.clearOpenRequest(id);
2581
+ });
2582
+ this.openRequests.clear();
2583
+ this.openRequestsOtherTabs.clear();
2584
+ this.acknowledgedRequests.clear();
2585
+ this.clearAllOpenRequestTimeouts();
1997
2586
  }
1998
2587
  sendNotificationWithAccessToken(notification) {
1999
2588
  return __awaiter(this, void 0, void 0, function* () {