mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-25 19:58:11 +01:00
507c2cb475
* Capture Sentry errors prior to initialization Sentry errors captured before/during the wallet initialization are currently not captured because we don't have the controller state yet to determine whether the user has consented. The Sentry setup has been updated to check the persisted state for whether the user has consented, as a fallback in case the controller state hasn't been initialized yet. This ensures that we capture errors during initialization if the user has opted in. * Always await async check for whether the user has opted in * Remove unused import * Update JSDoc return type * Remove unused driver method * Fix metametrics controller unit tests * Fix e2e tests * Fix e2e test on Firefox * Start session upon install rather than toggle
438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
// dev only, "react-devtools" import is skipped in prod builds
|
|
import 'react-devtools';
|
|
|
|
// This import sets up a global function required for Sentry to function.
|
|
// It must be run first in case an error is thrown later during initialization.
|
|
import './lib/setup-persisted-state-hook';
|
|
|
|
import PortStream from 'extension-port-stream';
|
|
import browser from 'webextension-polyfill';
|
|
|
|
import Eth from 'ethjs';
|
|
import EthQuery from 'eth-query';
|
|
import StreamProvider from 'web3-stream-provider';
|
|
import log from 'loglevel';
|
|
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,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
registerDesktopErrorActions,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} from '../../shared/lib/error-utils';
|
|
import ExtensionPlatform from './platforms/extension';
|
|
import { setupMultiplex } from './lib/stream-utils';
|
|
import { getEnvironmentType, getPlatform } from './lib/util';
|
|
import metaRPCClientFactory from './lib/metaRPCClientFactory';
|
|
|
|
const container = document.getElementById('app-content');
|
|
|
|
const ONE_SECOND_IN_MILLISECONDS = 1_000;
|
|
|
|
// Service Worker Keep Alive Message Constants
|
|
const WORKER_KEEP_ALIVE_INTERVAL = ONE_SECOND_IN_MILLISECONDS;
|
|
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
|
|
const ACK_KEEP_ALIVE_WAIT_TIME = 60_000; // 1 minute
|
|
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
|
|
|
|
// Timeout for initializing phishing warning page.
|
|
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
|
|
|
|
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
|
|
|
|
let lastMessageReceivedTimestamp = Date.now();
|
|
|
|
let extensionPort;
|
|
let ackTimeoutToDisplayError;
|
|
|
|
/*
|
|
* As long as UI is open it will keep sending messages to service worker
|
|
* In service worker as this message is received
|
|
* if service worker is inactive it is reactivated and script re-loaded
|
|
* Time has been kept to 1000ms but can be reduced for even faster re-activation of service worker
|
|
*/
|
|
if (isManifestV3) {
|
|
// Checking for SW aliveness (or stuckness) flow
|
|
// 1. Check if we have an extensionPort, if yes
|
|
// 2a. Send a keep alive message to the background via extensionPort
|
|
// 2b. Add a listener to it (if not already added)
|
|
// 3a. Set a timeout to check if we have received an ACK from background
|
|
// 3b. If we have not received an ACK within ACK_KEEP_ALIVE_WAIT_TIME,
|
|
// we know the background is stuck or dead
|
|
// 4. If we recieve an ACK_KEEP_ALIVE_MESSAGE from the service worker, we know it is alive
|
|
|
|
const ackKeepAliveListener = (message) => {
|
|
if (message.name === ACK_KEEP_ALIVE_MESSAGE) {
|
|
lastMessageReceivedTimestamp = Date.now();
|
|
clearTimeout(ackTimeoutToDisplayError);
|
|
}
|
|
};
|
|
|
|
const keepAliveInterval = setInterval(() => {
|
|
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
|
|
|
|
if (extensionPort !== null && extensionPort !== undefined) {
|
|
extensionPort.postMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
|
|
|
|
if (extensionPort.onMessage.hasListener(ackKeepAliveListener) === false) {
|
|
extensionPort.onMessage.addListener(ackKeepAliveListener);
|
|
}
|
|
}
|
|
|
|
ackTimeoutToDisplayError = setTimeout(() => {
|
|
if (
|
|
Date.now() - lastMessageReceivedTimestamp >
|
|
ACK_KEEP_ALIVE_WAIT_TIME
|
|
) {
|
|
clearInterval(keepAliveInterval);
|
|
displayCriticalError(
|
|
'somethingIsWrong',
|
|
new Error("Something's gone wrong. Try reloading the page."),
|
|
);
|
|
}
|
|
}, ACK_KEEP_ALIVE_WAIT_TIME);
|
|
}, WORKER_KEEP_ALIVE_INTERVAL);
|
|
}
|
|
|
|
start().catch(log.error);
|
|
|
|
async function start() {
|
|
// create platform global
|
|
global.platform = new ExtensionPlatform();
|
|
|
|
// identify window type (popup, notification)
|
|
const windowType = getEnvironmentType();
|
|
|
|
let isUIInitialised = false;
|
|
|
|
// setup stream to background
|
|
extensionPort = browser.runtime.connect({ name: windowType });
|
|
let connectionStream = new PortStream(extensionPort);
|
|
|
|
const activeTab = await queryCurrentActiveTab(windowType);
|
|
|
|
let loadPhishingWarningPage;
|
|
|
|
if (isManifestV3) {
|
|
/*
|
|
* 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) => {
|
|
if (message?.data?.method === 'startUISync') {
|
|
if (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();
|
|
} else {
|
|
initializeUiWithTab(activeTab);
|
|
}
|
|
await loadPhishingWarningPage();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* An error thrown if the phishing warning page takes too long to load.
|
|
*/
|
|
class PhishingWarningPageTimeoutError extends Error {
|
|
constructor() {
|
|
super('Timeout failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the phishing warning page temporarily to ensure the service
|
|
* worker has been registered, so that the warning page works offline.
|
|
*/
|
|
loadPhishingWarningPage = async function () {
|
|
// 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();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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,
|
|
);
|
|
|
|
// message below will try to activate service worker
|
|
// in MV3 is likely that reason of stream closing is service worker going in-active
|
|
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
|
|
|
|
extensionPort = browser.runtime.connect({ name: windowType });
|
|
connectionStream = new PortStream(extensionPort);
|
|
extensionPort.onMessage.addListener(messageListener);
|
|
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
|
|
};
|
|
|
|
extensionPort.onMessage.addListener(messageListener);
|
|
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
|
|
} else {
|
|
initializeUiWithTab(activeTab);
|
|
}
|
|
|
|
function initializeUiWithTab(tab) {
|
|
initializeUi(
|
|
tab,
|
|
connectionStream,
|
|
(
|
|
err,
|
|
store,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
backgroundConnection,
|
|
///: END:ONLY_INCLUDE_IN
|
|
) => {
|
|
if (err) {
|
|
// if there's an error, store will be = metamaskState
|
|
displayCriticalError(
|
|
'troubleStarting',
|
|
err,
|
|
store,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
backgroundConnection,
|
|
///: END:ONLY_INCLUDE_IN
|
|
);
|
|
return;
|
|
}
|
|
isUIInitialised = true;
|
|
|
|
const state = store.getState();
|
|
const { metamask: { completedOnboarding } = {} } = state;
|
|
|
|
if (
|
|
!completedOnboarding &&
|
|
windowType !== ENVIRONMENT_TYPE_FULLSCREEN
|
|
) {
|
|
global.platform.openExtensionInBrowser();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
// Function to update new backgroundConnection in the UI
|
|
function updateUiStreams() {
|
|
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
|
|
if (err) {
|
|
displayCriticalError(
|
|
'troubleStarting',
|
|
err,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
undefined,
|
|
backgroundConnection,
|
|
///: END:ONLY_INCLUDE_IN
|
|
);
|
|
return;
|
|
}
|
|
|
|
updateBackgroundConnection(backgroundConnection);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function queryCurrentActiveTab(windowType) {
|
|
// 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 };
|
|
}
|
|
|
|
function initializeUi(activeTab, connectionStream, cb) {
|
|
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
|
|
if (err) {
|
|
cb(
|
|
err,
|
|
null,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
backgroundConnection,
|
|
///: END:ONLY_INCLUDE_IN
|
|
);
|
|
return;
|
|
}
|
|
|
|
launchMetaMaskUi(
|
|
{
|
|
activeTab,
|
|
container,
|
|
backgroundConnection,
|
|
},
|
|
cb,
|
|
);
|
|
});
|
|
}
|
|
|
|
async function displayCriticalError(
|
|
errorKey,
|
|
err,
|
|
metamaskState,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
backgroundConnection,
|
|
///: END:ONLY_INCLUDE_IN
|
|
) {
|
|
const html = await getErrorHtml(
|
|
errorKey,
|
|
SUPPORT_LINK,
|
|
metamaskState,
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
err,
|
|
///: END:ONLY_INCLUDE_IN
|
|
);
|
|
|
|
container.innerHTML = html;
|
|
|
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
|
registerDesktopErrorActions(backgroundConnection, browser);
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
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
|
|
* @param {Function} cb - Called when controller connection is established
|
|
*/
|
|
function connectToAccountManager(connectionStream, cb) {
|
|
const mx = setupMultiplex(connectionStream);
|
|
const controllerConnectionStream = mx.createStream('controller');
|
|
setupControllerConnection(controllerConnectionStream, cb);
|
|
setupWeb3Connection(mx.createStream('provider'));
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param {Function} cb - Called when the remote account manager connection is established
|
|
*/
|
|
function setupControllerConnection(controllerConnectionStream, cb) {
|
|
const backgroundRPC = metaRPCClientFactory(controllerConnectionStream);
|
|
cb(null, backgroundRPC);
|
|
}
|