diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 16e21905a..b9b2497be 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1605,6 +1605,9 @@ "message": "You need $1 more $2 to complete this swap", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, + "swapBetterQuoteAvailable": { + "message": "A better quote is available" + }, "swapBuildQuotePlaceHolderText": { "message": "No tokens available matching $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" @@ -1701,8 +1704,8 @@ "message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapNQuotesAvailable": { - "message": "$1 quotes available", + "swapNQuotes": { + "message": "$1 quotes", "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { @@ -1737,7 +1740,7 @@ "message": "Quote details" }, "swapQuoteDetailsSlippageInfo": { - "message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting." + "message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"slippage tolerance\" setting." }, "swapQuoteIncludesRate": { "message": "Quote includes a $1% MetaMask fee", @@ -1830,6 +1833,9 @@ "swapUnknown": { "message": "Unknown" }, + "swapUsingBestQuote": { + "message": "Using the best quote" + }, "swapVerifyTokenExplanation": { "message": "Multiple tokens can use the same name and symbol. Check Etherscan to verify this is the token you're looking for." }, @@ -1846,15 +1852,11 @@ "swapsAdvancedOptions": { "message": "Advanced Options" }, - "swapsBestQuote": { - "message": "Best quote" - }, - "swapsConvertToAbout": { - "message": "Convert $1 to about", - "description": "This message is part of a quote for a swap. The $1 is the amount being converted, and the amount it is being swapped for is below this message" + "swapsExcessiveSlippageWarning": { + "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." }, "swapsMaxSlippage": { - "message": "Max slippage" + "message": "Slippage Tolerance" }, "swapsNotEnoughForTx": { "message": "Not enough $1 to complete this transaction", diff --git a/app/home.html b/app/home.html index 5c131bfc8..072eddea9 100644 --- a/app/home.html +++ b/app/home.html @@ -10,6 +10,10 @@
+ + + + diff --git a/app/images/down-arrow-grey.svg b/app/images/down-arrow-grey.svg new file mode 100644 index 000000000..fcdb33eec --- /dev/null +++ b/app/images/down-arrow-grey.svg @@ -0,0 +1,3 @@ + diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 3192abeb4..b9b650071 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -2,6 +2,7 @@ "author": "https://metamask.io", "background": { "scripts": [ + "globalthis.js", "initSentry.js", "lockdown.cjs", "runLockdown.js", @@ -36,7 +37,12 @@ "content_scripts": [ { "matches": ["file://*/*", "http://*/*", "https://*/*"], - "js": ["lockdown.cjs", "runLockdown.js", "contentscript.js"], + "js": [ + "globalthis.js", + "lockdown.cjs", + "runLockdown.js", + "contentscript.js" + ], "run_at": "document_start", "all_frames": true }, diff --git a/app/notification.html b/app/notification.html index 3419acdca..4f424e3c0 100644 --- a/app/notification.html +++ b/app/notification.html @@ -33,6 +33,10 @@ + + + + + diff --git a/app/scripts/background.js b/app/scripts/background.js index fa1e48a10..92f55319a 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -19,6 +19,7 @@ import extension from 'extensionizer' import storeTransform from 'obs-store/lib/transform' import asStream from 'obs-store/lib/asStream' import PortStream from 'extension-port-stream' +import { captureException } from '@sentry/browser' import migrations from './migrations' import Migrator from './lib/migrator' import ExtensionPlatform from './platforms/extension' @@ -279,6 +280,7 @@ function setupController(initState, initLangCode) { await localStore.set(state) } catch (err) { // log error so we dont break the pipeline + captureException(err) log.error('error setting state in local store:', err) } } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 05d82c538..5f5f25f9d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -16,16 +16,13 @@ const inpageContent = fs.readFileSync( const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n` const inpageBundle = inpageContent + inpageSuffix -// Eventually this streaming injection could be replaced with: -// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction -// -// But for now that is only Firefox -// If we create a FireFox-only code path using that API, -// MetaMask will be much faster loading and performant on Firefox. +const CONTENT_SCRIPT = 'metamask-contentscript' +const INPAGE = 'metamask-inpage' +const PROVIDER = 'metamask-provider' if (shouldInjectProvider()) { injectScript(inpageBundle) - start() + setupStreams() } /** @@ -46,15 +43,6 @@ function injectScript(content) { } } -/** - * Sets up the stream communication and submits site metadata - * - */ -async function start() { - await setupStreams() - await domIsReady() -} - /** * Sets up two-way communication streams between the * browser extension and local per-page browser context. @@ -63,10 +51,10 @@ async function start() { async function setupStreams() { // the transport-specific streams for communication between inpage and background const pageStream = new LocalMessageDuplexStream({ - name: 'contentscript', - target: 'inpage', + name: CONTENT_SCRIPT, + target: INPAGE, }) - const extensionPort = extension.runtime.connect({ name: 'contentscript' }) + const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT }) const extensionStream = new PortStream(extensionPort) // create and connect channel muxers @@ -79,26 +67,26 @@ async function setupStreams() { pump(pageMux, pageStream, pageMux, (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err), ) - pump(extensionMux, extensionStream, extensionMux, (err) => - logStreamDisconnectWarning('MetaMask Background Multiplex', err), - ) + pump(extensionMux, extensionStream, extensionMux, (err) => { + logStreamDisconnectWarning('MetaMask Background Multiplex', err) + notifyInpageOfStreamFailure() + }) // forward communication across inpage-background for these channels only - forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) - forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux) + forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux) // connect "phishing" channel to warning system const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) } -function forwardTrafficBetweenMuxers(channelName, muxA, muxB) { +function forwardTrafficBetweenMuxes(channelName, muxA, muxB) { const channelA = muxA.createStream(channelName) const channelB = muxB.createStream(channelName) - pump(channelA, channelB, channelA, (err) => - logStreamDisconnectWarning( + pump(channelA, channelB, channelA, (error) => + console.debug( `MetaMask muxed traffic for channel "${channelName}" failed.`, - err, + error, ), ) } @@ -107,14 +95,35 @@ function forwardTrafficBetweenMuxers(channelName, muxA, muxB) { * Error handler for page to extension stream disconnections * * @param {string} remoteLabel - Remote stream name - * @param {Error} err - Stream connection error + * @param {Error} error - Stream connection error */ -function logStreamDisconnectWarning(remoteLabel, err) { - let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` - if (err) { - warningMsg += `\n${err.stack}` - } - console.warn(warningMsg) +function logStreamDisconnectWarning(remoteLabel, error) { + console.debug( + `MetaMask Contentscript: Lost connection to "${remoteLabel}".`, + error, + ) +} + +/** + * This function must ONLY be called in pump destruction/close callbacks. + * Notifies the inpage context that streams have failed, via window.postMessage. + * Relies on obj-multiplex and post-message-stream implementation details. + */ +function notifyInpageOfStreamFailure() { + window.postMessage( + { + target: INPAGE, // the post-message-stream "target" + data: { + // this object gets passed to obj-multiplex + name: PROVIDER, // the obj-multiplex channel name + data: { + jsonrpc: '2.0', + method: 'METAMASK_STREAM_FAILURE', + }, + }, + }, + window.location.origin, + ) } /** @@ -221,17 +230,3 @@ function redirectToPhishingWarning() { href: window.location.href, })}` } - -/** - * Returns a promise that resolves when the DOM is loaded (does not wait for images to load) - */ -async function domIsReady() { - // already loaded - if (['interactive', 'complete'].includes(document.readyState)) { - return undefined - } - // wait for load - return new Promise((resolve) => - window.addEventListener('DOMContentLoaded', resolve, { once: true }), - ) -} diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 1fda6dccc..df821335c 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -47,7 +47,8 @@ export default class DetectTokensController { for (const contractAddress in contracts) { if ( contracts[contractAddress].erc20 && - !this.tokenAddresses.includes(contractAddress.toLowerCase()) + !this.tokenAddresses.includes(contractAddress.toLowerCase()) && + !this.hiddenTokens.includes(contractAddress.toLowerCase()) ) { tokensToDetect.push(contractAddress) } @@ -130,10 +131,12 @@ export default class DetectTokensController { this.tokenAddresses = currentTokens ? currentTokens.map((token) => token.address) : [] - preferences.store.subscribe(({ tokens = [] }) => { + this.hiddenTokens = preferences.store.getState().hiddenTokens + preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => { this.tokenAddresses = tokens.map((token) => { return token.address }) + this.hiddenTokens = hiddenTokens }) preferences.store.subscribe(({ selectedAddress }) => { if (this.selectedAddress !== selectedAddress) { diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 0d1514d61..2a26c9b75 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -7,7 +7,8 @@ import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware' import createInfuraMiddleware from 'eth-json-rpc-infura' import BlockTracker from 'eth-block-tracker' -import * as networkEnums from './enums' + +import { NETWORK_TYPE_TO_ID_MAP } from './enums' export default function createInfuraClient({ network, projectId }) { const infuraMiddleware = createInfuraMiddleware({ @@ -32,36 +33,14 @@ export default function createInfuraClient({ network, projectId }) { } function createNetworkAndChainIdMiddleware({ network }) { - let chainId - let netId - - switch (network) { - case 'mainnet': - netId = networkEnums.MAINNET_NETWORK_ID - chainId = '0x01' - break - case 'ropsten': - netId = networkEnums.ROPSTEN_NETWORK_ID - chainId = '0x03' - break - case 'rinkeby': - netId = networkEnums.RINKEBY_NETWORK_ID - chainId = '0x04' - break - case 'kovan': - netId = networkEnums.KOVAN_NETWORK_ID - chainId = networkEnums.KOVAN_CHAIN_ID - break - case 'goerli': - netId = networkEnums.GOERLI_NETWORK_ID - chainId = '0x05' - break - default: - throw new Error(`createInfuraClient - unknown network "${network}"`) + if (!NETWORK_TYPE_TO_ID_MAP[network]) { + throw new Error(`createInfuraClient - unknown network "${network}"`) } + const { chainId, networkId } = NETWORK_TYPE_TO_ID_MAP[network] + return createScaffoldMiddleware({ eth_chainId: chainId, - net_version: netId, + net_version: networkId, }) } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 016331bce..9dc605cf2 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -19,6 +19,8 @@ import { MAINNET, INFURA_PROVIDER_TYPES, NETWORK_TYPE_TO_ID_MAP, + MAINNET_CHAIN_ID, + RINKEBY_CHAIN_ID, } from './enums' const env = process.env.METAMASK_ENV @@ -32,9 +34,9 @@ if (process.env.IN_TEST === 'true') { nickname: 'Localhost 8545', } } else if (process.env.METAMASK_DEBUG || env === 'test') { - defaultProviderConfigOpts = { type: RINKEBY } + defaultProviderConfigOpts = { type: RINKEBY, chainId: RINKEBY_CHAIN_ID } } else { - defaultProviderConfigOpts = { type: MAINNET } + defaultProviderConfigOpts = { type: MAINNET, chainId: MAINNET_CHAIN_ID } } const defaultProviderConfig = { diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index 56a87fdf5..d5ca52e30 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -19,10 +19,15 @@ export const CAVEAT_TYPES = { } export const NOTIFICATION_NAMES = { - accountsChanged: 'wallet_accountsChanged', + accountsChanged: 'metamask_accountsChanged', + unlockStateChanged: 'metamask_unlockStateChanged', + chainChanged: 'metamask_chainChanged', } -export const LOG_IGNORE_METHODS = ['wallet_sendDomainMetadata'] +export const LOG_IGNORE_METHODS = [ + 'wallet_registerOnboarding', + 'wallet_watchAsset', +] export const LOG_METHOD_TYPES = { restricted: 'restricted', @@ -82,8 +87,9 @@ export const SAFE_METHODS = [ 'eth_submitWork', 'eth_syncing', 'eth_uninstallFilter', - 'metamask_watchAsset', - 'wallet_watchAsset', 'eth_getEncryptionPublicKey', 'eth_decrypt', + 'metamask_watchAsset', + 'wallet_watchAsset', + 'metamask_getProviderState', ] diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 2e3c12775..454bd8e56 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -28,6 +28,7 @@ export class PermissionsController { getKeyringAccounts, getRestrictedMethods, getUnlockPromise, + isUnlocked, notifyDomain, notifyAllDomains, preferences, @@ -47,6 +48,7 @@ export class PermissionsController { this._notifyDomain = notifyDomain this._notifyAllDomains = notifyAllDomains this._showPermissionRequest = showPermissionRequest + this._isUnlocked = isUnlocked this._restrictedMethods = getRestrictedMethods({ getKeyringAccounts: this.getKeyringAccounts.bind(this), @@ -463,21 +465,20 @@ export class PermissionsController { throw new Error('Invalid accounts', newAccounts) } - this._notifyDomain(origin, { - method: NOTIFICATION_NAMES.accountsChanged, - result: newAccounts, - }) - - // if the accounts changed from the perspective of the dapp, - // update "last seen" time for the origin and account(s) - // exception: no accounts -> no times to update - this.permissionsLog.updateAccountsHistory(origin, newAccounts) + // We do not share accounts when the extension is locked. + if (this._isUnlocked()) { + this._notifyDomain(origin, { + method: NOTIFICATION_NAMES.accountsChanged, + params: newAccounts, + }) + this.permissionsLog.updateAccountsHistory(origin, newAccounts) + } // NOTE: - // we don't check for accounts changing in the notifyAllDomains case, - // because the log only records when accounts were last seen, - // and the accounts only change for all domains at once when permissions - // are removed + // We don't check for accounts changing in the notifyAllDomains case, + // because the log only records when accounts were last seen, and the + // the accounts only change for all domains at once when permissions are + // removed. } /** @@ -508,9 +509,11 @@ export class PermissionsController { */ clearPermissions() { this.permissions.clearDomains() + // It's safe to notify that no accounts are available, regardless of + // extension lock state this._notifyAllDomains({ method: NOTIFICATION_NAMES.accountsChanged, - result: [], + params: [], }) } @@ -749,7 +752,3 @@ export class PermissionsController { ) } } - -export function addInternalMethodPrefix(method) { - return WALLET_PREFIX + method -} diff --git a/app/scripts/controllers/permissions/permissionsLog.js b/app/scripts/controllers/permissions/permissionsLog.js index 6bb142993..e1a37f0c8 100644 --- a/app/scripts/controllers/permissions/permissionsLog.js +++ b/app/scripts/controllers/permissions/permissionsLog.js @@ -58,6 +58,7 @@ export default class PermissionsLogController { /** * Updates the exposed account history for the given origin. * Sets the 'last seen' time to Date.now() for the given accounts. + * Returns if the accounts array is empty. * * @param {string} origin - The origin that the accounts are exposed to. * @param {Array{bestQuoteText}
+ )} ++ {t('swapNQuotes', [numberOfQuotes])} +
+- {t('swapQuoteIncludesRate', [metaMaskFee])} -
-diff --git a/app/phishing.html b/app/phishing.html index 1c913db2e..59ea3ac71 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -2,6 +2,7 @@
+ diff --git a/app/popup.html b/app/popup.html index 4d29f6153..e73f3e4d2 100644 --- a/app/popup.html +++ b/app/popup.html @@ -10,6 +10,7 @@