@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.
- package/README.md +41 -0
- package/dist/cjs/dapp-client/DAppClient.d.ts +113 -4
- package/dist/cjs/dapp-client/DAppClient.js +962 -373
- package/dist/cjs/dapp-client/DAppClient.js.map +1 -1
- package/dist/cjs/dapp-client/DAppClientOptions.d.ts +17 -0
- package/dist/cjs/dapp-client/DAppClientOptions.js.map +1 -1
- package/dist/cjs/events.d.ts +27 -2
- package/dist/cjs/events.js +9 -5
- package/dist/cjs/events.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/utils/get-instance.js +3 -0
- package/dist/cjs/utils/get-instance.js.map +1 -1
- package/dist/esm/dapp-client/DAppClient.d.ts +113 -4
- package/dist/esm/dapp-client/DAppClient.js +968 -368
- package/dist/esm/dapp-client/DAppClient.js.map +1 -1
- package/dist/esm/dapp-client/DAppClientOptions.d.ts +17 -0
- package/dist/esm/dapp-client/DAppClientOptions.js.map +1 -1
- package/dist/esm/events.d.ts +27 -2
- package/dist/esm/events.js +6 -3
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/get-instance.js +3 -0
- package/dist/esm/utils/get-instance.js.map +1 -1
- package/dist/octez.connect.dapp.min.js +1 -1
- package/package.json +9 -9
|
@@ -6,7 +6,7 @@ import { TransportType, StorageKey, BeaconMessageType, PermissionScope, NetworkT
|
|
|
6
6
|
// EncryptPayloadResponse,
|
|
7
7
|
// EncryptPayloadRequest
|
|
8
8
|
} from '@tezos-x/octez.connect-types';
|
|
9
|
-
import { Client, AppMetadataManager, Serializer, LocalStorage, getAccountIdentifier, getSenderId, Logger, StorageValidator, SDK_VERSION, IndexedDBStorage, MultiTabChannel,
|
|
9
|
+
import { Client, AppMetadataManager, Serializer, LocalStorage, getAccountIdentifier, getSenderId, Logger, StorageValidator, SDK_VERSION, IndexedDBStorage, MultiTabChannel, getError } from '@tezos-x/octez.connect-core';
|
|
10
10
|
import { getAddressFromPublicKey, ExposedPromise, generateGUID, toHex, signMessage, CONTRACT_PREFIX, prefixPublicKey, isValidAddress, getKeypairFromSeed } from '@tezos-x/octez.connect-utils';
|
|
11
11
|
import { messageEvents } from '../beacon-message-events';
|
|
12
12
|
import { BeaconEvent, BeaconEventHandler } from '../events';
|
|
@@ -18,6 +18,23 @@ import { PostMessageTransport } from '@tezos-x/octez.connect-transport-postmessa
|
|
|
18
18
|
import { closeToast, getColorMode, setColorMode, setDesktopList, setExtensionList, setWebList, setiOSList, getiOSList, getDesktopList, getExtensionList, getWebList, isBrowser, isDesktop, isMobileOS, isIOS, currentOS } from '@tezos-x/octez.connect-ui';
|
|
19
19
|
import { WalletConnectTransport } from '@tezos-x/octez.connect-transport-walletconnect';
|
|
20
20
|
const logger = new Logger('DAppClient');
|
|
21
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
22
|
+
const SESSION_UPDATE_REQUEST_ID = 'session_update';
|
|
23
|
+
const createLazyPromise = (factory) => {
|
|
24
|
+
let promise;
|
|
25
|
+
const getPromise = () => {
|
|
26
|
+
if (!promise) {
|
|
27
|
+
promise = factory();
|
|
28
|
+
}
|
|
29
|
+
return promise;
|
|
30
|
+
};
|
|
31
|
+
const lazyPromise = {
|
|
32
|
+
then: (onfulfilled, onrejected) => getPromise().then(onfulfilled, onrejected),
|
|
33
|
+
catch: (onrejected) => getPromise().catch(onrejected),
|
|
34
|
+
finally: (onfinally) => getPromise().finally(onfinally ?? undefined)
|
|
35
|
+
};
|
|
36
|
+
return lazyPromise;
|
|
37
|
+
};
|
|
21
38
|
/**
|
|
22
39
|
* @publicapi
|
|
23
40
|
*
|
|
@@ -51,12 +68,20 @@ export class DAppClient extends Client {
|
|
|
51
68
|
walletConnectTransport;
|
|
52
69
|
wcProjectId;
|
|
53
70
|
wcRelayUrl;
|
|
71
|
+
/**
|
|
72
|
+
* When true, the WalletConnect transport is not built or listened to. See
|
|
73
|
+
* DAppClientOptions.disableWalletConnect.
|
|
74
|
+
*/
|
|
75
|
+
disableWalletConnect;
|
|
54
76
|
isGetActiveAccountHandled = false;
|
|
55
77
|
openRequestsOtherTabs = new Set();
|
|
56
78
|
/**
|
|
57
79
|
* A map of requests that are currently "open", meaning we have sent them to a wallet and are still awaiting a response.
|
|
58
80
|
*/
|
|
59
81
|
openRequests = new Map();
|
|
82
|
+
acknowledgedRequests = new Set();
|
|
83
|
+
openRequestTimeouts = new Map();
|
|
84
|
+
requestTimeoutMs;
|
|
60
85
|
/**
|
|
61
86
|
* The currently active account. For all requests that are associated to a specific request (operation request, signing request),
|
|
62
87
|
* the active account is used to determine the network and destination wallet
|
|
@@ -67,8 +92,22 @@ export class DAppClient extends Client {
|
|
|
67
92
|
*/
|
|
68
93
|
_activePeer = new ExposedPromise();
|
|
69
94
|
_initPromise;
|
|
70
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Rejector for the in-flight {@link _initPromise}. Populated while init is
|
|
97
|
+
* awaiting a peer pairing; cleared automatically when {@link _initPromise}
|
|
98
|
+
* settles. Abort paths (modal close, WC session-proposal rejection,
|
|
99
|
+
* transport-level connection failure) call this to unwedge any
|
|
100
|
+
* `await this.init()` caller, surfacing the error to handleRequestError
|
|
101
|
+
* instead of hanging.
|
|
102
|
+
*/
|
|
103
|
+
_initReject;
|
|
104
|
+
_initSubstratePairing;
|
|
105
|
+
_requestPermissionsPromise;
|
|
106
|
+
_requestPermissionsKey;
|
|
107
|
+
_sdkSecretSeedPromise;
|
|
108
|
+
destroyed = false;
|
|
71
109
|
activeAccountLoaded;
|
|
110
|
+
storageValidated;
|
|
72
111
|
appMetadataManager;
|
|
73
112
|
disclaimerText;
|
|
74
113
|
errorMessages;
|
|
@@ -76,6 +115,11 @@ export class DAppClient extends Client {
|
|
|
76
115
|
storageValidator;
|
|
77
116
|
beaconIDB = new IndexedDBStorage('beacon', ['bug_report', 'metrics']);
|
|
78
117
|
debounceSetActiveAccount = false;
|
|
118
|
+
// Constructor-time invalid storage recovery can race through multiple async paths.
|
|
119
|
+
// Keep this guard scoped to those startup paths.
|
|
120
|
+
hasEmittedInvalidAccountDeactivated = false;
|
|
121
|
+
_disconnectPromise;
|
|
122
|
+
_disconnectNotifyPeers = false;
|
|
79
123
|
multiTabChannel = new MultiTabChannel('octez.connect-sdk-channel', this.onBCMessageHandler.bind(this), this.onElectedLeaderhandler.bind(this));
|
|
80
124
|
constructor(config) {
|
|
81
125
|
super({
|
|
@@ -83,8 +127,11 @@ export class DAppClient extends Client {
|
|
|
83
127
|
...config
|
|
84
128
|
});
|
|
85
129
|
this.description = config.description;
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
130
|
+
this.disableWalletConnect = config.disableWalletConnect ?? false;
|
|
131
|
+
if (!this.disableWalletConnect) {
|
|
132
|
+
this.wcProjectId = config.walletConnectOptions?.projectId || '24469fd0a06df227b6e5f7dc7de0ff4f';
|
|
133
|
+
this.wcRelayUrl = config.walletConnectOptions?.relayUrl;
|
|
134
|
+
}
|
|
88
135
|
this.featuredWallets = config.featuredWallets;
|
|
89
136
|
this.events = new BeaconEventHandler(config.eventHandlers, config.disableDefaultEvents ?? false);
|
|
90
137
|
this.blockExplorer = config.blockExplorer ?? new TzktBlockExplorer();
|
|
@@ -97,24 +144,32 @@ export class DAppClient extends Client {
|
|
|
97
144
|
this.enableAppSwitching =
|
|
98
145
|
config.enableAppSwitching === undefined ? true : !!config.enableAppSwitching;
|
|
99
146
|
this.enableMetrics = config.enableMetrics ? true : false;
|
|
147
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
100
148
|
// Subscribe to storage changes and update the active account if it changes on other tabs
|
|
101
149
|
this.storage.subscribeToStorageChanged(async (event) => {
|
|
150
|
+
if (event.oldValue === event.newValue) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
102
153
|
if (event.eventType === 'storageCleared') {
|
|
103
|
-
this.setActiveAccount(undefined);
|
|
154
|
+
await this.setActiveAccount(undefined);
|
|
104
155
|
return;
|
|
105
156
|
}
|
|
106
157
|
if (event.eventType === 'entryModified') {
|
|
107
158
|
if (event.key === this.storage.getPrefixedKey(StorageKey.ACTIVE_ACCOUNT)) {
|
|
108
159
|
const accountIdentifier = event.newValue;
|
|
109
160
|
if (!accountIdentifier || accountIdentifier === 'undefined') {
|
|
110
|
-
this.setActiveAccount(undefined);
|
|
161
|
+
await this.setActiveAccount(undefined);
|
|
111
162
|
}
|
|
112
163
|
else {
|
|
113
164
|
const account = await this.getAccount(accountIdentifier);
|
|
114
|
-
this.setActiveAccount(account);
|
|
165
|
+
await this.setActiveAccount(account);
|
|
115
166
|
}
|
|
116
167
|
return;
|
|
117
168
|
}
|
|
169
|
+
if (event.key === this.storage.getPrefixedKey(StorageKey.ACCOUNTS)) {
|
|
170
|
+
await this.recoverActiveAccountFromAccountsChange();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
118
173
|
if (event.key === this.storage.getPrefixedKey(StorageKey.ENABLE_METRICS)) {
|
|
119
174
|
this.enableMetrics = !!(await this.storage.get(StorageKey.ENABLE_METRICS));
|
|
120
175
|
return;
|
|
@@ -130,30 +185,42 @@ export class DAppClient extends Client {
|
|
|
130
185
|
this.activeAccountLoaded = this.storage
|
|
131
186
|
.get(StorageKey.ACTIVE_ACCOUNT)
|
|
132
187
|
.then(async (activeAccountIdentifier) => {
|
|
188
|
+
const accounts = await this.accountManager.getAccounts();
|
|
189
|
+
if (activeAccountIdentifier === 'undefined') {
|
|
190
|
+
// Non-normalizing storage backends can still surface legacy sentinel strings directly.
|
|
191
|
+
await this.deactivateInvalidAccountState('missing_active_account');
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
133
194
|
if (activeAccountIdentifier) {
|
|
134
|
-
const account =
|
|
195
|
+
const account = accounts.find((storedAccount) => storedAccount.accountIdentifier === activeAccountIdentifier);
|
|
196
|
+
if (!account) {
|
|
197
|
+
await this.deactivateInvalidAccountState('missing_active_account');
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
135
200
|
await this.setActiveAccount(account);
|
|
136
201
|
return account;
|
|
137
202
|
}
|
|
138
|
-
|
|
139
|
-
await this.
|
|
203
|
+
if (accounts.length > 0 && this.hasStoredActiveAccountPointer()) {
|
|
204
|
+
await this.deactivateInvalidAccountState('missing_active_account');
|
|
140
205
|
return undefined;
|
|
141
206
|
}
|
|
207
|
+
await this.setActiveAccount(undefined);
|
|
208
|
+
return undefined;
|
|
142
209
|
})
|
|
143
210
|
.catch(async (storageError) => {
|
|
144
211
|
logger.error(storageError);
|
|
145
|
-
await this.
|
|
146
|
-
this.events.emit(BeaconEvent.INVALID_ACCOUNT_DEACTIVATED);
|
|
212
|
+
await this.deactivateInvalidAccountState('invalid_active_account_storage');
|
|
147
213
|
return undefined;
|
|
148
214
|
});
|
|
149
215
|
this.handleResponse = async (message, connectionInfo) => {
|
|
150
|
-
const
|
|
216
|
+
const isV3WrappedMessage = message.version === '3' && 'message' in message;
|
|
217
|
+
const typedMessage = isV3WrappedMessage
|
|
151
218
|
? message.message
|
|
152
219
|
: message;
|
|
153
|
-
let appMetadata =
|
|
220
|
+
let appMetadata = isV3WrappedMessage
|
|
154
221
|
? typedMessage.blockchainData?.appMetadata
|
|
155
222
|
: typedMessage.appMetadata;
|
|
156
|
-
if (!appMetadata &&
|
|
223
|
+
if (!appMetadata && isV3WrappedMessage) {
|
|
157
224
|
const storedMetadata = await Promise.all([
|
|
158
225
|
this.storage.get(StorageKey.TRANSPORT_P2P_PEERS_DAPP),
|
|
159
226
|
this.storage.get(StorageKey.TRANSPORT_WALLETCONNECT_PEERS_DAPP),
|
|
@@ -182,8 +249,11 @@ export class DAppClient extends Client {
|
|
|
182
249
|
},
|
|
183
250
|
id: message.id
|
|
184
251
|
});
|
|
185
|
-
if (typedMessage.type
|
|
186
|
-
this.
|
|
252
|
+
if (typedMessage.type === BeaconMessageType.Acknowledge) {
|
|
253
|
+
this.clearOpenRequestTimeout(message.id);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.clearOpenRequest(message.id);
|
|
187
257
|
}
|
|
188
258
|
return;
|
|
189
259
|
}
|
|
@@ -210,15 +280,19 @@ export class DAppClient extends Client {
|
|
|
210
280
|
await this.events.emit(BeaconEvent.CHANNEL_CLOSED);
|
|
211
281
|
};
|
|
212
282
|
if (openRequest && typedMessage.type === BeaconMessageType.Acknowledge) {
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.
|
|
217
|
-
message
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
283
|
+
if (!this.acknowledgedRequests.has(message.id)) {
|
|
284
|
+
this.acknowledgedRequests.add(message.id);
|
|
285
|
+
this.clearOpenRequestTimeout(message.id);
|
|
286
|
+
this.analytics.track('event', 'DAppClient', 'Acknowledge received from Wallet');
|
|
287
|
+
logger.log('handleResponse', `acknowledge message received for ${message.id}`);
|
|
288
|
+
this.events
|
|
289
|
+
.emit(BeaconEvent.ACKNOWLEDGE_RECEIVED, {
|
|
290
|
+
message: typedMessage,
|
|
291
|
+
extraInfo: {},
|
|
292
|
+
walletInfo: await this.getWalletInfo()
|
|
293
|
+
})
|
|
294
|
+
.catch((emitError) => logger.error('handleResponse', emitError));
|
|
295
|
+
}
|
|
222
296
|
}
|
|
223
297
|
else if (openRequest) {
|
|
224
298
|
if (typedMessage.type === BeaconMessageType.PermissionResponse && appMetadata) {
|
|
@@ -230,7 +304,8 @@ export class DAppClient extends Client {
|
|
|
230
304
|
else {
|
|
231
305
|
openRequest.resolve({ message, connectionInfo });
|
|
232
306
|
}
|
|
233
|
-
this.
|
|
307
|
+
this.clearOpenRequest(message.id);
|
|
308
|
+
this.acknowledgedRequests.delete(message.id);
|
|
234
309
|
}
|
|
235
310
|
else {
|
|
236
311
|
if (typedMessage.type === BeaconMessageType.Disconnect) {
|
|
@@ -239,16 +314,23 @@ export class DAppClient extends Client {
|
|
|
239
314
|
else if (typedMessage.type === BeaconMessageType.ChangeAccountRequest) {
|
|
240
315
|
await this.onNewAccount(typedMessage, connectionInfo);
|
|
241
316
|
}
|
|
317
|
+
else {
|
|
318
|
+
logger.warn('handleResponse', 'received response for unknown or closed request', {
|
|
319
|
+
id: message.id,
|
|
320
|
+
type: typedMessage.type,
|
|
321
|
+
connectionInfo
|
|
322
|
+
});
|
|
323
|
+
}
|
|
242
324
|
}
|
|
243
325
|
if (this._transport.isResolved()) {
|
|
244
326
|
const transport = await this.transport;
|
|
245
327
|
if (transport instanceof WalletConnectTransport &&
|
|
246
|
-
!this.openRequests.has(
|
|
247
|
-
this.openRequests.set(
|
|
328
|
+
!this.openRequests.has(SESSION_UPDATE_REQUEST_ID)) {
|
|
329
|
+
this.openRequests.set(SESSION_UPDATE_REQUEST_ID, new ExposedPromise());
|
|
248
330
|
}
|
|
249
331
|
}
|
|
250
332
|
};
|
|
251
|
-
this.storageValidator
|
|
333
|
+
this.storageValidated = this.storageValidator
|
|
252
334
|
.validate()
|
|
253
335
|
.then(async (isValid) => {
|
|
254
336
|
const account = await this.activeAccountLoaded;
|
|
@@ -264,15 +346,22 @@ export class DAppClient extends Client {
|
|
|
264
346
|
url: info.deeplink
|
|
265
347
|
});
|
|
266
348
|
const nowValid = await this.storageValidator.validate();
|
|
267
|
-
if (!nowValid) {
|
|
268
|
-
this.
|
|
349
|
+
if (!nowValid && account) {
|
|
350
|
+
await this.deactivateInvalidAccountState('storage_validation_failed');
|
|
351
|
+
}
|
|
352
|
+
else if (!nowValid) {
|
|
353
|
+
await this.resetInvalidState(false);
|
|
269
354
|
}
|
|
270
355
|
}
|
|
271
356
|
if (account && account.origin.type !== 'p2p') {
|
|
272
|
-
|
|
357
|
+
// Fire-and-forget eager init for stored sessions. init() can now
|
|
358
|
+
// reject (e.g. when an abort path settles _initPromise), so swallow
|
|
359
|
+
// here; downstream callers of init() handle rejection on their own.
|
|
360
|
+
this.init().catch((initError) => logger.error('eager init failed', initError?.message ?? initError));
|
|
273
361
|
}
|
|
274
362
|
})
|
|
275
363
|
.catch((err) => logger.error(err.message));
|
|
364
|
+
this.retainStorageValidationPromise();
|
|
276
365
|
this.sendMetrics('enable-metrics?' + this.addQueryParam('version', SDK_VERSION), undefined, (res) => {
|
|
277
366
|
if (!res.ok) {
|
|
278
367
|
res.status === 426
|
|
@@ -343,9 +432,43 @@ export class DAppClient extends Client {
|
|
|
343
432
|
const transport = (await this._transport.promise);
|
|
344
433
|
await transport.waitForResolution();
|
|
345
434
|
this.openRequestsOtherTabs.add(message.id);
|
|
346
|
-
isV3
|
|
435
|
+
const requestPromise = isV3
|
|
347
436
|
? this.makeRequestV3(message.data, message.id)
|
|
348
437
|
: this.makeRequest(message.data, false, message.id);
|
|
438
|
+
requestPromise.catch((requestError) => {
|
|
439
|
+
const errorResponse = this.buildDelegatedRequestError(message.id, requestError);
|
|
440
|
+
this.multiTabChannel.postMessage({
|
|
441
|
+
type: 'RESPONSE',
|
|
442
|
+
data: {
|
|
443
|
+
message: errorResponse,
|
|
444
|
+
connectionInfo: {
|
|
445
|
+
origin: Origin.WALLETCONNECT,
|
|
446
|
+
id: ''
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
id: message.id
|
|
450
|
+
});
|
|
451
|
+
this.clearOpenRequest(message.id);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
buildDelegatedRequestError(id, error) {
|
|
455
|
+
if (typeof error === 'object' &&
|
|
456
|
+
error !== null &&
|
|
457
|
+
'type' in error &&
|
|
458
|
+
'errorType' in error &&
|
|
459
|
+
error.type === BeaconMessageType.Error) {
|
|
460
|
+
return {
|
|
461
|
+
...error,
|
|
462
|
+
id
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
id,
|
|
467
|
+
type: BeaconMessageType.Error,
|
|
468
|
+
errorType: BeaconErrorType.UNKNOWN_ERROR,
|
|
469
|
+
senderId: '',
|
|
470
|
+
version: '2'
|
|
471
|
+
};
|
|
349
472
|
}
|
|
350
473
|
async createStateSnapshot() {
|
|
351
474
|
if (!localStorage || !this.enableMetrics) {
|
|
@@ -371,10 +494,7 @@ export class DAppClient extends Client {
|
|
|
371
494
|
this.storage.set(StorageKey.USER_ID, this.userId);
|
|
372
495
|
}
|
|
373
496
|
async initInternalTransports() {
|
|
374
|
-
const seed = await this.
|
|
375
|
-
if (!seed) {
|
|
376
|
-
throw new Error('Secret seed not found');
|
|
377
|
-
}
|
|
497
|
+
const seed = await this.getOrCreateSDKSecretSeed();
|
|
378
498
|
const keyPair = await getKeypairFromSeed(seed);
|
|
379
499
|
if (this.postMessageTransport || this.p2pTransport || this.walletConnectTransport) {
|
|
380
500
|
return;
|
|
@@ -383,6 +503,11 @@ export class DAppClient extends Client {
|
|
|
383
503
|
await this.addListener(this.postMessageTransport);
|
|
384
504
|
this.p2pTransport = new DappP2PTransport(this.name, keyPair, this.storage, this.matrixNodes, this.iconUrl, this.appUrl);
|
|
385
505
|
await this.addListener(this.p2pTransport);
|
|
506
|
+
// Skip WalletConnect entirely when disabled (e.g. inside a Firefox MV3 content-script
|
|
507
|
+
// compartment where the WC provider cannot run). postMessage + P2P are unaffected.
|
|
508
|
+
if (this.disableWalletConnect) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
386
511
|
const wcOptions = {
|
|
387
512
|
projectId: this.wcProjectId,
|
|
388
513
|
relayUrl: this.wcRelayUrl,
|
|
@@ -400,6 +525,37 @@ export class DAppClient extends Client {
|
|
|
400
525
|
this.initEvents();
|
|
401
526
|
await this.addListener(this.walletConnectTransport);
|
|
402
527
|
}
|
|
528
|
+
async getOrCreateSDKSecretSeed() {
|
|
529
|
+
if (this._sdkSecretSeedPromise) {
|
|
530
|
+
return this._sdkSecretSeedPromise;
|
|
531
|
+
}
|
|
532
|
+
this._sdkSecretSeedPromise = this.loadOrCreateSDKSecretSeed()
|
|
533
|
+
.finally(() => {
|
|
534
|
+
this._sdkSecretSeedPromise = undefined;
|
|
535
|
+
})
|
|
536
|
+
.catch((error) => {
|
|
537
|
+
throw error;
|
|
538
|
+
});
|
|
539
|
+
return this._sdkSecretSeedPromise;
|
|
540
|
+
}
|
|
541
|
+
async loadOrCreateSDKSecretSeed() {
|
|
542
|
+
const existingSeed = await this.storage.get(StorageKey.BEACON_SDK_SECRET_SEED);
|
|
543
|
+
if (this.isValidSDKSecretSeed(existingSeed)) {
|
|
544
|
+
return existingSeed;
|
|
545
|
+
}
|
|
546
|
+
await this.storage.delete(StorageKey.BEACON_SDK_SECRET_SEED);
|
|
547
|
+
this._keyPair = new ExposedPromise();
|
|
548
|
+
this._beaconId = new ExposedPromise();
|
|
549
|
+
await this.initSDK();
|
|
550
|
+
const restoredSeed = await this.storage.get(StorageKey.BEACON_SDK_SECRET_SEED);
|
|
551
|
+
if (this.isValidSDKSecretSeed(restoredSeed)) {
|
|
552
|
+
return restoredSeed;
|
|
553
|
+
}
|
|
554
|
+
throw new Error('Secret seed not found');
|
|
555
|
+
}
|
|
556
|
+
isValidSDKSecretSeed(seed) {
|
|
557
|
+
return typeof seed === 'string' && seed !== '' && seed !== 'undefined' && seed !== 'null';
|
|
558
|
+
}
|
|
403
559
|
initEvents() {
|
|
404
560
|
if (!this.walletConnectTransport) {
|
|
405
561
|
return;
|
|
@@ -431,20 +587,26 @@ export class DAppClient extends Client {
|
|
|
431
587
|
});
|
|
432
588
|
}
|
|
433
589
|
else {
|
|
434
|
-
this
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
590
|
+
// The WC transport reports session-proposal rejection through this event
|
|
591
|
+
// when there are no active listeners yet (no peer paired). Reject the
|
|
592
|
+
// in-flight init promise so `await dapp.requestPermissions()` unwinds
|
|
593
|
+
// through requestPermissions' catch -> handleRequestError, which both
|
|
594
|
+
// emits PERMISSION_REQUEST_ERROR and resets transport state. Without
|
|
595
|
+
// this, the dapp wedges with a pending `_initPromise`.
|
|
596
|
+
this._initReject?.(this.createAbortedError());
|
|
438
597
|
}
|
|
439
598
|
}
|
|
440
599
|
async channelClosedHandler(type) {
|
|
600
|
+
if (!this._transport.isResolved()) {
|
|
601
|
+
await this.events.emit(BeaconEvent.CHANNEL_CLOSED);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
441
604
|
const transport = await this.transport;
|
|
442
605
|
if (transport.type !== type) {
|
|
443
606
|
return;
|
|
444
607
|
}
|
|
445
608
|
await this.events.emit(BeaconEvent.CHANNEL_CLOSED);
|
|
446
|
-
this.
|
|
447
|
-
await this.disconnect();
|
|
609
|
+
await this.disconnect({ notifyPeers: false });
|
|
448
610
|
}
|
|
449
611
|
/**
|
|
450
612
|
* Destroy the instance.
|
|
@@ -454,11 +616,73 @@ export class DAppClient extends Client {
|
|
|
454
616
|
* If you wish to disconnect your dApp, use `disconnect` instead.
|
|
455
617
|
*/
|
|
456
618
|
async destroy() {
|
|
619
|
+
this.destroyed = true;
|
|
457
620
|
await this.createStateSnapshot();
|
|
621
|
+
await this.destroyInternalTransports();
|
|
458
622
|
await super.destroy();
|
|
459
623
|
}
|
|
624
|
+
async destroyInternalTransports() {
|
|
625
|
+
this.abortPendingInit();
|
|
626
|
+
const transports = [];
|
|
627
|
+
if (this.postMessageTransport) {
|
|
628
|
+
transports.push(this.postMessageTransport);
|
|
629
|
+
}
|
|
630
|
+
if (this.p2pTransport) {
|
|
631
|
+
transports.push(this.p2pTransport);
|
|
632
|
+
}
|
|
633
|
+
if (this.walletConnectTransport) {
|
|
634
|
+
transports.push(this.walletConnectTransport);
|
|
635
|
+
}
|
|
636
|
+
if (transports.length > 0) {
|
|
637
|
+
await this.cleanup(transports);
|
|
638
|
+
await Promise.all(transports.map(async (transport) => {
|
|
639
|
+
await transport.disconnect();
|
|
640
|
+
if ('doClientCleanup' in transport) {
|
|
641
|
+
await transport.doClientCleanup();
|
|
642
|
+
}
|
|
643
|
+
}));
|
|
644
|
+
this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined;
|
|
645
|
+
await this.setTransport(undefined);
|
|
646
|
+
}
|
|
647
|
+
await this.multiTabChannel.close();
|
|
648
|
+
}
|
|
649
|
+
abortPendingInit() {
|
|
650
|
+
this._initReject?.(this.createAbortedError());
|
|
651
|
+
this._initPromise = undefined;
|
|
652
|
+
this._initReject = undefined;
|
|
653
|
+
this._initSubstratePairing = undefined;
|
|
654
|
+
this._requestPermissionsPromise = undefined;
|
|
655
|
+
this._requestPermissionsKey = undefined;
|
|
656
|
+
}
|
|
657
|
+
createAbortedError(id = '') {
|
|
658
|
+
return {
|
|
659
|
+
type: BeaconMessageType.Error,
|
|
660
|
+
id,
|
|
661
|
+
senderId: '',
|
|
662
|
+
version: '2',
|
|
663
|
+
errorType: BeaconErrorType.ABORTED_ERROR
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* @internal
|
|
668
|
+
*/
|
|
669
|
+
isDestroyed() {
|
|
670
|
+
return this.destroyed;
|
|
671
|
+
}
|
|
672
|
+
assertNotDestroyed() {
|
|
673
|
+
if (this.destroyed) {
|
|
674
|
+
throw new Error('DAppClient has been destroyed and cannot be used again.');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
460
677
|
async init(transport, substratePairing) {
|
|
678
|
+
this.assertNotDestroyed();
|
|
679
|
+
const requestedSubstratePairing = substratePairing === true;
|
|
461
680
|
if (this._initPromise) {
|
|
681
|
+
if (!transport &&
|
|
682
|
+
this._initReject &&
|
|
683
|
+
this._initSubstratePairing !== requestedSubstratePairing) {
|
|
684
|
+
throw new Error('Cannot start a permission request with different pairing options while another pairing is pending');
|
|
685
|
+
}
|
|
462
686
|
return this._initPromise;
|
|
463
687
|
}
|
|
464
688
|
try {
|
|
@@ -467,160 +691,265 @@ export class DAppClient extends Client {
|
|
|
467
691
|
catch {
|
|
468
692
|
//
|
|
469
693
|
}
|
|
470
|
-
this._initPromise = new Promise(async (resolve) => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
await this.initInternalTransports();
|
|
493
|
-
if (!this.postMessageTransport || !this.p2pTransport || !this.walletConnectTransport) {
|
|
494
|
-
return;
|
|
694
|
+
this._initPromise = new Promise(async (resolve, reject) => {
|
|
695
|
+
this._initSubstratePairing = requestedSubstratePairing;
|
|
696
|
+
// Capture reject so abort paths (modal close, transport-level rejection)
|
|
697
|
+
// can unwedge a pending `await this.init()`. _initReject is cleared by
|
|
698
|
+
// the .finally below whether the promise resolves or rejects, so the
|
|
699
|
+
// field accurately means "currently rejectable" rather than "most recent
|
|
700
|
+
// rejector".
|
|
701
|
+
this._initReject = (reason) => {
|
|
702
|
+
this._initPromise = undefined;
|
|
703
|
+
reject(reason);
|
|
704
|
+
};
|
|
705
|
+
// The body uses `new Promise(async ...)` (an anti-pattern: unhandled
|
|
706
|
+
// throws are swallowed). Wrap it so any unexpected throw rejects the
|
|
707
|
+
// promise instead of stranding `await this.init()` callers forever.
|
|
708
|
+
// A proper structural fix would replace the async-executor entirely
|
|
709
|
+
// with chained promises; left as follow-up to keep this PR focused.
|
|
710
|
+
try {
|
|
711
|
+
if (transport) {
|
|
712
|
+
await this.addListener(transport);
|
|
713
|
+
resolve(await super.init(transport));
|
|
495
714
|
}
|
|
496
|
-
this.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// Select the transport that matches the active account
|
|
500
|
-
if (origin === Origin.EXTENSION) {
|
|
501
|
-
resolve(await super.init(this.postMessageTransport));
|
|
502
|
-
}
|
|
503
|
-
else if (origin === Origin.P2P) {
|
|
504
|
-
resolve(await super.init(this.p2pTransport));
|
|
505
|
-
}
|
|
506
|
-
else if (origin === Origin.WALLETCONNECT) {
|
|
507
|
-
resolve(await super.init(this.walletConnectTransport));
|
|
508
|
-
}
|
|
715
|
+
else if (this._transport.isSettled()) {
|
|
716
|
+
await (await this.transport).connect();
|
|
717
|
+
resolve(await super.init(await this.transport));
|
|
509
718
|
}
|
|
510
719
|
else {
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
518
|
-
|
|
720
|
+
const activeAccount = await this.getActiveAccount();
|
|
721
|
+
const stopListening = () => {
|
|
722
|
+
const onError = (err) => logger.error('stopListeningForNewPeers', err);
|
|
723
|
+
if (this.postMessageTransport) {
|
|
724
|
+
this.postMessageTransport.stopListeningForNewPeers().catch(onError);
|
|
725
|
+
}
|
|
726
|
+
if (this.p2pTransport) {
|
|
727
|
+
this.p2pTransport.stopListeningForNewPeers().catch(onError);
|
|
728
|
+
}
|
|
729
|
+
if (this.walletConnectTransport) {
|
|
730
|
+
this.walletConnectTransport.stopListeningForNewPeers().catch(onError);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
await this.initInternalTransports();
|
|
734
|
+
if (!this.postMessageTransport ||
|
|
735
|
+
!this.p2pTransport ||
|
|
736
|
+
(!this.disableWalletConnect && !this.walletConnectTransport)) {
|
|
737
|
+
// Bare return here used to strand the promise forever -- no UI is
|
|
738
|
+
// emitted, no peer ever pairs, no abort path fires. Reject explicitly
|
|
739
|
+
// so callers see a deterministic error. (WalletConnect is exempt when
|
|
740
|
+
// disableWalletConnect is set -- its transport is intentionally absent.)
|
|
741
|
+
const noTransportError = {
|
|
742
|
+
type: BeaconMessageType.Error,
|
|
743
|
+
id: '',
|
|
744
|
+
senderId: '',
|
|
745
|
+
version: '2',
|
|
746
|
+
errorType: BeaconErrorType.UNKNOWN_ERROR
|
|
747
|
+
};
|
|
748
|
+
reject(noTransportError);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
this.postMessageTransport.connect().then().catch(console.error);
|
|
752
|
+
if (activeAccount && activeAccount.origin) {
|
|
753
|
+
const origin = activeAccount.origin.type;
|
|
754
|
+
// Select the transport that matches the active account
|
|
755
|
+
if (origin === Origin.EXTENSION) {
|
|
756
|
+
resolve(await super.init(this.postMessageTransport));
|
|
757
|
+
}
|
|
758
|
+
else if (origin === Origin.P2P) {
|
|
759
|
+
resolve(await super.init(this.p2pTransport));
|
|
760
|
+
}
|
|
761
|
+
else if (origin === Origin.WALLETCONNECT && this.walletConnectTransport) {
|
|
762
|
+
resolve(await super.init(this.walletConnectTransport));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
const p2pTransport = this.p2pTransport;
|
|
767
|
+
const postMessageTransport = this.postMessageTransport;
|
|
768
|
+
const walletConnectTransport = this.walletConnectTransport;
|
|
769
|
+
postMessageTransport
|
|
770
|
+
.listenForNewPeer((peer) => {
|
|
771
|
+
logger.log('init', 'postmessage transport peer connected', peer);
|
|
772
|
+
this.analytics.track('event', 'DAppClient', 'Extension connected', {
|
|
773
|
+
peerName: peer.name
|
|
774
|
+
});
|
|
775
|
+
this.events
|
|
776
|
+
.emit(BeaconEvent.PAIR_SUCCESS, peer)
|
|
777
|
+
.catch((emitError) => console.warn(emitError));
|
|
778
|
+
this.setActivePeer(peer).catch(console.error);
|
|
779
|
+
this.setTransport(this.postMessageTransport).catch(console.error);
|
|
780
|
+
stopListening();
|
|
781
|
+
resolve(TransportType.POST_MESSAGE);
|
|
782
|
+
})
|
|
783
|
+
.catch(console.error);
|
|
784
|
+
p2pTransport
|
|
785
|
+
.listenForNewPeer((peer) => {
|
|
786
|
+
logger.log('init', 'p2p transport peer connected', peer);
|
|
787
|
+
this.analytics.track('event', 'DAppClient', 'octez.connect Wallet connected', {
|
|
788
|
+
peerName: peer.name
|
|
789
|
+
});
|
|
790
|
+
this.events
|
|
791
|
+
.emit(BeaconEvent.PAIR_SUCCESS, peer)
|
|
792
|
+
.catch((emitError) => console.warn(emitError));
|
|
793
|
+
this.setActivePeer(peer).catch(console.error);
|
|
794
|
+
this.setTransport(this.p2pTransport).catch(console.error);
|
|
795
|
+
stopListening();
|
|
796
|
+
resolve(TransportType.P2P);
|
|
797
|
+
})
|
|
798
|
+
.catch(console.error);
|
|
799
|
+
walletConnectTransport
|
|
800
|
+
?.listenForNewPeer((peer) => {
|
|
801
|
+
logger.log('init', 'walletconnect transport peer connected', peer);
|
|
802
|
+
this.analytics.track('event', 'DAppClient', 'WalletConnect Wallet connected', {
|
|
803
|
+
peerName: peer.name
|
|
804
|
+
});
|
|
805
|
+
this.events
|
|
806
|
+
.emit(BeaconEvent.PAIR_SUCCESS, peer)
|
|
807
|
+
.catch((emitError) => console.warn(emitError));
|
|
808
|
+
this.setActivePeer(peer).catch(console.error);
|
|
809
|
+
this.setTransport(this.walletConnectTransport).catch(console.error);
|
|
810
|
+
stopListening();
|
|
811
|
+
resolve(TransportType.WALLETCONNECT);
|
|
812
|
+
})
|
|
813
|
+
.catch(console.error);
|
|
814
|
+
PostMessageTransport.getAvailableExtensions()
|
|
815
|
+
.then(async (extensions) => {
|
|
816
|
+
this.analytics.track('event', 'DAppClient', 'Extensions detected', { extensions });
|
|
817
|
+
})
|
|
818
|
+
.catch((error) => {
|
|
819
|
+
this._initPromise = undefined;
|
|
820
|
+
console.error(error);
|
|
519
821
|
});
|
|
520
|
-
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
822
|
+
const abortHandler = async () => {
|
|
823
|
+
logger.log('init', 'ABORTED');
|
|
824
|
+
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'abort'));
|
|
825
|
+
await Promise.all([
|
|
826
|
+
postMessageTransport.disconnect(),
|
|
827
|
+
// p2pTransport.disconnect(), do not abort connection manually
|
|
828
|
+
walletConnectTransport?.disconnect()
|
|
829
|
+
]);
|
|
830
|
+
this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
|
|
831
|
+
this._activeAccount.isResolved() && this.clearActiveAccount();
|
|
832
|
+
// Reject _initPromise so any awaiter (makeRequest -> requestPermissions)
|
|
833
|
+
// unwinds via handleRequestError instead of hanging. _initReject
|
|
834
|
+
// also clears _initPromise as a side effect.
|
|
835
|
+
this._initReject?.(this.createAbortedError());
|
|
836
|
+
};
|
|
837
|
+
const serializer = new Serializer();
|
|
838
|
+
const p2pPeerInfo = new Promise(async (resolve) => {
|
|
839
|
+
try {
|
|
840
|
+
await p2pTransport.connect();
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
logger.error(err);
|
|
844
|
+
await this.hideUI(['alert']); // hide pairing alert
|
|
845
|
+
setTimeout(() => this.events.emit(BeaconEvent.GENERIC_ERROR, err.message), 1000);
|
|
846
|
+
abortHandler().catch((abortErr) => logger.warn('init', 'abortHandler', abortErr));
|
|
847
|
+
resolve('');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
resolve(await serializer.serialize(await p2pTransport.getPairingRequestInfo()));
|
|
534
851
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
logger.log('init', 'walletconnect transport peer connected', peer);
|
|
547
|
-
this.analytics.track('event', 'DAppClient', 'WalletConnect Wallet connected', {
|
|
548
|
-
peerName: peer.name
|
|
852
|
+
const walletConnectPeerInfo = walletConnectTransport
|
|
853
|
+
? createLazyPromise(() => walletConnectTransport
|
|
854
|
+
.getPairingRequestInfo()
|
|
855
|
+
.then((pairingRequestInfo) => pairingRequestInfo.uri)
|
|
856
|
+
.catch((error) => {
|
|
857
|
+
logger.warn('init', 'walletconnect pairing request failed', error);
|
|
858
|
+
return '';
|
|
859
|
+
}))
|
|
860
|
+
: undefined;
|
|
861
|
+
const postmessagePeerInfo = new Promise(async (resolve) => {
|
|
862
|
+
resolve(await serializer.serialize(await postMessageTransport.getPairingRequestInfo()));
|
|
549
863
|
});
|
|
550
864
|
this.events
|
|
551
|
-
.emit(BeaconEvent.
|
|
865
|
+
.emit(BeaconEvent.PAIR_INIT, {
|
|
866
|
+
p2pPeerInfo,
|
|
867
|
+
postmessagePeerInfo,
|
|
868
|
+
// Omitted entirely when WalletConnect is disabled, so the pairing UI
|
|
869
|
+
// never tries to read a peer-info promise for a transport that isn't there.
|
|
870
|
+
...(walletConnectPeerInfo ? { walletConnectPeerInfo } : {}),
|
|
871
|
+
networkType: this.network.type,
|
|
872
|
+
abortedHandler: abortHandler.bind(this),
|
|
873
|
+
disclaimerText: this.disclaimerText,
|
|
874
|
+
analytics: this.analytics,
|
|
875
|
+
featuredWallets: this.featuredWallets,
|
|
876
|
+
substratePairing
|
|
877
|
+
})
|
|
552
878
|
.catch((emitError) => console.warn(emitError));
|
|
553
|
-
|
|
554
|
-
this.setTransport(this.walletConnectTransport).catch(console.error);
|
|
555
|
-
stopListening();
|
|
556
|
-
resolve(TransportType.WALLETCONNECT);
|
|
557
|
-
})
|
|
558
|
-
.catch(console.error);
|
|
559
|
-
PostMessageTransport.getAvailableExtensions()
|
|
560
|
-
.then(async (extensions) => {
|
|
561
|
-
this.analytics.track('event', 'DAppClient', 'Extensions detected', { extensions });
|
|
562
|
-
})
|
|
563
|
-
.catch((error) => {
|
|
564
|
-
this._initPromise = undefined;
|
|
565
|
-
console.error(error);
|
|
566
|
-
});
|
|
567
|
-
const abortHandler = async () => {
|
|
568
|
-
logger.log('init', 'ABORTED');
|
|
569
|
-
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'abort'));
|
|
570
|
-
await Promise.all([
|
|
571
|
-
postMessageTransport.disconnect(),
|
|
572
|
-
// p2pTransport.disconnect(), do not abort connection manually
|
|
573
|
-
walletConnectTransport.disconnect()
|
|
574
|
-
]);
|
|
575
|
-
this.postMessageTransport = this.walletConnectTransport = this.p2pTransport = undefined;
|
|
576
|
-
this._activeAccount.isResolved() && this.clearActiveAccount();
|
|
577
|
-
this._initPromise = undefined;
|
|
578
|
-
};
|
|
579
|
-
const serializer = new Serializer();
|
|
580
|
-
const p2pPeerInfo = new Promise(async (resolve) => {
|
|
581
|
-
try {
|
|
582
|
-
await p2pTransport.connect();
|
|
583
|
-
}
|
|
584
|
-
catch (err) {
|
|
585
|
-
logger.error(err);
|
|
586
|
-
await this.hideUI(['alert']); // hide pairing alert
|
|
587
|
-
setTimeout(() => this.events.emit(BeaconEvent.GENERIC_ERROR, err.message), 1000);
|
|
588
|
-
abortHandler();
|
|
589
|
-
resolve('');
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
resolve(await serializer.serialize(await p2pTransport.getPairingRequestInfo()));
|
|
593
|
-
});
|
|
594
|
-
const walletConnectPeerInfo = new Promise(async (resolve) => {
|
|
595
|
-
resolve((await walletConnectTransport.getPairingRequestInfo()).uri);
|
|
596
|
-
});
|
|
597
|
-
const postmessagePeerInfo = new Promise(async (resolve) => {
|
|
598
|
-
resolve(await serializer.serialize(await postMessageTransport.getPairingRequestInfo()));
|
|
599
|
-
});
|
|
600
|
-
this.events
|
|
601
|
-
.emit(BeaconEvent.PAIR_INIT, {
|
|
602
|
-
p2pPeerInfo,
|
|
603
|
-
postmessagePeerInfo,
|
|
604
|
-
walletConnectPeerInfo,
|
|
605
|
-
networkType: this.network.type,
|
|
606
|
-
abortedHandler: abortHandler.bind(this),
|
|
607
|
-
disclaimerText: this.disclaimerText,
|
|
608
|
-
analytics: this.analytics,
|
|
609
|
-
featuredWallets: this.featuredWallets,
|
|
610
|
-
substratePairing
|
|
611
|
-
})
|
|
612
|
-
.catch((emitError) => console.warn(emitError));
|
|
879
|
+
}
|
|
613
880
|
}
|
|
614
881
|
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
// An async-executor throw would otherwise be silently swallowed,
|
|
884
|
+
// leaving _initPromise pending and every awaiter stranded.
|
|
885
|
+
reject(err);
|
|
886
|
+
}
|
|
615
887
|
});
|
|
888
|
+
// Drop the _initReject reference whether init resolved or rejected, so the
|
|
889
|
+
// field is never stale. Using an explicit helper keeps the dangling
|
|
890
|
+
// observer chain out of statement position (which the lint rules forbid).
|
|
891
|
+
this.clearInitRejectOnSettle(this._initPromise);
|
|
616
892
|
return this._initPromise;
|
|
617
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Attach a settle observer to {@link _initPromise} that drops
|
|
896
|
+
* {@link _initReject} once the promise settles (resolved or rejected).
|
|
897
|
+
* Errors on the observer chain are swallowed; only the original
|
|
898
|
+
* {@link _initPromise} surfaces them to its awaiters.
|
|
899
|
+
*/
|
|
900
|
+
clearInitRejectOnSettle(initPromise) {
|
|
901
|
+
initPromise
|
|
902
|
+
.finally(() => {
|
|
903
|
+
this._initReject = undefined;
|
|
904
|
+
this._initSubstratePairing = undefined;
|
|
905
|
+
})
|
|
906
|
+
.catch(() => {
|
|
907
|
+
// observer-only; original promise's rejection is delivered to awaiters
|
|
908
|
+
});
|
|
909
|
+
}
|
|
618
910
|
/**
|
|
619
911
|
* Returns the active account
|
|
620
912
|
*/
|
|
621
913
|
async getActiveAccount() {
|
|
914
|
+
this.assertNotDestroyed();
|
|
622
915
|
return this._activeAccount.promise;
|
|
623
916
|
}
|
|
917
|
+
retainStorageValidationPromise() {
|
|
918
|
+
// Constructor validation is intentionally fire-and-forget, but retaining
|
|
919
|
+
// the promise gives tests a deterministic hook without changing public API.
|
|
920
|
+
this.storageValidated.catch((err) => logger.error(err.message));
|
|
921
|
+
}
|
|
922
|
+
async deactivateInvalidAccountState(reason) {
|
|
923
|
+
if (this.hasEmittedInvalidAccountDeactivated) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
this.hasEmittedInvalidAccountDeactivated = true;
|
|
927
|
+
logger.log('deactivateInvalidAccountState', reason);
|
|
928
|
+
if (reason === 'missing_active_account') {
|
|
929
|
+
await this.repairMissingActiveAccount();
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
await this.resetInvalidState(false);
|
|
933
|
+
}
|
|
934
|
+
await this.events.emit(BeaconEvent.INVALID_ACCOUNT_DEACTIVATED, { reason });
|
|
935
|
+
}
|
|
936
|
+
async repairMissingActiveAccount() {
|
|
937
|
+
this._activeAccount = ExposedPromise.resolve(undefined);
|
|
938
|
+
await this.storage.delete(StorageKey.ACTIVE_ACCOUNT);
|
|
939
|
+
await this.setActivePeer(undefined);
|
|
940
|
+
}
|
|
941
|
+
hasStoredActiveAccountPointer() {
|
|
942
|
+
// LocalStorage-specific repair: that backend is where literal sentinel strings were produced.
|
|
943
|
+
if (typeof localStorage === 'undefined') {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
return localStorage.getItem(this.storage.getPrefixedKey(StorageKey.ACTIVE_ACCOUNT)) !== null;
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
624
953
|
async isInvalidState(account) {
|
|
625
954
|
const activeAccount = await this._activeAccount.promise;
|
|
626
955
|
return !activeAccount
|
|
@@ -628,11 +957,15 @@ export class DAppClient extends Client {
|
|
|
628
957
|
: activeAccount?.address !== account?.address && !this.isGetActiveAccountHandled;
|
|
629
958
|
}
|
|
630
959
|
async resetInvalidState(emit = true) {
|
|
631
|
-
this.accountManager.removeAllAccounts();
|
|
960
|
+
await this.accountManager.removeAllAccounts();
|
|
632
961
|
this._activeAccount = ExposedPromise.resolve(undefined);
|
|
633
|
-
this.storage.
|
|
634
|
-
|
|
635
|
-
|
|
962
|
+
await this.storage.delete(StorageKey.ACTIVE_ACCOUNT);
|
|
963
|
+
if (emit) {
|
|
964
|
+
await this.events.emit(BeaconEvent.INVALID_ACTIVE_ACCOUNT_STATE);
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
await this.hideUI(['alert']);
|
|
968
|
+
}
|
|
636
969
|
await Promise.all([
|
|
637
970
|
this.postMessageTransport?.disconnect(),
|
|
638
971
|
this.walletConnectTransport?.disconnect()
|
|
@@ -648,7 +981,8 @@ export class DAppClient extends Client {
|
|
|
648
981
|
* @param account The account that will be set as the active account
|
|
649
982
|
*/
|
|
650
983
|
async setActiveAccount(account) {
|
|
651
|
-
|
|
984
|
+
const activeAccountAlreadyCleared = !account && (await this.isActiveAccountAlreadyCleared());
|
|
985
|
+
if (!activeAccountAlreadyCleared && !this.isGetActiveAccountHandled) {
|
|
652
986
|
console.warn(`An active account has been received, but no active subscription was found for BeaconEvent.ACTIVE_ACCOUNT_SET.`);
|
|
653
987
|
}
|
|
654
988
|
if (account && this._activeAccount.isSettled() && (await this.isInvalidState(account))) {
|
|
@@ -660,9 +994,9 @@ export class DAppClient extends Client {
|
|
|
660
994
|
}
|
|
661
995
|
// when I'm resetting the activeAccount
|
|
662
996
|
if (!account && this._activeAccount.isResolved() && (await this.getActiveAccount())) {
|
|
663
|
-
const transport = await this.transport;
|
|
997
|
+
const transport = this._transport.isResolved() ? await this.transport : undefined;
|
|
664
998
|
const activeAccount = await this.getActiveAccount();
|
|
665
|
-
if (!
|
|
999
|
+
if (!activeAccount) {
|
|
666
1000
|
return;
|
|
667
1001
|
}
|
|
668
1002
|
if (!this.debounceSetActiveAccount && transport instanceof WalletConnectTransport) {
|
|
@@ -678,22 +1012,15 @@ export class DAppClient extends Client {
|
|
|
678
1012
|
type: 'DISCONNECT'
|
|
679
1013
|
});
|
|
680
1014
|
}
|
|
681
|
-
|
|
682
|
-
.filter(([id, _promise]) => id !== 'session_update')
|
|
683
|
-
.forEach(([id, promise]) => {
|
|
684
|
-
promise.reject({
|
|
685
|
-
type: BeaconMessageType.Error,
|
|
686
|
-
errorType: BeaconErrorType.ABORTED_ERROR,
|
|
687
|
-
id,
|
|
688
|
-
senderId: '',
|
|
689
|
-
version: '2'
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
this.openRequests.clear();
|
|
1015
|
+
this.abortOpenRequests();
|
|
693
1016
|
this.debounceSetActiveAccount = false;
|
|
694
1017
|
}
|
|
695
1018
|
}
|
|
696
|
-
if (
|
|
1019
|
+
if (activeAccountAlreadyCleared) {
|
|
1020
|
+
// Keep peer and transport cleanup below idempotent, but do not write storage or emit
|
|
1021
|
+
// another ACTIVE_ACCOUNT_SET(undefined) for an already-empty account.
|
|
1022
|
+
}
|
|
1023
|
+
else if (this._activeAccount.isSettled()) {
|
|
697
1024
|
// If the promise has already been resolved we need to create a new one.
|
|
698
1025
|
this._activeAccount = ExposedPromise.resolve(account);
|
|
699
1026
|
}
|
|
@@ -734,10 +1061,37 @@ export class DAppClient extends Client {
|
|
|
734
1061
|
await this.setActivePeer(undefined);
|
|
735
1062
|
await this.setTransport(undefined);
|
|
736
1063
|
}
|
|
737
|
-
|
|
738
|
-
|
|
1064
|
+
if (account) {
|
|
1065
|
+
await this.storage.set(StorageKey.ACTIVE_ACCOUNT, account.accountIdentifier);
|
|
1066
|
+
}
|
|
1067
|
+
else if (!activeAccountAlreadyCleared) {
|
|
1068
|
+
await this.storage.delete(StorageKey.ACTIVE_ACCOUNT);
|
|
1069
|
+
}
|
|
1070
|
+
if (!activeAccountAlreadyCleared) {
|
|
1071
|
+
await this.events.emit(BeaconEvent.ACTIVE_ACCOUNT_SET, account);
|
|
1072
|
+
}
|
|
739
1073
|
return;
|
|
740
1074
|
}
|
|
1075
|
+
async isActiveAccountAlreadyCleared() {
|
|
1076
|
+
return this._activeAccount.isResolved() && !(await this.getActiveAccount());
|
|
1077
|
+
}
|
|
1078
|
+
async recoverActiveAccountFromAccountsChange() {
|
|
1079
|
+
const activeAccountIdentifier = await this.storage.get(StorageKey.ACTIVE_ACCOUNT);
|
|
1080
|
+
if (!activeAccountIdentifier || activeAccountIdentifier === 'undefined') {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (this._activeAccount.isResolved()) {
|
|
1084
|
+
const activeAccount = await this.getActiveAccount();
|
|
1085
|
+
if (activeAccount?.accountIdentifier === activeAccountIdentifier) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const account = await this.getAccount(activeAccountIdentifier);
|
|
1090
|
+
if (!account) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
await this.setActiveAccount(account);
|
|
1094
|
+
}
|
|
741
1095
|
/**
|
|
742
1096
|
* Clear the active account
|
|
743
1097
|
*/
|
|
@@ -812,30 +1166,16 @@ export class DAppClient extends Client {
|
|
|
812
1166
|
})
|
|
813
1167
|
};
|
|
814
1168
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
sendMetrics(uri, options, thenHandler, catchHandler) {
|
|
824
|
-
if (!this.enableMetrics && uri === 'performance-metrics/save') {
|
|
825
|
-
options && this.updateMetricsStorage(options.body);
|
|
826
|
-
}
|
|
827
|
-
if (!this.enableMetrics) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
fetch(`${BACKEND_URL}/${uri}`, options)
|
|
831
|
-
.then((res) => thenHandler && thenHandler(res))
|
|
832
|
-
.catch((err) => {
|
|
833
|
-
console.warn('Network error encountered. Metrics sharing have been automatically disabled.');
|
|
834
|
-
logger.error(err.message);
|
|
835
|
-
this.enableMetrics = false; // in the event of a network error, stop sending metrics
|
|
836
|
-
catchHandler && catchHandler(err);
|
|
837
|
-
});
|
|
1169
|
+
// Metrics phone home to a Papers-controlled endpoint that the ECAD fork
|
|
1170
|
+
// does not control or ingest. The disabled path also queued payloads into
|
|
1171
|
+
// IndexedDB before the DB was ready, surfacing as unhandledRejection.
|
|
1172
|
+
// Hard-disabled here; full removal (option, storage key, IDB store,
|
|
1173
|
+
// BACKEND_URL constant, all call sites) tracked as a follow-up.
|
|
1174
|
+
/* 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. */
|
|
1175
|
+
sendMetrics(_uri, _options, _thenHandler, _catchHandler) {
|
|
1176
|
+
return;
|
|
838
1177
|
}
|
|
1178
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
839
1179
|
async checkMakeRequest() {
|
|
840
1180
|
const isResolved = this._transport.isResolved();
|
|
841
1181
|
const isWCInstance = isResolved && (await this.transport) instanceof WalletConnectTransport;
|
|
@@ -880,14 +1220,19 @@ export class DAppClient extends Client {
|
|
|
880
1220
|
/**
|
|
881
1221
|
* Remove all peers and all accounts that have been connected through those peers
|
|
882
1222
|
*/
|
|
883
|
-
async removeAllPeers(sendDisconnectToPeers = false) {
|
|
884
|
-
const transport = await this.transport;
|
|
1223
|
+
async removeAllPeers(sendDisconnectToPeers = false, options = {}) {
|
|
1224
|
+
const transport = (await this.transport);
|
|
885
1225
|
const peers = await transport.getPeers();
|
|
886
|
-
const removePeerResult = transport.removeAllPeers();
|
|
1226
|
+
const removePeerResult = await transport.removeAllPeers();
|
|
887
1227
|
await this.removeAccountsForPeers(peers);
|
|
888
1228
|
if (sendDisconnectToPeers) {
|
|
889
1229
|
const disconnectPromises = peers.map((peer) => this.sendDisconnectToPeer(peer, transport));
|
|
890
|
-
|
|
1230
|
+
if (options.ignoreDisconnectErrors) {
|
|
1231
|
+
await Promise.allSettled(disconnectPromises);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
await Promise.all(disconnectPromises);
|
|
1235
|
+
}
|
|
891
1236
|
}
|
|
892
1237
|
return removePeerResult;
|
|
893
1238
|
}
|
|
@@ -992,13 +1337,15 @@ export class DAppClient extends Client {
|
|
|
992
1337
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'start'));
|
|
993
1338
|
const logId = `makeRequestV3 ${Date.now()}`;
|
|
994
1339
|
logger.time(true, logId);
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1340
|
+
let resolved;
|
|
1341
|
+
try {
|
|
1342
|
+
resolved = await this.makeRequestV3(request);
|
|
1343
|
+
}
|
|
1344
|
+
catch (requestError) {
|
|
1345
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1346
|
+
throw requestError;
|
|
1347
|
+
}
|
|
1348
|
+
const { message: response, connectionInfo } = resolved;
|
|
1002
1349
|
logger.time(false, logId);
|
|
1003
1350
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'start'));
|
|
1004
1351
|
logger.log('RESPONSE V3', response, connectionInfo);
|
|
@@ -1062,14 +1409,18 @@ export class DAppClient extends Client {
|
|
|
1062
1409
|
const res = (await this.checkMakeRequest())
|
|
1063
1410
|
? this.makeRequestV3(request)
|
|
1064
1411
|
: this.makeRequestBC(request);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1412
|
+
let resolved;
|
|
1413
|
+
try {
|
|
1414
|
+
resolved = await res;
|
|
1415
|
+
}
|
|
1416
|
+
catch (requestError) {
|
|
1417
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1418
|
+
throw requestError;
|
|
1419
|
+
}
|
|
1420
|
+
if (!resolved) {
|
|
1421
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1422
|
+
}
|
|
1423
|
+
const { message: response, connectionInfo } = resolved;
|
|
1073
1424
|
logger.time(false, logId);
|
|
1074
1425
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'success'));
|
|
1075
1426
|
await blockchain.handleResponse({
|
|
@@ -1093,16 +1444,34 @@ export class DAppClient extends Client {
|
|
|
1093
1444
|
* @param input The message details we need to prepare the PermissionRequest message.
|
|
1094
1445
|
*/
|
|
1095
1446
|
async requestPermissions(input) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1447
|
+
this.assertNotDestroyed();
|
|
1448
|
+
const scopes = this.getPermissionRequestScopes(input);
|
|
1449
|
+
const requestPermissionsKey = this.getPermissionRequestKey(scopes);
|
|
1450
|
+
if (this._requestPermissionsPromise) {
|
|
1451
|
+
if (this._requestPermissionsKey !== requestPermissionsKey) {
|
|
1452
|
+
throw new Error('Cannot start a permission request with different scopes while another permission request is pending');
|
|
1453
|
+
}
|
|
1454
|
+
return this._requestPermissionsPromise;
|
|
1455
|
+
}
|
|
1456
|
+
const requestPermissionsPromise = this.requestPermissionsInternal(scopes);
|
|
1457
|
+
this._requestPermissionsPromise = requestPermissionsPromise;
|
|
1458
|
+
this._requestPermissionsKey = requestPermissionsKey;
|
|
1459
|
+
try {
|
|
1460
|
+
return await requestPermissionsPromise;
|
|
1461
|
+
}
|
|
1462
|
+
finally {
|
|
1463
|
+
if (this._requestPermissionsPromise === requestPermissionsPromise) {
|
|
1464
|
+
this._requestPermissionsPromise = undefined;
|
|
1465
|
+
this._requestPermissionsKey = undefined;
|
|
1466
|
+
}
|
|
1098
1467
|
}
|
|
1468
|
+
}
|
|
1469
|
+
async requestPermissionsInternal(scopes) {
|
|
1099
1470
|
const request = {
|
|
1100
1471
|
appMetadata: await this.getOwnAppMetadata(),
|
|
1101
1472
|
type: BeaconMessageType.PermissionRequest,
|
|
1102
1473
|
network: this.network,
|
|
1103
|
-
scopes
|
|
1104
|
-
? input.scopes
|
|
1105
|
-
: [PermissionScope.OPERATION_REQUEST, PermissionScope.SIGN]
|
|
1474
|
+
scopes
|
|
1106
1475
|
};
|
|
1107
1476
|
this.analytics.track('event', 'DAppClient', 'Permission requested');
|
|
1108
1477
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'start'));
|
|
@@ -1111,14 +1480,18 @@ export class DAppClient extends Client {
|
|
|
1111
1480
|
const res = (await this.checkMakeRequest()) || !(await this.getActiveAccount())
|
|
1112
1481
|
? this.makeRequest(request, undefined, undefined)
|
|
1113
1482
|
: this.makeRequestBC(request);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1483
|
+
let resolved;
|
|
1484
|
+
try {
|
|
1485
|
+
resolved = await res;
|
|
1486
|
+
}
|
|
1487
|
+
catch (requestError) {
|
|
1488
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1489
|
+
throw requestError;
|
|
1490
|
+
}
|
|
1491
|
+
if (!resolved) {
|
|
1492
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1493
|
+
}
|
|
1494
|
+
const { message, connectionInfo } = resolved;
|
|
1122
1495
|
logger.time(false, logId);
|
|
1123
1496
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('connect', 'success'));
|
|
1124
1497
|
logger.log('requestPermissions', '######## MESSAGE #######');
|
|
@@ -1145,6 +1518,17 @@ export class DAppClient extends Client {
|
|
|
1145
1518
|
});
|
|
1146
1519
|
return output;
|
|
1147
1520
|
}
|
|
1521
|
+
getPermissionRequestScopes(input) {
|
|
1522
|
+
if (input && 'network' in input) {
|
|
1523
|
+
throw new Error('[BEACON] the "network" property is no longer accepted in input. Please provide it when instantiating DAppClient.');
|
|
1524
|
+
}
|
|
1525
|
+
return input && input.scopes
|
|
1526
|
+
? input.scopes
|
|
1527
|
+
: [PermissionScope.OPERATION_REQUEST, PermissionScope.SIGN];
|
|
1528
|
+
}
|
|
1529
|
+
getPermissionRequestKey(scopes) {
|
|
1530
|
+
return [...scopes].sort().join('|');
|
|
1531
|
+
}
|
|
1148
1532
|
/**
|
|
1149
1533
|
* Send a proof of event request to the wallet. The wallet will either accept or decline the challenge.
|
|
1150
1534
|
* 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.
|
|
@@ -1171,14 +1555,18 @@ export class DAppClient extends Client {
|
|
|
1171
1555
|
const res = (await this.checkMakeRequest())
|
|
1172
1556
|
? this.makeRequest(request)
|
|
1173
1557
|
: this.makeRequestBC(request);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1558
|
+
let resolved;
|
|
1559
|
+
try {
|
|
1560
|
+
resolved = await res;
|
|
1561
|
+
}
|
|
1562
|
+
catch (requestError) {
|
|
1563
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1564
|
+
throw requestError;
|
|
1565
|
+
}
|
|
1566
|
+
if (!resolved) {
|
|
1567
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1568
|
+
}
|
|
1569
|
+
const { message, connectionInfo } = resolved;
|
|
1182
1570
|
logger.time(false, logId);
|
|
1183
1571
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'success'));
|
|
1184
1572
|
this.analytics.track('event', 'DAppClient', `Proof of event challenge ${message.isAccepted ? 'accepted' : 'refused'}`, { address: activeAccount.address });
|
|
@@ -1217,14 +1605,18 @@ export class DAppClient extends Client {
|
|
|
1217
1605
|
const res = (await this.checkMakeRequest())
|
|
1218
1606
|
? this.makeRequest(request)
|
|
1219
1607
|
: this.makeRequestBC(request);
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1608
|
+
let resolved;
|
|
1609
|
+
try {
|
|
1610
|
+
resolved = await res;
|
|
1611
|
+
}
|
|
1612
|
+
catch (requestError) {
|
|
1613
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1614
|
+
throw requestError;
|
|
1615
|
+
}
|
|
1616
|
+
if (!resolved) {
|
|
1617
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1618
|
+
}
|
|
1619
|
+
const { message, connectionInfo } = resolved;
|
|
1228
1620
|
logger.time(false, logId);
|
|
1229
1621
|
this.analytics.track('event', 'DAppClient', `Simulated proof of event challenge ${!message.errorMessage ? 'accepted' : 'refused'}`, { address: activeAccount.address });
|
|
1230
1622
|
await this.notifySuccess(request, {
|
|
@@ -1284,14 +1676,18 @@ export class DAppClient extends Client {
|
|
|
1284
1676
|
const res = (await this.checkMakeRequest())
|
|
1285
1677
|
? this.makeRequest(request)
|
|
1286
1678
|
: this.makeRequestBC(request);
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1679
|
+
let resolved;
|
|
1680
|
+
try {
|
|
1681
|
+
resolved = await res;
|
|
1682
|
+
}
|
|
1683
|
+
catch (requestError) {
|
|
1684
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1685
|
+
throw requestError;
|
|
1686
|
+
}
|
|
1687
|
+
if (!resolved) {
|
|
1688
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1689
|
+
}
|
|
1690
|
+
const { message, connectionInfo } = resolved;
|
|
1295
1691
|
logger.time(false, logId);
|
|
1296
1692
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'success'));
|
|
1297
1693
|
await this.notifySuccess(request, {
|
|
@@ -1379,14 +1775,18 @@ export class DAppClient extends Client {
|
|
|
1379
1775
|
const res = (await this.checkMakeRequest())
|
|
1380
1776
|
? this.makeRequest(request)
|
|
1381
1777
|
: this.makeRequestBC(request);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1778
|
+
let resolved;
|
|
1779
|
+
try {
|
|
1780
|
+
resolved = await res;
|
|
1781
|
+
}
|
|
1782
|
+
catch (requestError) {
|
|
1783
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1784
|
+
throw requestError;
|
|
1785
|
+
}
|
|
1786
|
+
if (!resolved) {
|
|
1787
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1788
|
+
}
|
|
1789
|
+
const { message, connectionInfo } = resolved;
|
|
1390
1790
|
logger.time(false, logId);
|
|
1391
1791
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'success'));
|
|
1392
1792
|
await this.notifySuccess(request, {
|
|
@@ -1426,14 +1826,18 @@ export class DAppClient extends Client {
|
|
|
1426
1826
|
const res = (await this.checkMakeRequest())
|
|
1427
1827
|
? this.makeRequest(request)
|
|
1428
1828
|
: this.makeRequestBC(request);
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1829
|
+
let resolved;
|
|
1830
|
+
try {
|
|
1831
|
+
resolved = await res;
|
|
1832
|
+
}
|
|
1833
|
+
catch (requestError) {
|
|
1834
|
+
await this.runRequestErrorSideEffects(request, requestError, logId);
|
|
1835
|
+
throw requestError;
|
|
1836
|
+
}
|
|
1837
|
+
if (!resolved) {
|
|
1838
|
+
throw new Error('Internal error: makeRequest returned no result');
|
|
1839
|
+
}
|
|
1840
|
+
const { message, connectionInfo } = resolved;
|
|
1437
1841
|
logger.time(false, logId);
|
|
1438
1842
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'success'));
|
|
1439
1843
|
await this.notifySuccess(request, {
|
|
@@ -1499,11 +1903,14 @@ export class DAppClient extends Client {
|
|
|
1499
1903
|
* @param peersToRemove An array of peers for which accounts should be removed
|
|
1500
1904
|
*/
|
|
1501
1905
|
async removeAccountsForPeers(peersToRemove) {
|
|
1906
|
+
if (peersToRemove.length === 0) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1502
1909
|
const peerIdsToRemove = peersToRemove.map((peer) => peer.senderId);
|
|
1503
1910
|
return this.removeAccountsForPeerIds(peerIdsToRemove);
|
|
1504
1911
|
}
|
|
1505
1912
|
async removeAccountsForPeerIds(peerIds) {
|
|
1506
|
-
const accounts = await this.accountManager.getAccounts();
|
|
1913
|
+
const accounts = (await this.accountManager.getAccounts()) ?? [];
|
|
1507
1914
|
// Remove all accounts with origin of the specified peer
|
|
1508
1915
|
const accountsToRemove = accounts.filter((account) => peerIds.includes(account.senderId));
|
|
1509
1916
|
const accountIdentifiersToRemove = accountsToRemove.map((accountInfo) => accountInfo.accountIdentifier);
|
|
@@ -1516,6 +1923,44 @@ export class DAppClient extends Client {
|
|
|
1516
1923
|
}
|
|
1517
1924
|
}
|
|
1518
1925
|
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Run the standard request-error side effects (abort/error metric,
|
|
1928
|
+
* timer stop, {@link handleRequestError}) on a single awaited control
|
|
1929
|
+
* flow so callers can re-throw the original {@link ErrorResponse} without
|
|
1930
|
+
* leaking an UnhandledPromiseRejection.
|
|
1931
|
+
*
|
|
1932
|
+
* Why this exists: every request method historically used a fire-and-
|
|
1933
|
+
* forget `res.catch(handler)` whose handler did `throw await
|
|
1934
|
+
* this.handleRequestError(...)`. The catch returned a detached promise
|
|
1935
|
+
* nobody awaited; `handleRequestError`'s thrown wrapped error became an
|
|
1936
|
+
* unhandled rejection. The bug rarely surfaced before the dapp-init
|
|
1937
|
+
* promise was made rejectable, because pre-pairing rejection paths used
|
|
1938
|
+
* to hang instead of routing through these handlers. Once init started
|
|
1939
|
+
* rejecting reliably, the unhandled rejection started firing on every
|
|
1940
|
+
* WC session-proposal rejection. We swallow the wrapped throw here so
|
|
1941
|
+
* callers can re-throw the original `ErrorResponse`, matching the
|
|
1942
|
+
* post-pairing rejection contract at the openRequest path.
|
|
1943
|
+
*
|
|
1944
|
+
* @param request The request we sent
|
|
1945
|
+
* @param requestError The error we received
|
|
1946
|
+
* @param logId The {@link logger.time} label opened for this request
|
|
1947
|
+
*/
|
|
1948
|
+
async runRequestErrorSideEffects(request, rawError, logId) {
|
|
1949
|
+
// V3 request shapes (PermissionRequestV3, BlockchainRequestV3) flow through
|
|
1950
|
+
// here too. They aren't part of BeaconRequestInputMessage, but
|
|
1951
|
+
// handleRequestError only inspects `.type` plus optional fields it already
|
|
1952
|
+
// guards on. Catch params are `unknown` under strict TS, same story.
|
|
1953
|
+
const typedRequest = request;
|
|
1954
|
+
const requestError = rawError;
|
|
1955
|
+
if (requestError.errorType === BeaconErrorType.ABORTED_ERROR) {
|
|
1956
|
+
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'abort'));
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
this.sendMetrics('performance-metrics/save', await this.buildPayload('message', 'error'));
|
|
1960
|
+
}
|
|
1961
|
+
logger.time(false, logId);
|
|
1962
|
+
await this.handleRequestError(typedRequest, requestError).catch(() => undefined);
|
|
1963
|
+
}
|
|
1519
1964
|
/**
|
|
1520
1965
|
* This message handles errors that we receive from the wallet.
|
|
1521
1966
|
*
|
|
@@ -1683,18 +2128,8 @@ export class DAppClient extends Client {
|
|
|
1683
2128
|
}
|
|
1684
2129
|
async makeRequest(requestInput, skipResponse, otherTabMessageId) {
|
|
1685
2130
|
const messageId = otherTabMessageId ?? (await generateGUID());
|
|
1686
|
-
if (this._initPromise && this.isInitPending) {
|
|
1687
|
-
await Promise.all([
|
|
1688
|
-
this.postMessageTransport?.disconnect(),
|
|
1689
|
-
this.walletConnectTransport?.disconnect()
|
|
1690
|
-
]);
|
|
1691
|
-
this._initPromise = undefined;
|
|
1692
|
-
this.hideUI(['toast']);
|
|
1693
|
-
}
|
|
1694
2131
|
logger.log('makeRequest', 'starting');
|
|
1695
|
-
this.isInitPending = true;
|
|
1696
2132
|
await this.init();
|
|
1697
|
-
this.isInitPending = false;
|
|
1698
2133
|
logger.log('makeRequest', 'after init');
|
|
1699
2134
|
if (await this.addRequestAndCheckIfRateLimited()) {
|
|
1700
2135
|
this.events
|
|
@@ -1720,20 +2155,37 @@ export class DAppClient extends Client {
|
|
|
1720
2155
|
exposed = new ExposedPromise();
|
|
1721
2156
|
this.addOpenRequest(request.id, exposed);
|
|
1722
2157
|
}
|
|
1723
|
-
const payload = await new Serializer().serialize(request);
|
|
1724
|
-
const account = await this.getActiveAccount();
|
|
1725
|
-
const peer = await this.getPeer(account);
|
|
1726
|
-
const walletInfo = await this.getWalletInfo(peer, account);
|
|
1727
|
-
logger.log('makeRequest', 'sending message', request);
|
|
1728
2158
|
try {
|
|
1729
|
-
;
|
|
1730
|
-
|
|
2159
|
+
const payload = await new Serializer().serialize(request);
|
|
2160
|
+
const account = await this.getActiveAccount();
|
|
2161
|
+
const peer = await this.getPeer(account);
|
|
2162
|
+
const walletInfo = await this.getWalletInfo(peer, account);
|
|
2163
|
+
logger.log('makeRequest', 'sending message', request);
|
|
2164
|
+
await (await this.transport).send(payload, peer);
|
|
1731
2165
|
if (request.type !== BeaconMessageType.PermissionRequest ||
|
|
1732
2166
|
(this._activeAccount.isResolved() && (await this._activeAccount.promise))) {
|
|
1733
2167
|
this.tryToAppSwitch();
|
|
1734
2168
|
}
|
|
2169
|
+
if (!otherTabMessageId) {
|
|
2170
|
+
this.events
|
|
2171
|
+
.emit(messageEvents[requestInput.type].sent, {
|
|
2172
|
+
walletInfo: {
|
|
2173
|
+
...walletInfo,
|
|
2174
|
+
name: walletInfo.name ?? 'Wallet'
|
|
2175
|
+
},
|
|
2176
|
+
extraInfo: {
|
|
2177
|
+
resetCallback: async () => {
|
|
2178
|
+
await this.disconnect();
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
})
|
|
2182
|
+
.catch((emitError) => logger.warn('makeRequest', emitError));
|
|
2183
|
+
}
|
|
1735
2184
|
}
|
|
1736
2185
|
catch (sendError) {
|
|
2186
|
+
if (!skipResponse) {
|
|
2187
|
+
this.clearOpenRequest(request.id);
|
|
2188
|
+
}
|
|
1737
2189
|
this.events.emit(BeaconEvent.INTERNAL_ERROR, {
|
|
1738
2190
|
text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
|
|
1739
2191
|
buttons: [
|
|
@@ -1748,21 +2200,6 @@ export class DAppClient extends Client {
|
|
|
1748
2200
|
});
|
|
1749
2201
|
throw sendError;
|
|
1750
2202
|
}
|
|
1751
|
-
if (!otherTabMessageId) {
|
|
1752
|
-
this.events
|
|
1753
|
-
.emit(messageEvents[requestInput.type].sent, {
|
|
1754
|
-
walletInfo: {
|
|
1755
|
-
...walletInfo,
|
|
1756
|
-
name: walletInfo.name ?? 'Wallet'
|
|
1757
|
-
},
|
|
1758
|
-
extraInfo: {
|
|
1759
|
-
resetCallback: async () => {
|
|
1760
|
-
this.disconnect();
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
})
|
|
1764
|
-
.catch((emitError) => console.warn(emitError));
|
|
1765
|
-
}
|
|
1766
2203
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1767
2204
|
return exposed?.promise; // TODO: fix type
|
|
1768
2205
|
}
|
|
@@ -1775,19 +2212,9 @@ export class DAppClient extends Client {
|
|
|
1775
2212
|
* @param account The account that the message will be sent to
|
|
1776
2213
|
*/
|
|
1777
2214
|
async makeRequestV3(requestInput, otherTabMessageId) {
|
|
1778
|
-
if (this._initPromise && this.isInitPending) {
|
|
1779
|
-
await Promise.all([
|
|
1780
|
-
this.postMessageTransport?.disconnect(),
|
|
1781
|
-
this.walletConnectTransport?.disconnect()
|
|
1782
|
-
]);
|
|
1783
|
-
this._initPromise = undefined;
|
|
1784
|
-
this.hideUI(['toast']);
|
|
1785
|
-
}
|
|
1786
2215
|
const messageId = otherTabMessageId ?? (await generateGUID());
|
|
1787
2216
|
logger.log('makeRequest', 'starting');
|
|
1788
|
-
this.isInitPending = true;
|
|
1789
2217
|
await this.init(undefined, true);
|
|
1790
|
-
this.isInitPending = false;
|
|
1791
2218
|
logger.log('makeRequest', 'after init');
|
|
1792
2219
|
if (await this.addRequestAndCheckIfRateLimited()) {
|
|
1793
2220
|
this.events
|
|
@@ -1806,20 +2233,34 @@ export class DAppClient extends Client {
|
|
|
1806
2233
|
};
|
|
1807
2234
|
const exposed = new ExposedPromise();
|
|
1808
2235
|
this.addOpenRequest(request.id, exposed);
|
|
1809
|
-
const payload = await new Serializer().serialize(request);
|
|
1810
|
-
const account = await this.getActiveAccount();
|
|
1811
|
-
const peer = await this.getPeer(account);
|
|
1812
|
-
const walletInfo = await this.getWalletInfo(peer, account);
|
|
1813
|
-
logger.log('makeRequest', 'sending message', request);
|
|
1814
2236
|
try {
|
|
1815
|
-
;
|
|
1816
|
-
|
|
2237
|
+
const payload = await new Serializer().serialize(request);
|
|
2238
|
+
const account = await this.getActiveAccount();
|
|
2239
|
+
const peer = await this.getPeer(account);
|
|
2240
|
+
const walletInfo = await this.getWalletInfo(peer, account);
|
|
2241
|
+
logger.log('makeRequest', 'sending message', request);
|
|
2242
|
+
await (await this.transport).send(payload, peer);
|
|
1817
2243
|
if (request.message.type !== BeaconMessageType.PermissionRequest ||
|
|
1818
2244
|
(this._activeAccount.isResolved() && (await this._activeAccount.promise))) {
|
|
1819
2245
|
this.tryToAppSwitch();
|
|
1820
2246
|
}
|
|
2247
|
+
const index = requestInput.type;
|
|
2248
|
+
this.events
|
|
2249
|
+
.emit(messageEvents[index].sent, {
|
|
2250
|
+
walletInfo: {
|
|
2251
|
+
...walletInfo,
|
|
2252
|
+
name: walletInfo.name ?? 'Wallet'
|
|
2253
|
+
},
|
|
2254
|
+
extraInfo: {
|
|
2255
|
+
resetCallback: async () => {
|
|
2256
|
+
await this.disconnect();
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
})
|
|
2260
|
+
.catch((emitError) => logger.warn('makeRequestV3', emitError));
|
|
1821
2261
|
}
|
|
1822
2262
|
catch (sendError) {
|
|
2263
|
+
this.clearOpenRequest(request.id);
|
|
1823
2264
|
this.events.emit(BeaconEvent.INTERNAL_ERROR, {
|
|
1824
2265
|
text: 'Unable to send message. If this problem persists, please reset the connection and pair your wallet again.',
|
|
1825
2266
|
buttons: [
|
|
@@ -1834,20 +2275,6 @@ export class DAppClient extends Client {
|
|
|
1834
2275
|
});
|
|
1835
2276
|
throw sendError;
|
|
1836
2277
|
}
|
|
1837
|
-
const index = requestInput.type;
|
|
1838
|
-
this.events
|
|
1839
|
-
.emit(messageEvents[index].sent, {
|
|
1840
|
-
walletInfo: {
|
|
1841
|
-
...walletInfo,
|
|
1842
|
-
name: walletInfo.name ?? 'Wallet'
|
|
1843
|
-
},
|
|
1844
|
-
extraInfo: {
|
|
1845
|
-
resetCallback: async () => {
|
|
1846
|
-
this.disconnect();
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
})
|
|
1850
|
-
.catch((emitError) => console.warn(emitError));
|
|
1851
2278
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1852
2279
|
return exposed.promise; // TODO: fix type
|
|
1853
2280
|
}
|
|
@@ -1887,24 +2314,140 @@ export class DAppClient extends Client {
|
|
|
1887
2314
|
this.addOpenRequest(id, exposed);
|
|
1888
2315
|
return exposed.promise;
|
|
1889
2316
|
}
|
|
1890
|
-
|
|
2317
|
+
/**
|
|
2318
|
+
* Disconnects all resolved transports (postMessage, P2P, WalletConnect), removes their peers,
|
|
2319
|
+
* and clears active account state. After calling, the DAppClient remains usable for a new
|
|
2320
|
+
* requestPermissions().
|
|
2321
|
+
*/
|
|
2322
|
+
async disconnect(options = {}) {
|
|
2323
|
+
const { notifyPeers = true } = options;
|
|
2324
|
+
if (!this._disconnectPromise) {
|
|
2325
|
+
this._disconnectNotifyPeers = false;
|
|
2326
|
+
}
|
|
2327
|
+
this._disconnectNotifyPeers = this._disconnectNotifyPeers || notifyPeers;
|
|
2328
|
+
if (this._disconnectPromise) {
|
|
2329
|
+
return this._disconnectPromise;
|
|
2330
|
+
}
|
|
2331
|
+
this._disconnectPromise = (async () => this.disconnectInternal())().finally(() => {
|
|
2332
|
+
this._disconnectPromise = undefined;
|
|
2333
|
+
this._disconnectNotifyPeers = false;
|
|
2334
|
+
});
|
|
2335
|
+
return this._disconnectPromise;
|
|
2336
|
+
}
|
|
2337
|
+
async disconnectInternal() {
|
|
2338
|
+
this.abortPendingInit();
|
|
1891
2339
|
if (!this._transport.isResolved()) {
|
|
1892
|
-
|
|
2340
|
+
const initializedTransports = this.getInitializedTransports();
|
|
2341
|
+
this.abortOpenRequests();
|
|
2342
|
+
await this.removeAllPeersFromTransports(initializedTransports);
|
|
2343
|
+
await this.clearActiveAccount();
|
|
2344
|
+
await this.disconnectResolvedTransports(initializedTransports);
|
|
2345
|
+
this.clearInternalTransportReferences();
|
|
2346
|
+
return;
|
|
1893
2347
|
}
|
|
1894
|
-
const transport = await this.transport;
|
|
2348
|
+
const transport = (await this.transport);
|
|
1895
2349
|
if (transport.connectionStatus === TransportStatus.NOT_CONNECTED) {
|
|
1896
|
-
|
|
2350
|
+
await this.clearActiveAccount();
|
|
2351
|
+
this.abortOpenRequests();
|
|
2352
|
+
return;
|
|
1897
2353
|
}
|
|
1898
2354
|
await this.createStateSnapshot();
|
|
1899
2355
|
this.sendMetrics('performance-metrics/save', await this.buildPayload('disconnect', 'start'));
|
|
2356
|
+
this.abortOpenRequests();
|
|
2357
|
+
const transports = this.getResolvedTransports(transport);
|
|
2358
|
+
await this.removeAllPeersFromTransports(transports);
|
|
1900
2359
|
await this.clearActiveAccount();
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2360
|
+
await this.disconnectResolvedTransports(transports);
|
|
2361
|
+
this.clearInternalTransportReferences();
|
|
2362
|
+
this.sendMetrics('performance-metrics/save', await this.buildPayload('disconnect', 'success'));
|
|
2363
|
+
}
|
|
2364
|
+
getInitializedTransports() {
|
|
2365
|
+
const transports = [];
|
|
2366
|
+
const addTransport = (transport) => {
|
|
2367
|
+
if (transport && !transports.includes(transport)) {
|
|
2368
|
+
transports.push(transport);
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
addTransport(this.postMessageTransport);
|
|
2372
|
+
addTransport(this.p2pTransport);
|
|
2373
|
+
addTransport(this.walletConnectTransport);
|
|
2374
|
+
return transports;
|
|
2375
|
+
}
|
|
2376
|
+
getResolvedTransports(selectedTransport) {
|
|
2377
|
+
const transports = this.getInitializedTransports();
|
|
2378
|
+
const addTransport = (transport) => {
|
|
2379
|
+
if (transport && !transports.includes(transport)) {
|
|
2380
|
+
transports.push(transport);
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
addTransport(this.postMessageTransport);
|
|
2384
|
+
addTransport(this.p2pTransport);
|
|
2385
|
+
addTransport(this.walletConnectTransport);
|
|
2386
|
+
addTransport(selectedTransport);
|
|
2387
|
+
return transports;
|
|
2388
|
+
}
|
|
2389
|
+
clearInternalTransportReferences() {
|
|
1904
2390
|
this.postMessageTransport = undefined;
|
|
1905
2391
|
this.p2pTransport = undefined;
|
|
1906
2392
|
this.walletConnectTransport = undefined;
|
|
1907
|
-
|
|
2393
|
+
}
|
|
2394
|
+
async removeAllPeersFromTransports(transports) {
|
|
2395
|
+
const peersByTransport = await Promise.all(transports.map(async (transport) => {
|
|
2396
|
+
const transportPeers = await transport.getPeers();
|
|
2397
|
+
return {
|
|
2398
|
+
transport,
|
|
2399
|
+
peers: transportPeers
|
|
2400
|
+
};
|
|
2401
|
+
}));
|
|
2402
|
+
await Promise.all(peersByTransport.map(({ transport }) => transport.removeAllPeers()));
|
|
2403
|
+
const peersToRemove = peersByTransport.flatMap(({ peers: transportPeers }) => transportPeers);
|
|
2404
|
+
await this.removeAccountsForPeers(peersToRemove);
|
|
2405
|
+
// If cleanup already removed peers, a late notify upgrade has nothing left to notify.
|
|
2406
|
+
const sendDisconnectToPeers = this._disconnectNotifyPeers;
|
|
2407
|
+
if (!sendDisconnectToPeers) {
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
await Promise.allSettled(this.getUniquePeerNotifications(peersByTransport).map(({ transport, peer }) => this.sendDisconnectToPeer(peer, transport)));
|
|
2411
|
+
}
|
|
2412
|
+
getUniquePeerNotifications(peersByTransport) {
|
|
2413
|
+
const notifications = [];
|
|
2414
|
+
const seen = new Set();
|
|
2415
|
+
peersByTransport.forEach(({ transport, peers: transportPeers }) => {
|
|
2416
|
+
transportPeers.forEach((peer) => {
|
|
2417
|
+
const key = this.getPeerNotificationKey(transport, peer);
|
|
2418
|
+
if (seen.has(key)) {
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
seen.add(key);
|
|
2422
|
+
notifications.push({ transport, peer });
|
|
2423
|
+
});
|
|
2424
|
+
});
|
|
2425
|
+
return notifications;
|
|
2426
|
+
}
|
|
2427
|
+
getPeerNotificationKey(transport, peer) {
|
|
2428
|
+
return [transport.type, peer.publicKey, peer.senderId, peer.id].filter(Boolean).join(':');
|
|
2429
|
+
}
|
|
2430
|
+
async disconnectResolvedTransports(transports) {
|
|
2431
|
+
const results = await Promise.allSettled(transports.map(async (transport) => {
|
|
2432
|
+
if (this.isTransportConnected(transport)) {
|
|
2433
|
+
await transport.disconnect();
|
|
2434
|
+
}
|
|
2435
|
+
if (this.hasClientCleanup(transport)) {
|
|
2436
|
+
await transport.doClientCleanup();
|
|
2437
|
+
}
|
|
2438
|
+
}));
|
|
2439
|
+
results.forEach((result) => {
|
|
2440
|
+
if (result.status === 'rejected') {
|
|
2441
|
+
logger.warn('disconnectResolvedTransports', result.reason);
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
hasClientCleanup(transport) {
|
|
2446
|
+
const transportWithClientCleanup = transport;
|
|
2447
|
+
return typeof transportWithClientCleanup.doClientCleanup === 'function';
|
|
2448
|
+
}
|
|
2449
|
+
isTransportConnected(transport) {
|
|
2450
|
+
return transport.connectionStatus !== TransportStatus.NOT_CONNECTED;
|
|
1908
2451
|
}
|
|
1909
2452
|
/**
|
|
1910
2453
|
* Adds a requests to the "openRequests" set so we know what messages have already been answered/handled.
|
|
@@ -1914,7 +2457,64 @@ export class DAppClient extends Client {
|
|
|
1914
2457
|
*/
|
|
1915
2458
|
addOpenRequest(id, promise) {
|
|
1916
2459
|
logger.log('addOpenRequest', this.name, `adding request ${id} and waiting for answer`);
|
|
2460
|
+
this.clearOpenRequestTimeout(id);
|
|
2461
|
+
this.openRequests.delete(id);
|
|
1917
2462
|
this.openRequests.set(id, promise);
|
|
2463
|
+
this.addOpenRequestTimeout(id, promise);
|
|
2464
|
+
}
|
|
2465
|
+
addOpenRequestTimeout(id, promise) {
|
|
2466
|
+
if (id === SESSION_UPDATE_REQUEST_ID) {
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
const timeout = setTimeout(() => {
|
|
2473
|
+
if (this.openRequests.get(id) !== promise) {
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const errorResponse = {
|
|
2477
|
+
id,
|
|
2478
|
+
type: BeaconMessageType.Error,
|
|
2479
|
+
errorType: BeaconErrorType.PEER_UNREACHABLE,
|
|
2480
|
+
senderId: '',
|
|
2481
|
+
version: '2'
|
|
2482
|
+
};
|
|
2483
|
+
promise.reject(errorResponse);
|
|
2484
|
+
this.clearOpenRequest(id);
|
|
2485
|
+
this.events.emit(BeaconEvent.CHANNEL_CLOSED).catch((emitError) => {
|
|
2486
|
+
logger.error('addOpenRequestTimeout', emitError);
|
|
2487
|
+
});
|
|
2488
|
+
}, this.requestTimeoutMs);
|
|
2489
|
+
this.openRequestTimeouts.set(id, timeout);
|
|
2490
|
+
}
|
|
2491
|
+
clearOpenRequest(id) {
|
|
2492
|
+
this.clearOpenRequestTimeout(id);
|
|
2493
|
+
this.openRequests.delete(id);
|
|
2494
|
+
this.openRequestsOtherTabs.delete(id);
|
|
2495
|
+
}
|
|
2496
|
+
clearOpenRequestTimeout(id) {
|
|
2497
|
+
const timeout = this.openRequestTimeouts.get(id);
|
|
2498
|
+
if (timeout) {
|
|
2499
|
+
clearTimeout(timeout);
|
|
2500
|
+
this.openRequestTimeouts.delete(id);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
clearAllOpenRequestTimeouts() {
|
|
2504
|
+
this.openRequestTimeouts.forEach((timeout) => clearTimeout(timeout));
|
|
2505
|
+
this.openRequestTimeouts.clear();
|
|
2506
|
+
}
|
|
2507
|
+
abortOpenRequests() {
|
|
2508
|
+
Array.from(this.openRequests.entries())
|
|
2509
|
+
.filter(([id]) => id !== SESSION_UPDATE_REQUEST_ID)
|
|
2510
|
+
.forEach(([id, promise]) => {
|
|
2511
|
+
promise.reject(this.createAbortedError(id));
|
|
2512
|
+
this.clearOpenRequest(id);
|
|
2513
|
+
});
|
|
2514
|
+
this.openRequests.clear();
|
|
2515
|
+
this.openRequestsOtherTabs.clear();
|
|
2516
|
+
this.acknowledgedRequests.clear();
|
|
2517
|
+
this.clearAllOpenRequestTimeouts();
|
|
1918
2518
|
}
|
|
1919
2519
|
async sendNotificationWithAccessToken(notification) {
|
|
1920
2520
|
const { url, recipient, title, body, payload, protocolIdentifier, accessToken } = notification;
|