From 25082ae272afc76ba27d4d79d77812d544a978d1 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 26 May 2022 10:18:23 +0530 Subject: [PATCH] Adding flag for MV3 (#14762) --- app/home.html | 2 +- app/manifest/{ => v2}/_base.json | 0 app/manifest/{ => v2}/brave.json | 0 app/manifest/{ => v2}/chrome.json | 0 app/manifest/{ => v2}/firefox.json | 0 app/manifest/{ => v2}/opera.json | 0 app/manifest/v3/_base.json | 81 ++++++++++++++++++++++++++++ app/manifest/v3/brave.json | 1 + app/manifest/v3/chrome.json | 7 +++ app/manifest/v3/firefox.json | 26 +++++++++ app/manifest/v3/opera.json | 9 ++++ app/scripts/app-init.js | 55 +++++++++++++++++++ app/scripts/background.js | 65 ++++++++++++++++------ app/scripts/contentscript.js | 9 +++- app/scripts/ui.js | 16 +++++- development/build/manifest.js | 6 ++- development/build/scripts.js | 62 ++++++++++++++++++++- lavamoat/browserify/main/policy.json | 38 +++++++++++++ package.json | 1 + shared/modules/mv3.utils.js | 4 ++ 20 files changed, 361 insertions(+), 21 deletions(-) rename app/manifest/{ => v2}/_base.json (100%) rename app/manifest/{ => v2}/brave.json (100%) rename app/manifest/{ => v2}/chrome.json (100%) rename app/manifest/{ => v2}/firefox.json (100%) rename app/manifest/{ => v2}/opera.json (100%) create mode 100644 app/manifest/v3/_base.json create mode 100644 app/manifest/v3/brave.json create mode 100644 app/manifest/v3/chrome.json create mode 100644 app/manifest/v3/firefox.json create mode 100644 app/manifest/v3/opera.json create mode 100644 app/scripts/app-init.js create mode 100644 shared/modules/mv3.utils.js diff --git a/app/home.html b/app/home.html index 97334c73c..0686d4034 100644 --- a/app/home.html +++ b/app/home.html @@ -8,7 +8,7 @@ -
+
Loading...
diff --git a/app/manifest/_base.json b/app/manifest/v2/_base.json similarity index 100% rename from app/manifest/_base.json rename to app/manifest/v2/_base.json diff --git a/app/manifest/brave.json b/app/manifest/v2/brave.json similarity index 100% rename from app/manifest/brave.json rename to app/manifest/v2/brave.json diff --git a/app/manifest/chrome.json b/app/manifest/v2/chrome.json similarity index 100% rename from app/manifest/chrome.json rename to app/manifest/v2/chrome.json diff --git a/app/manifest/firefox.json b/app/manifest/v2/firefox.json similarity index 100% rename from app/manifest/firefox.json rename to app/manifest/v2/firefox.json diff --git a/app/manifest/opera.json b/app/manifest/v2/opera.json similarity index 100% rename from app/manifest/opera.json rename to app/manifest/v2/opera.json diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json new file mode 100644 index 000000000..6836caafb --- /dev/null +++ b/app/manifest/v3/_base.json @@ -0,0 +1,81 @@ +{ + "action": { + "default_icon": { + "16": "images/icon-16.png", + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" + }, + "default_title": "MetaMask", + "default_popup": "popup.html" + }, + "author": "https://metamask.io", + "background": { + "service_worker": "app-init.js" + }, + "commands": { + "_execute_browser_action": { + "suggested_key": { + "windows": "Alt+Shift+M", + "mac": "Alt+Shift+M", + "chromeos": "Alt+Shift+M", + "linux": "Alt+Shift+M" + } + } + }, + "content_scripts": [ + { + "matches": ["file://*/*", "http://*/*", "https://*/*"], + "js": [ + "disable-console.js", + "globalthis.js", + "lockdown-install.js", + "lockdown-run.js", + "lockdown-more.js", + "contentscript.js" + ], + "run_at": "document_start", + "all_frames": true + }, + { + "matches": ["*://connect.trezor.io/*/popup.html"], + "js": ["vendor/trezor/content-script.js"] + } + ], + "default_locale": "en", + "description": "__MSG_appDescription__", + "icons": { + "16": "images/icon-16.png", + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "48": "images/icon-48.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" + }, + "manifest_version": 3, + "name": "__MSG_appName__", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ], + "short_name": "__MSG_appName__", + "web_accessible_resources": [ + { + "resources": ["inpage.js", "phishing.html"], + "matches": ["http://*/*", "https://*/*"] + } + ] +} diff --git a/app/manifest/v3/brave.json b/app/manifest/v3/brave.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/app/manifest/v3/brave.json @@ -0,0 +1 @@ +{} diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json new file mode 100644 index 000000000..e4bb01cdd --- /dev/null +++ b/app/manifest/v3/chrome.json @@ -0,0 +1,7 @@ +{ + "externally_connectable": { + "matches": ["https://metamask.io/*"], + "ids": ["*"] + }, + "minimum_chrome_version": "66" +} diff --git a/app/manifest/v3/firefox.json b/app/manifest/v3/firefox.json new file mode 100644 index 000000000..918311d54 --- /dev/null +++ b/app/manifest/v3/firefox.json @@ -0,0 +1,26 @@ +{ + "applications": { + "gecko": { + "id": "webextension@metamask.io", + "strict_min_version": "68.0" + } + }, + "background": { + "page": "background.html", + "persistent": true + }, + "browser_action": { + "default_icon": { + "16": "images/icon-16.png", + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" + }, + "default_title": "MetaMask", + "default_popup": "popup.html" + }, + "manifest_version": 2 +} diff --git a/app/manifest/v3/opera.json b/app/manifest/v3/opera.json new file mode 100644 index 000000000..6cfefd402 --- /dev/null +++ b/app/manifest/v3/opera.json @@ -0,0 +1,9 @@ +{ + "permissions": [ + "storage", + "tabs", + "clipboardWrite", + "clipboardRead", + "http://localhost:8545/" + ] +} diff --git a/app/scripts/app-init.js b/app/scripts/app-init.js new file mode 100644 index 000000000..6d2bf2d65 --- /dev/null +++ b/app/scripts/app-init.js @@ -0,0 +1,55 @@ +// eslint-disable-next-line import/unambiguous +function tryImport(...fileNames) { + try { + // eslint-disable-next-line + importScripts(...fileNames); + return true; + } catch (e) { + console.error(e); + return false; + } +} + +function importAllScripts() { + const startImportScriptsTime = Date.now(); + // applyLavaMoat has been hard coded to "true" as + // tryImport('./runtime-cjs.js') is giving issue with XMLHttpRequest object which is not avaialble to service worker. + // we need to dynamically inject values of applyLavaMoat once this is fixed. + const applyLavaMoat = true; + + tryImport('./globalthis.js'); + tryImport('./sentry-install.js'); + + if (applyLavaMoat) { + tryImport('./runtime-lavamoat.js'); + tryImport('./lockdown-more.js'); + tryImport('./policy-load.js'); + } else { + tryImport('./lockdown-install.js'); + tryImport('./lockdown-more.js'); + tryImport('./lockdown-run.js'); + tryImport('./runtime-cjs.js'); + } + + const fileList = [ + // The list of files is injected at build time by replacing comment below with comma separated strings of file names + /** FILE NAMES */ + ]; + + fileList.forEach((fileName) => tryImport(fileName)); + + // for performance metrics/reference + console.log( + `SCRIPTS IMPORT COMPLETE in Seconds: ${ + (Date.now() - startImportScriptsTime) / 1000 + }`, + ); +} + +// Placing script import call here ensures that scripts are inported each time service worker is activated. +importAllScripts(); + +/** + * An open issue is changes in this file break during hot reloading. Reason is dynamic injection of "FILE NAMES". + * Developers need to restart local server if they change this file. + */ diff --git a/app/scripts/background.js b/app/scripts/background.js index ae85d781b..119af9e87 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -23,6 +23,7 @@ import { REJECT_NOTFICIATION_CLOSE, REJECT_NOTFICIATION_CLOSE_SIG, } from '../../shared/constants/metametrics'; +import { isManifestV3 } from '../../shared/modules/mv3.utils'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -45,6 +46,14 @@ import { getPlatform } from './lib/util'; const { sentry } = global; const firstTimeState = { ...rawFirstTimeState }; +const metamaskInternalProcessHash = { + [ENVIRONMENT_TYPE_POPUP]: true, + [ENVIRONMENT_TYPE_NOTIFICATION]: true, + [ENVIRONMENT_TYPE_FULLSCREEN]: true, +}; + +const metamaskBlockedPorts = ['trezor-connect']; + log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info'); const platform = new ExtensionPlatform(); @@ -67,8 +76,23 @@ if (inTest || process.env.METAMASK_DEBUG) { global.metamaskGetState = localStore.get.bind(localStore); } -// initialization flow -initialize().catch(log.error); +/** + * In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised. + * Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated. + */ + +const initApp = async (remotePort) => { + browser.runtime.onConnect.removeListener(initApp); + await initialize(remotePort); + log.info('MetaMask initialization complete.'); +}; + +if (isManifestV3()) { + browser.runtime.onConnect.addListener(initApp); +} else { + // initialization flow + initialize().catch(log.error); +} /** * @typedef {import('../../shared/constants/transaction').TransactionMeta} TransactionMeta @@ -128,12 +152,13 @@ initialize().catch(log.error); /** * Initializes the MetaMask controller, and sets up all platform configuration. * + * @param {string} remotePort - remote application port connecting to extension. * @returns {Promise} Setup complete. */ -async function initialize() { +async function initialize(remotePort) { const initState = await loadStateFromPersistence(); const initLangCode = await getFirstPreferredLangCode(); - await setupController(initState, initLangCode); + await setupController(initState, initLangCode, remotePort); log.info('MetaMask initialization complete.'); } @@ -205,9 +230,10 @@ async function loadStateFromPersistence() { * * @param {Object} initState - The initial state to start the controller with, matches the state that is emitted from the controller. * @param {string} initLangCode - The region code for the language preferred by the current user. + * @param {string} remoteSourcePort - remote application port connecting to extension. * @returns {Promise} After setup is complete. */ -function setupController(initState, initLangCode) { +function setupController(initState, initLangCode, remoteSourcePort) { // // MetaMask Controller // @@ -294,17 +320,13 @@ function setupController(initState, initLangCode) { // // connect to other contexts // + if (isManifestV3() && remoteSourcePort) { + connectRemote(remoteSourcePort); + } + browser.runtime.onConnect.addListener(connectRemote); browser.runtime.onConnectExternal.addListener(connectExternal); - const metamaskInternalProcessHash = { - [ENVIRONMENT_TYPE_POPUP]: true, - [ENVIRONMENT_TYPE_NOTIFICATION]: true, - [ENVIRONMENT_TYPE_FULLSCREEN]: true, - }; - - const metamaskBlockedPorts = ['trezor-connect']; - const isClientOpenStatus = () => { return ( popupIsOpen || @@ -368,6 +390,13 @@ function setupController(initState, initLangCode) { controller.isClientOpen = true; controller.setupTrustedCommunication(portStream, remotePort.sender); + if (isManifestV3()) { + // Message below if captured by UI code in app/scripts/ui.js which will trigger UI initialisation + // This ensures that UI is initialised only after background is ready + // It fixes the issue of blank screen coming when extension is loaded, the issue is very frequent in MV3 + remotePort.postMessage({ name: 'CONNECTION_READY' }); + } + if (processName === ENVIRONMENT_TYPE_POPUP) { popupIsOpen = true; endOfStream(portStream, () => { @@ -480,8 +509,14 @@ function setupController(initState, initLangCode) { if (count) { label = String(count); } - browser.browserAction.setBadgeText({ text: label }); - browser.browserAction.setBadgeBackgroundColor({ color: '#037DD6' }); + // browserAction has been replaced by action in MV3 + if (isManifestV3()) { + browser.action.setBadgeText({ text: label }); + browser.action.setBadgeBackgroundColor({ color: '#037DD6' }); + } else { + browser.browserAction.setBadgeText({ text: label }); + browser.browserAction.setBadgeBackgroundColor({ color: '#037DD6' }); + } } function getUnapprovedTransactionCount() { diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 037f3de14..a3a2dd3dd 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -6,6 +6,8 @@ import browser from 'webextension-polyfill'; import PortStream from 'extension-port-stream'; import { obj as createThoughStream } from 'through2'; +import { isManifestV3 } from '../../shared/modules/mv3.utils'; + // These require calls need to use require to be statically recognized by browserify const fs = require('fs'); const path = require('path'); @@ -42,7 +44,12 @@ function injectScript(content) { const container = document.head || document.documentElement; const scriptTag = document.createElement('script'); scriptTag.setAttribute('async', 'false'); - scriptTag.textContent = content; + // Inline scripts do not work in MV3 due to more strict security policy + if (isManifestV3()) { + scriptTag.setAttribute('src', browser.runtime.getURL('inpage.js')); + } else { + scriptTag.textContent = content; + } container.insertBefore(scriptTag, container.children[0]); container.removeChild(scriptTag); } catch (error) { diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 1c4a3fc1c..27e464ef2 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -16,6 +16,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP, } from '../../shared/constants/app'; +import { isManifestV3 } from '../../shared/modules/mv3.utils'; import ExtensionPlatform from './platforms/extension'; import { setupMultiplex } from './lib/stream-utils'; import { getEnvironmentType } from './lib/util'; @@ -35,7 +36,20 @@ async function start() { const connectionStream = new PortStream(extensionPort); const activeTab = await queryCurrentActiveTab(windowType); - initializeUiWithTab(activeTab); + + /** + * 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 background is ready. + */ + if (isManifestV3()) { + extensionPort.onMessage.addListener((message) => { + if (message?.name === 'CONNECTION_READY') { + initializeUiWithTab(activeTab); + } + }); + } else { + initializeUiWithTab(activeTab); + } function displayCriticalError(container, err) { container.innerHTML = diff --git a/development/build/manifest.js b/development/build/manifest.js index 498ab2785..0b35f125c 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -2,7 +2,9 @@ const { promises: fs } = require('fs'); const path = require('path'); const { mergeWith, cloneDeep } = require('lodash'); -const baseManifest = require('../../app/manifest/_base.json'); +const baseManifest = process.env.ENABLE_MV3 + ? require('../../app/manifest/v3/_base.json') + : require('../../app/manifest/v2/_base.json'); const { BuildType } = require('../lib/build-type'); const { createTask, composeSeries } = require('./task'); @@ -24,7 +26,7 @@ function createManifestTasks({ '..', '..', 'app', - 'manifest', + process.env.ENABLE_MV3 ? 'manifest/v3' : 'manifest/v2', `${platform}.json`, ), ); diff --git a/development/build/scripts.js b/development/build/scripts.js index 1447b3d79..b3f62b75e 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -345,6 +345,50 @@ function createScriptTasks({ } } +// Function generates app-init.js for browsers chrome, brave and opera. +// It dynamically injects list of files generated in the build. +async function bundleMV3AppInitialiser({ + jsBundles, + browserPlatforms, + buildType, + devMode, + ignoredFiles, + testing, + policyOnly, + shouldLintFenceFiles, +}) { + const label = 'app-init'; + // TODO: remove this filter for firefox once MV3 is supported in it + const mv3BrowserPlatforms = browserPlatforms.filter( + (platform) => platform !== 'firefox', + ); + const fileList = jsBundles.reduce( + (result, file) => `${result}'${file}',\n `, + '', + ); + + await createNormalBundle({ + browserPlatforms: mv3BrowserPlatforms, + buildType, + destFilepath: 'app-init.js', + devMode, + entryFilepath: './app/scripts/app-init.js', + ignoredFiles, + label, + testing, + policyOnly, + shouldLintFenceFiles, + })(); + + mv3BrowserPlatforms.forEach((browser) => { + const appInitFile = `./dist/${browser}/app-init.js`; + const fileContent = readFileSync('./app/scripts/app-init.js', 'utf8'); + const fileOutput = fileContent.replace('/** FILE NAMES */', fileList); + writeFileSync(appInitFile, fileOutput); + }); + console.log(`Bundle end: service worker app-init.js`); +} + function createFactoredBuild({ applyLavaMoat, browserPlatforms, @@ -457,7 +501,7 @@ function createFactoredBuild({ }); // wait for bundle completion for postprocessing - events.on('bundleDone', () => { + events.on('bundleDone', async () => { // Skip HTML generation if nothing is to be written to disk if (policyOnly) { return; @@ -503,6 +547,22 @@ function createFactoredBuild({ browserPlatforms, applyLavaMoat, }); + if (process.env.ENABLE_MV3) { + const jsBundles = [ + ...commonSet.values(), + ...groupSet.values(), + ].map((label) => `./${label}.js`); + await bundleMV3AppInitialiser({ + jsBundles, + browserPlatforms, + buildType, + devMode, + ignoredFiles, + testing, + policyOnly, + shouldLintFenceFiles, + }); + } break; } case 'content-script': { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 88685c3a7..bc3e11725 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -3511,6 +3511,7 @@ "setTimeout": true }, "packages": { + "@metamask/snap-controllers>@metamask/controllers": true, "@metamask/controllers": true, "@metamask/post-message-stream": true, "@metamask/providers>@metamask/object-multiplex": true, @@ -3533,6 +3534,43 @@ "semver": true } }, + "@metamask/snap-controllers>@metamask/controllers": { + "packages": { + "@metamask/controllers>isomorphic-fetch": true, + "browserify>buffer": true, + "ethereumjs-util": true, + "ethjs>ethjs-unit": true, + "eth-rpc-errors": true, + "eth-ens-namehash": true, + "eth-sig-util": true, + "jsonschema": true, + "@metamask/controllers>multiformats": true, + "@storybook/api>fast-deep-equal": true, + "eth-query": true, + "@metamask/controllers>async-mutex": true, + "@metamask/snap-controllers>nanoid": true, + "immer": true, + "web3": true, + "single-call-balance-checker-abi": true, + "@metamask/metamask-eth-abis": true, + "ethereumjs-wallet": true, + "eth-keyring-controller": true, + "uuid": true, + "browserify>events": true, + "@metamask/controllers>web3-provider-engine": true, + "eth-json-rpc-infura": true, + "punycode": true, + "@metamask/controllers>eth-phishing-detect": true, + "eth-method-registry": true, + "@ethereumjs/common": true, + "@ethereumjs/tx": true, + "@metamask/contract-metadata": true, + "@metamask/controllers>abort-controller": true, + "ethers": true, + "deep-freeze-strict": true, + "json-rpc-engine": true + } + }, "@metamask/snap-controllers>@metamask/obs-store": { "packages": { "@metamask/snap-controllers>@metamask/obs-store>through2": true, diff --git a/package.json b/package.json index 3f0e83e48..2a6666b9d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "setup:postinstall": "yarn patch-package && yarn allow-scripts", "start": "yarn build:dev dev --apply-lavamoat=false", "start:lavamoat": "yarn build:dev dev --apply-lavamoat=true", + "start:mv3": "ENABLE_MV3=true yarn build:dev dev --apply-lavamoat=false", "dist": "yarn build prod", "build": "yarn lavamoat:build", "build:dev": "node development/build/index.js", diff --git a/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js new file mode 100644 index 000000000..a990075b6 --- /dev/null +++ b/shared/modules/mv3.utils.js @@ -0,0 +1,4 @@ +import browser from 'webextension-polyfill'; + +export const isManifestV3 = () => + browser.runtime.getManifest().manifest_version === 3;