-
Notifications
You must be signed in to change notification settings - Fork 5.1k
/
Copy pathui.js
376 lines (311 loc) · 11.9 KB
/
ui.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// Disabled to allow setting up initial state hooks first
// This import sets up global functions required for Sentry to function.
// It must be run first in case an error is thrown later during initialization.
import './lib/setup-initial-state-hooks';
import '../../development/wdyr';
// dev only, "react-devtools" import is skipped in prod builds
import 'react-devtools';
import PortStream from 'extension-port-stream';
import browser from 'webextension-polyfill';
import Eth from '@metamask/ethjs';
import EthQuery from '@metamask/eth-query';
import StreamProvider from 'web3-stream-provider';
import log from 'loglevel';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import launchMetaMaskUi, { updateBackgroundConnection } from '../../ui';
import {
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_FIREFOX,
} from '../../shared/constants/app';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
import { SUPPORT_LINK } from '../../shared/lib/ui-utils';
import { getErrorHtml } from '../../shared/lib/error-utils';
import { endTrace, trace, TraceName } from '../../shared/lib/trace';
import ExtensionPlatform from './platforms/extension';
import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType, getPlatform } from './lib/util';
import metaRPCClientFactory from './lib/metaRPCClientFactory';
const PHISHING_WARNING_PAGE_TIMEOUT = 1 * 1000; // 1 Second
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
const METHOD_START_UI_SYNC = 'startUISync';
const container = document.getElementById('app-content');
let extensionPort;
let isUIInitialised = false;
/**
* An error thrown if the phishing warning page takes too long to load.
*/
class PhishingWarningPageTimeoutError extends Error {
constructor() {
super('Timeout failed');
}
}
start().catch(log.error);
async function start() {
const startTime = performance.now();
const traceContext = trace({
name: TraceName.UIStartup,
startTime: performance.timeOrigin,
});
trace({
name: TraceName.LoadScripts,
startTime: performance.timeOrigin,
parentContext: traceContext,
});
endTrace({
name: TraceName.LoadScripts,
timestamp: performance.timeOrigin + startTime,
});
// create platform global
global.platform = new ExtensionPlatform();
// identify window type (popup, notification)
const windowType = getEnvironmentType();
// setup stream to background
extensionPort = browser.runtime.connect({ name: windowType });
let connectionStream = new PortStream(extensionPort);
const activeTab = await queryCurrentActiveTab(windowType);
/*
* In case of MV3 the issue of blank screen was very frequent, it is caused by UI initialising before background is ready to send state.
* Code below ensures that UI is rendered only after "CONNECTION_READY" or "startUISync"
* messages are received thus the background is ready, and ensures that streams and
* phishing warning page load only after the "startUISync" message is received.
* In case the UI is already rendered, only update the streams.
*/
const messageListener = async (message) => {
const method = message?.data?.method;
if (method !== METHOD_START_UI_SYNC) {
return;
}
endTrace({ name: TraceName.BackgroundConnect });
if (isManifestV3 && isUIInitialised) {
// Currently when service worker is revived we create new streams
// in later version we might try to improve it by reviving same streams.
updateUiStreams(connectionStream);
} else {
await initializeUiWithTab(
activeTab,
connectionStream,
windowType,
traceContext,
);
}
if (isManifestV3) {
await loadPhishingWarningPage();
} else {
extensionPort.onMessage.removeListener(messageListener);
}
};
if (isManifestV3) {
// resetExtensionStreamAndListeners takes care to remove listeners from closed streams
// it also creates new streams and attaches event listeners to them
const resetExtensionStreamAndListeners = () => {
extensionPort.onMessage.removeListener(messageListener);
extensionPort.onDisconnect.removeListener(
resetExtensionStreamAndListeners,
);
extensionPort = browser.runtime.connect({ name: windowType });
connectionStream = new PortStream(extensionPort);
extensionPort.onMessage.addListener(messageListener);
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
};
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
}
trace({
name: TraceName.BackgroundConnect,
parentContext: traceContext,
});
extensionPort.onMessage.addListener(messageListener);
}
/**
* Load the phishing warning page temporarily to ensure the service
* worker has been registered, so that the warning page works offline.
*/
async function loadPhishingWarningPage() {
// Check session storage for whether we've already initialized the phishing warning
// service worker in this browser session and do not attempt to re-initialize if so.
const phishingSWMemoryFetch = await browser.storage.session.get(
PHISHING_WARNING_SW_STORAGE_KEY,
);
if (phishingSWMemoryFetch[PHISHING_WARNING_SW_STORAGE_KEY]) {
return;
}
const currentPlatform = getPlatform();
let iframe;
try {
const extensionStartupPhishingPageUrl = new URL(
process.env.PHISHING_WARNING_PAGE_URL,
);
// The `extensionStartup` hash signals to the phishing warning page that it should not bother
// setting up streams for user interaction. Otherwise this page load would cause a console
// error.
extensionStartupPhishingPageUrl.hash = '#extensionStartup';
iframe = window.document.createElement('iframe');
iframe.setAttribute('src', extensionStartupPhishingPageUrl.href);
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
// Create "deferred Promise" to allow passing resolve/reject to event handlers
let deferredResolve;
let deferredReject;
const loadComplete = new Promise((resolve, reject) => {
deferredResolve = resolve;
deferredReject = reject;
});
// The load event is emitted once loading has completed, even if the loading failed.
// If loading failed we can't do anything about it, so we don't need to check.
iframe.addEventListener('load', deferredResolve);
// This step initiates the page loading.
window.document.body.appendChild(iframe);
// This timeout ensures that this iframe gets cleaned up in a reasonable
// timeframe, and ensures that the "initialization complete" message
// doesn't get delayed too long.
setTimeout(
() => deferredReject(new PhishingWarningPageTimeoutError()),
PHISHING_WARNING_PAGE_TIMEOUT,
);
await loadComplete;
// store a flag in sessions storage that we've already loaded the service worker
// and don't need to try again
if (currentPlatform === PLATFORM_FIREFOX) {
// Firefox does not yet support the storage.session API introduced in MV3
// Tracked here: https://bugzilla.mozilla.org/show_bug.cgi?id=1687778
console.error(
'Firefox does not support required MV3 APIs: Phishing warning page iframe and service worker will reload each page refresh',
);
} else {
browser.storage.session.set({
[PHISHING_WARNING_SW_STORAGE_KEY]: true,
});
}
} catch (error) {
if (error instanceof PhishingWarningPageTimeoutError) {
console.warn(
'Phishing warning page timeout; page not guaranteed to work offline.',
);
} else {
console.error('Failed to initialize phishing warning page', error);
}
} finally {
if (iframe) {
iframe.remove();
}
}
}
async function initializeUiWithTab(
tab,
connectionStream,
windowType,
traceContext,
) {
try {
const store = await initializeUi(tab, connectionStream, traceContext);
endTrace({ name: TraceName.UIStartup });
isUIInitialised = true;
if (process.env.IN_TEST) {
window.document?.documentElement?.classList.add('controller-loaded');
}
const state = store.getState();
const { metamask: { completedOnboarding } = {} } = state;
if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) {
global.platform.openExtensionInBrowser();
}
} catch (err) {
displayCriticalError('troubleStarting', err);
}
}
// Function to update new backgroundConnection in the UI
function updateUiStreams(connectionStream) {
const backgroundConnection = connectToAccountManager(connectionStream);
updateBackgroundConnection(backgroundConnection);
}
async function queryCurrentActiveTab(windowType) {
// Shims the activeTab for E2E test runs only if the
// "activeTabOrigin" querystring key=value is set
if (process.env.IN_TEST) {
const searchParams = new URLSearchParams(window.location.search);
const mockUrl = searchParams.get('activeTabOrigin');
if (mockUrl) {
const { origin, protocol } = new URL(mockUrl);
const returnUrl = {
id: 'mock-site',
title: 'Mock Site',
url: mockUrl,
origin,
protocol,
};
return returnUrl;
}
}
// At the time of writing we only have the `activeTab` permission which means
// that this query will only succeed in the popup context (i.e. after a "browserAction")
if (windowType !== ENVIRONMENT_TYPE_POPUP) {
return {};
}
const tabs = await browser.tabs
.query({ active: true, currentWindow: true })
.catch((e) => {
checkForLastErrorAndLog() || log.error(e);
});
const [activeTab] = tabs;
const { id, title, url } = activeTab;
const { origin, protocol } = url ? new URL(url) : {};
if (!origin || origin === 'null') {
return {};
}
return { id, title, origin, protocol, url };
}
async function initializeUi(activeTab, connectionStream, traceContext) {
const backgroundConnection = connectToAccountManager(connectionStream);
return await launchMetaMaskUi({
activeTab,
container,
backgroundConnection,
traceContext,
});
}
async function displayCriticalError(errorKey, err, metamaskState) {
const html = await getErrorHtml(errorKey, SUPPORT_LINK, metamaskState);
container.innerHTML = html;
const button = document.getElementById('critical-error-button');
button?.addEventListener('click', (_) => {
browser.runtime.reload();
});
log.error(err.stack);
throw err;
}
/**
* Establishes a connection to the background and a Web3 provider
*
* @param {PortDuplexStream} connectionStream - PortStream instance establishing a background connection
*/
function connectToAccountManager(connectionStream) {
const mx = setupMultiplex(connectionStream);
const controllerConnectionStream = mx.createStream('controller');
const backgroundConnection = setupControllerConnection(
controllerConnectionStream,
);
setupWeb3Connection(mx.createStream('provider'));
return backgroundConnection;
}
/**
* Establishes a streamed connection to a Web3 provider
*
* @param {PortDuplexStream} connectionStream - PortStream instance establishing a background connection
*/
function setupWeb3Connection(connectionStream) {
const providerStream = new StreamProvider();
providerStream.pipe(connectionStream).pipe(providerStream);
connectionStream.on('error', console.error.bind(console));
providerStream.on('error', console.error.bind(console));
global.ethereumProvider = providerStream;
global.ethQuery = new EthQuery(providerStream);
global.eth = new Eth(providerStream);
}
/**
* Establishes a streamed connection to the background account manager
*
* @param {PortDuplexStream} controllerConnectionStream - PortStream instance establishing a background connection
*/
function setupControllerConnection(controllerConnectionStream) {
return metaRPCClientFactory(controllerConnectionStream);
}