diff --git a/.circleci/config.yml b/.circleci/config.yml index cd6efe2a3..e0407f18b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,19 @@ version: 2.1 +executors: + node-browsers: + docker: + - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + node-browsers-medium-plus: + docker: + - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + resource_class: medium+ + environment: + NODE_OPTIONS: --max_old_space_size=2048 + shellcheck: + docker: + - image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294 + workflows: test_and_release: jobs: @@ -92,14 +106,10 @@ workflows: only: develop requires: - prep-build-storybook - - coveralls-upload: - requires: - - test-unit jobs: create_release_pull_request: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -112,18 +122,20 @@ jobs: .circleci/scripts/release-create-release-pr prep-deps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout + - restore_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} - run: name: Install deps command: | .circleci/scripts/deps-install.sh - - run: - name: Collect yarn install HAR logs - command: | - .circleci/scripts/collect-har-artifact.sh + - save_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + paths: + - node_modules/ + - build-artifacts/yarn-install-har/ - persist_to_workspace: root: . paths: @@ -131,11 +143,7 @@ jobs: - build-artifacts prep-build: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -153,11 +161,7 @@ jobs: - builds prep-build-test: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -168,17 +172,17 @@ jobs: - run: name: Move test build to 'dist-test' to avoid conflict with production build command: mv ./dist ./dist-test + - run: + name: Move test zips to 'builds-test' to avoid conflict with production build + command: mv ./builds ./builds-test - persist_to_workspace: root: . paths: - dist-test + - builds-test prep-build-test-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - resource_class: medium+ - environment: - NODE_OPTIONS: --max_old_space_size=2048 + executor: node-browsers-medium-plus steps: - checkout - attach_workspace: @@ -189,14 +193,17 @@ jobs: - run: name: Move test build to 'dist-test-metrics' to avoid conflict with production build command: mv ./dist ./dist-test-metrics + - run: + name: Move test zips to 'builds-test' to avoid conflict with production build + command: mv ./builds ./builds-test-metrics - persist_to_workspace: root: . paths: - dist-test-metrics + - builds-test-metrics prep-build-storybook: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -210,8 +217,7 @@ jobs: - .out test-lint: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -224,8 +230,7 @@ jobs: command: yarn verify-locales --quiet test-lint-shellcheck: - docker: - - image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294 + executor: shellcheck steps: - checkout - run: apk add --no-cache bash jq yarn @@ -234,8 +239,7 @@ jobs: command: ./development/shellcheck.sh test-lint-lockfile: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -245,8 +249,7 @@ jobs: command: yarn lint:lockfile test-deps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -256,8 +259,7 @@ jobs: command: .circleci/scripts/yarn-audit test-e2e-chrome: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -265,6 +267,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: test:e2e:chrome command: | @@ -278,8 +283,7 @@ jobs: destination: test-artifacts test-e2e-chrome-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -287,6 +291,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test-metrics ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test-metrics ./builds - run: name: test:e2e:chrome:metrics command: | @@ -300,8 +307,7 @@ jobs: destination: test-artifacts test-e2e-firefox: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - run: @@ -312,6 +318,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: test:e2e:firefox command: | @@ -325,8 +334,7 @@ jobs: destination: test-artifacts test-e2e-firefox-metrics: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - run: @@ -337,6 +345,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test-metrics ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test-metrics ./builds - run: name: test:e2e:firefox:metrics command: | @@ -350,8 +361,7 @@ jobs: destination: test-artifacts benchmark: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -359,6 +369,9 @@ jobs: - run: name: Move test build to dist command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds - run: name: Run page load benchmark command: yarn benchmark:chrome --out test-artifacts/chrome/benchmark/pageload.json @@ -371,8 +384,7 @@ jobs: - test-artifacts job-publish-prerelease: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -386,6 +398,9 @@ jobs: - store_artifacts: path: builds destination: builds + - store_artifacts: + path: coverage + destination: coverage - store_artifacts: path: test-artifacts destination: test-artifacts @@ -403,8 +418,7 @@ jobs: command: ./development/metamaskbot-build-announce.js job-publish-release: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -421,8 +435,7 @@ jobs: command: .circleci/scripts/release-create-master-pr job-publish-storybook: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - add_ssh_keys: fingerprints: @@ -437,8 +450,7 @@ jobs: yarn storybook:deploy test-unit: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -452,8 +464,7 @@ jobs: - .nyc_output - coverage test-unit-global: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -463,8 +474,7 @@ jobs: command: yarn test:unit:global validate-source-maps: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -474,8 +484,7 @@ jobs: command: yarn validate-source-maps test-mozilla-lint: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - checkout - attach_workspace: @@ -485,20 +494,8 @@ jobs: command: NODE_OPTIONS=--max_old_space_size=3072 yarn mozilla-lint all-tests-pass: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 + executor: node-browsers steps: - run: name: All Tests Passed command: echo 'weew - everything passed!' - - coveralls-upload: - docker: - - image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88 - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Coveralls upload - command: yarn test:coveralls-upload diff --git a/.circleci/scripts/collect-har-artifact.sh b/.circleci/scripts/collect-har-artifact.sh deleted file mode 100755 index 7f6f1aa41..000000000 --- a/.circleci/scripts/collect-har-artifact.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -x - -mkdir -p build-artifacts/yarn-install-har -mv ./*.har build-artifacts/yarn-install-har/ diff --git a/.circleci/scripts/deps-install.sh b/.circleci/scripts/deps-install.sh index c8b15e29b..828d6e63a 100755 --- a/.circleci/scripts/deps-install.sh +++ b/.circleci/scripts/deps-install.sh @@ -7,6 +7,14 @@ set -e yarn --frozen-lockfile --ignore-scripts --har +# Move HAR file into directory with consistent name so that we can cache it +mkdir -p build-artifacts/yarn-install-har +har_files=(./*.har) +if [[ -f "${har_files[0]}" ]] +then + mv ./*.har build-artifacts/yarn-install-har/ +fi + # run each in subshell so directory change does not persist # scripts can be any of: # preinstall diff --git a/.circleci/scripts/firefox-install b/.circleci/scripts/firefox-install index 21766467e..0c3512114 100755 --- a/.circleci/scripts/firefox-install +++ b/.circleci/scripts/firefox-install @@ -4,7 +4,7 @@ set -e set -u set -o pipefail -FIREFOX_VERSION='70.0' +FIREFOX_VERSION='83.0' FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2" FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}" FIREFOX_PATH='/opt/firefox' diff --git a/.gitignore b/.gitignore index 6d11965cb..46064a64b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ ui/app/css/output/ notes.txt -.coveralls.yml .nyc_output .metamaskrc diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5c628f9..c4de8a262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Current Develop Branch +- [#10048](https://github.com/MetaMask/metamask-extension/pull/10048): Display boolean values when signing typed data ## 8.1.10 Fri Dec 18 2020 - [#10084](https://github.com/MetaMask/metamask-extension/pull/10084): Set last provider when switching to a customRPC diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c0a0f82cf..c1c969b13 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -95,7 +95,13 @@ "message": "Browsing a website with an unconnected account selected" }, "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected." + "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." + }, + "alertSettingsWeb3ShimUsage": { + "message": "When a website tries to use the removed window.web3 API" + }, + "alertSettingsWeb3ShimUsageDescription": { + "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." }, "alerts": { "message": "Alerts" @@ -233,6 +239,9 @@ "bytes": { "message": "Bytes" }, + "canToggleInSettings": { + "message": "You can re-enable this notification in Settings -> Alerts." + }, "cancel": { "message": "Cancel" }, @@ -1605,6 +1614,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 +1713,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": { @@ -1830,6 +1842,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,13 +1861,6 @@ "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%." }, @@ -2096,6 +2104,10 @@ "walletSeed": { "message": "Seed phrase" }, + "web3ShimUsageNotification": { + "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Welcome to MetaMask" }, 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/images/icons/connect.svg b/app/images/icons/connect.svg deleted file mode 100644 index ac6577090..000000000 --- a/app/images/icons/connect.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/manifest/chrome.json b/app/manifest/chrome.json index 908d430c9..281c847a4 100644 --- a/app/manifest/chrome.json +++ b/app/manifest/chrome.json @@ -3,5 +3,5 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "58" + "minimum_chrome_version": "63" } diff --git a/app/scripts/background.js b/app/scripts/background.js index 9b2b9a448..1f8badfda 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -16,8 +16,7 @@ import pump from 'pump' import debounce from 'debounce-stream' import log from 'loglevel' import extension from 'extensionizer' -import storeTransform from 'obs-store/lib/transform' -import asStream from 'obs-store/lib/asStream' +import { storeAsStream, storeTransformStream } from '@metamask/obs-store' import PortStream from 'extension-port-stream' import { captureException } from '@sentry/browser' import migrations from './migrations' @@ -119,6 +118,7 @@ initialize().catch(log.error) * @property {number} unapprovedDecryptMsgCount - The number of messages in unapprovedDecryptMsgs. * @property {Object} unapprovedTypedMsgs - An object of messages pending approval, mapping a unique ID to the options. * @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. + * @property {number} pendingApprovalCount - The number of pending request in the approval controller. * @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts. * @property {Keyring[]} keyrings - An array of keyring descriptions, summarizing the accounts that are available for use, and what keyrings they belong to. * @property {string} selectedAddress - A lower case hex string of the currently selected address. @@ -249,9 +249,9 @@ function setupController(initState, initLangCode) { // setup state persistence pump( - asStream(controller.store), + storeAsStream(controller.store), debounce(1000), - storeTransform(versionifyData), + storeTransformStream(versionifyData), createStreamSink(persistData), (error) => { log.error('MetaMask - Persistence pipeline failed', error) @@ -402,7 +402,7 @@ function setupController(initState, initLangCode) { controller.decryptMessageManager.on('updateBadge', updateBadge) controller.encryptionPublicKeyManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) - controller.permissionsController.permissions.subscribe(updateBadge) + controller.approvalController.subscribe(updateBadge) controller.appStateController.on('updateBadge', updateBadge) /** @@ -419,9 +419,7 @@ function setupController(initState, initLangCode) { unapprovedEncryptionPublicKeyMsgCount, } = controller.encryptionPublicKeyManager const { unapprovedTypedMessagesCount } = controller.typedMessageManager - const pendingPermissionRequests = Object.keys( - controller.permissionsController.permissions.state.permissionsRequests, - ).length + const pendingApprovalCount = controller.approvalController.getTotalApprovalCount() const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length const count = @@ -431,7 +429,7 @@ function setupController(initState, initLangCode) { unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + unapprovedTypedMessagesCount + - pendingPermissionRequests + + pendingApprovalCount + waitingForUnlockCount if (count) { label = String(count) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index a0fbc5500..7e55faec4 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,20 +67,20 @@ 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, (error) => @@ -116,6 +104,28 @@ function logStreamDisconnectWarning(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, + ) +} + /** * Determines if the provider should be injected * @@ -220,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/alert.js b/app/scripts/controllers/alert.js index a2dd3a7db..a58276d88 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -1,4 +1,8 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' +import { + TOGGLEABLE_ALERT_TYPES, + WEB3_SHIM_USAGE_ALERT_STATES, +} from '../../../shared/constants/alerts' /** * @typedef {Object} AlertControllerInitState @@ -14,14 +18,8 @@ import ObservableStore from 'obs-store' * @property {AlertControllerInitState} initState - The initial controller state */ -export const ALERT_TYPES = { - unconnectedAccount: 'unconnectedAccount', - // enumerated here but has no background state - invalidCustomNetwork: 'invalidCustomNetwork', -} - const defaultState = { - alertEnabledness: Object.keys(ALERT_TYPES).reduce( + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness, alertType) => { alertEnabledness[alertType] = true return alertEnabledness @@ -29,11 +27,11 @@ const defaultState = { {}, ), unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, } /** - * Controller responsible for maintaining - * alert related state + * Controller responsible for maintaining alert-related state. */ export default class AlertController { /** @@ -41,11 +39,13 @@ export default class AlertController { * @param {AlertControllerOptions} [opts] - Controller configuration parameters */ constructor(opts = {}) { - const { initState, preferencesStore } = opts + const { initState = {}, preferencesStore } = opts const state = { ...defaultState, - ...initState, - unconnectedAccountAlertShownOrigins: {}, + alertEnabledness: { + ...defaultState.alertEnabledness, + ...initState.alertEnabledness, + }, } this.store = new ObservableStore(state) @@ -83,4 +83,48 @@ export default class AlertController { unconnectedAccountAlertShownOrigins[origin] = true this.store.updateState({ unconnectedAccountAlertShownOrigins }) } + + /** + * Gets the web3 shim usage state for the given origin. + * + * @param {string} origin - The origin to get the web3 shim usage state for. + * @returns {undefined | 1 | 2} The web3 shim usage state for the given + * origin, or undefined. + */ + getWeb3ShimUsageState(origin) { + return this.store.getState().web3ShimUsageOrigins[origin] + } + + /** + * Sets the web3 shim usage state for the given origin to RECORDED. + * + * @param {string} origin - The origin the that used the web3 shim. + */ + setWeb3ShimUsageRecorded(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.RECORDED) + } + + /** + * Sets the web3 shim usage state for the given origin to DISMISSED. + * + * @param {string} origin - The origin that the web3 shim notification was + * dismissed for. + */ + setWeb3ShimUsageAlertDismissed(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.DISMISSED) + } + + /** + * @private + * @param {string} origin - The origin to set the state for. + * @param {number} value - The state value to set. + */ + _setWeb3ShimUsageState(origin, value) { + let { web3ShimUsageOrigins } = this.store.getState() + web3ShimUsageOrigins = { + ...web3ShimUsageOrigins, + } + web3ShimUsageOrigins[origin] = value + this.store.updateState({ web3ShimUsageOrigins }) + } } diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 738a0ef85..207ce96e4 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' export default class AppStateController extends EventEmitter { /** diff --git a/app/scripts/controllers/cached-balances.js b/app/scripts/controllers/cached-balances.js index 6e7e4eace..16ef40e88 100644 --- a/app/scripts/controllers/cached-balances.js +++ b/app/scripts/controllers/cached-balances.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /** * @typedef {Object} CachedBalancesOptions 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/ens/index.js b/app/scripts/controllers/ens/index.js index 766aeeb88..08647b12f 100644 --- a/app/scripts/controllers/ens/index.js +++ b/app/scripts/controllers/ens/index.js @@ -1,6 +1,6 @@ import punycode from 'punycode/punycode' import ethUtil from 'ethereumjs-util' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import Ens from './ens' diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 2f70d63cd..58a059dd2 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import BN from 'bn.js' import createId from '../lib/random-id' @@ -249,9 +249,7 @@ export default class IncomingTransactionsController { }) const incomingTxs = remoteTxs.filter( - (tx) => - tx.txParams.to && - tx.txParams.to.toLowerCase() === address.toLowerCase(), + (tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(), ) incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)) diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 60e3d759c..3ae74cbf8 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -1,5 +1,5 @@ import { merge, omit } from 'lodash' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { bufferToHex, sha3 } from 'ethereumjs-util' import { ENVIRONMENT_TYPE_BACKGROUND } from '../lib/enums' import { 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 9dc605cf2..dc7b4a648 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,7 +1,6 @@ import assert from 'assert' import EventEmitter from 'events' -import ObservableStore from 'obs-store' -import ComposedStore from 'obs-store/lib/composed' +import { ComposedStore, ObservableStore } from '@metamask/obs-store' import { JsonRpcEngine } from 'json-rpc-engine' import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine' import log from 'loglevel' diff --git a/app/scripts/controllers/onboarding.js b/app/scripts/controllers/onboarding.js index 71424a1e4..011edbfc3 100644 --- a/app/scripts/controllers/onboarding.js +++ b/app/scripts/controllers/onboarding.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' /** diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index de66fca9a..510fad1b3 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -1,3 +1,5 @@ +export const APPROVAL_TYPE = 'wallet_requestPermissions' + export const WALLET_PREFIX = 'wallet_' export const HISTORY_STORE_KEY = 'permissionsHistory' @@ -19,10 +21,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', @@ -78,6 +85,7 @@ export const SAFE_METHODS = [ 'eth_submitWork', 'eth_syncing', 'eth_uninstallFilter', + 'metamask_getProviderState', 'metamask_watchAsset', 'net_listening', 'net_peerCount', diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 2e3c12775..c4ffd14fb 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -1,6 +1,6 @@ import nanoid from 'nanoid' import { JsonRpcEngine } from 'json-rpc-engine' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import { CapabilitiesController as RpcCap } from 'rpc-cap' import { ethErrors } from 'eth-json-rpc-errors' @@ -11,6 +11,7 @@ import PermissionsLogController from './permissionsLog' // Methods that do not require any permissions to use: import { + APPROVAL_TYPE, SAFE_METHODS, // methods that do not require any permissions to use WALLET_PREFIX, METADATA_STORE_KEY, @@ -22,16 +23,20 @@ import { CAVEAT_TYPES, } from './enums' +// instanbul ignore next +const noop = () => undefined + export class PermissionsController { constructor( { + approvals, getKeyringAccounts, getRestrictedMethods, getUnlockPromise, + isUnlocked, notifyDomain, notifyAllDomains, preferences, - showPermissionRequest, } = {}, restoredPermissions = {}, restoredState = {}, @@ -46,7 +51,7 @@ export class PermissionsController { this._getUnlockPromise = getUnlockPromise this._notifyDomain = notifyDomain this._notifyAllDomains = notifyAllDomains - this._showPermissionRequest = showPermissionRequest + this._isUnlocked = isUnlocked this._restrictedMethods = getRestrictedMethods({ getKeyringAccounts: this.getKeyringAccounts.bind(this), @@ -56,8 +61,12 @@ export class PermissionsController { restrictedMethods: Object.keys(this._restrictedMethods), store: this.store, }) - this.pendingApprovals = new Map() - this.pendingApprovalOrigins = new Set() + + /** + * @type {import('@metamask/controllers').ApprovalController} + * @public + */ + this.approvals = approvals this._initializePermissions(restoredPermissions) this._lastSelectedAddress = preferences.getState().selectedAddress this.preferences = preferences @@ -137,7 +146,7 @@ export class PermissionsController { { origin }, req, res, - () => undefined, + noop, _end, ) @@ -191,13 +200,7 @@ export class PermissionsController { } const res = {} - this.permissions.providerMiddlewareFunction( - domain, - req, - res, - () => undefined, - _end, - ) + this.permissions.providerMiddlewareFunction(domain, req, res, noop, _end) function _end(_err) { const err = _err || res.error @@ -221,16 +224,16 @@ export class PermissionsController { */ async approvePermissionsRequest(approved, accounts) { const { id } = approved.metadata - const approval = this.pendingApprovals.get(id) - if (!approval) { - log.debug(`Permissions request with id '${id}' not found`) + if (!this.approvals.has({ id })) { + log.debug(`Permissions request with id '${id}' not found.`) return } try { if (Object.keys(approved.permissions).length === 0) { - approval.reject( + this.approvals.reject( + id, ethErrors.rpc.invalidRequest({ message: 'Must request at least one permission.', }), @@ -242,19 +245,18 @@ export class PermissionsController { approved.permissions, accounts, ) - approval.resolve(approved.permissions) + this.approvals.resolve(id, approved.permissions) } } catch (err) { // if finalization fails, reject the request - approval.reject( + this.approvals.reject( + id, ethErrors.rpc.invalidRequest({ message: err.message, data: err, }), ) } - - this._removePendingApproval(id) } /** @@ -265,15 +267,12 @@ export class PermissionsController { * @param {string} id - The id of the request rejected by the user */ async rejectPermissionsRequest(id) { - const approval = this.pendingApprovals.get(id) - - if (!approval) { - log.debug(`Permissions request with id '${id}' not found`) + if (!this.approvals.has({ id })) { + log.debug(`Permissions request with id '${id}' not found.`) return } - approval.reject(ethErrors.provider.userRejectedRequest()) - this._removePendingApproval(id) + this.approvals.reject(id, ethErrors.provider.userRejectedRequest()) } /** @@ -463,21 +462,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 +506,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: [], }) } @@ -667,37 +667,6 @@ export class PermissionsController { this.notifyAccountsChanged(origin, permittedAccounts) } - /** - * Adds a pending approval. - * @param {string} id - The id of the pending approval. - * @param {string} origin - The origin of the pending approval. - * @param {Function} resolve - The function resolving the pending approval Promise. - * @param {Function} reject - The function rejecting the pending approval Promise. - */ - _addPendingApproval(id, origin, resolve, reject) { - if ( - this.pendingApprovalOrigins.has(origin) || - this.pendingApprovals.has(id) - ) { - throw new Error( - `Pending approval with id '${id}' or origin '${origin}' already exists.`, - ) - } - - this.pendingApprovals.set(id, { origin, resolve, reject }) - this.pendingApprovalOrigins.add(origin) - } - - /** - * Removes the pending approval with the given id. - * @param {string} id - The id of the pending approval to remove. - */ - _removePendingApproval(id) { - const { origin } = this.pendingApprovals.get(id) - this.pendingApprovalOrigins.delete(origin) - this.pendingApprovals.delete(id) - } - /** * A convenience method for retrieving a login object * or creating a new one if needed. @@ -732,16 +701,10 @@ export class PermissionsController { metadata: { id, origin }, } = req - if (this.pendingApprovalOrigins.has(origin)) { - throw ethErrors.rpc.resourceUnavailable( - 'Permissions request already pending; please wait.', - ) - } - - this._showPermissionRequest() - - return new Promise((resolve, reject) => { - this._addPendingApproval(id, origin, resolve, reject) + return this.approvals.addAndShowApprovalRequest({ + id, + origin, + type: APPROVAL_TYPE, }) }, }, @@ -749,7 +712,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} accounts - The accounts. diff --git a/app/scripts/controllers/permissions/permissionsMethodMiddleware.js b/app/scripts/controllers/permissions/permissionsMethodMiddleware.js index f2eec6cf4..de9009f13 100644 --- a/app/scripts/controllers/permissions/permissionsMethodMiddleware.js +++ b/app/scripts/controllers/permissions/permissionsMethodMiddleware.js @@ -73,7 +73,7 @@ export default function createPermissionsMethodMiddleware({ // custom method for getting metadata from the requesting domain, // sent automatically by the inpage provider when it's initialized - case 'wallet_sendDomainMetadata': { + case 'metamask_sendDomainMetadata': { if (typeof req.domainMetadata?.name === 'string') { addDomainMetadata(req.origin, req.domainMetadata) } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index bfba2835b..7445be65b 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import { normalize as normalizeAddress } from 'eth-sig-util' import { isValidAddress } from 'ethereumjs-util' @@ -34,8 +34,10 @@ export default class PreferencesController { const initState = { frequentRpcListDetail: [], accountTokens: {}, + accountHiddenTokens: {}, assetImages: {}, tokens: [], + hiddenTokens: [], suggestedTokens: {}, useBlockie: false, useNonceField: false, @@ -191,6 +193,7 @@ export default class PreferencesController { setAddresses(addresses) { const oldIdentities = this.store.getState().identities const oldAccountTokens = this.store.getState().accountTokens + const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens const identities = addresses.reduce((ids, address, index) => { const oldId = oldIdentities[address] || {} @@ -202,7 +205,12 @@ export default class PreferencesController { tokens[address] = oldTokens return tokens }, {}) - this.store.updateState({ identities, accountTokens }) + const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => { + const oldHiddenTokens = oldAccountHiddenTokens[address] || {} + hiddenTokens[address] = oldHiddenTokens + return hiddenTokens + }, {}) + this.store.updateState({ identities, accountTokens, accountHiddenTokens }) } /** @@ -212,14 +220,19 @@ export default class PreferencesController { * @returns {string} the address that was removed */ removeAddress(address) { - const { identities } = this.store.getState() - const { accountTokens } = this.store.getState() + const { + identities, + accountTokens, + accountHiddenTokens, + } = this.store.getState() + if (!identities[address]) { throw new Error(`${address} can't be deleted cause it was not found`) } delete identities[address] delete accountTokens[address] - this.store.updateState({ identities, accountTokens }) + delete accountHiddenTokens[address] + this.store.updateState({ identities, accountTokens, accountHiddenTokens }) // If the selected account is no longer valid, // select an arbitrary other account: @@ -237,7 +250,11 @@ export default class PreferencesController { * */ addAddresses(addresses) { - const { identities, accountTokens } = this.store.getState() + const { + identities, + accountTokens, + accountHiddenTokens, + } = this.store.getState() addresses.forEach((address) => { // skip if already exists if (identities[address]) { @@ -247,9 +264,10 @@ export default class PreferencesController { const identityCount = Object.keys(identities).length accountTokens[address] = {} + accountHiddenTokens[address] = {} identities[address] = { name: `Account ${identityCount + 1}`, address } }) - this.store.updateState({ identities, accountTokens }) + this.store.updateState({ identities, accountTokens, accountHiddenTokens }) } /** @@ -346,7 +364,7 @@ export default class PreferencesController { */ /** - * Adds a new token to the token array, or updates the token if passed an address that already exists. + * Adds a new token to the token array and removes it from the hiddenToken array, or updates the token if passed an address that already exists. * Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects. * @see AddedToken {@link AddedToken} * @@ -359,8 +377,11 @@ export default class PreferencesController { async addToken(rawAddress, symbol, decimals, image) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } - const { tokens } = this.store.getState() + const { tokens, hiddenTokens } = this.store.getState() const assetImages = this.getAssetImages() + const updatedHiddenTokens = hiddenTokens.filter( + (tokenAddress) => tokenAddress !== rawAddress.toLowerCase(), + ) const previousEntry = tokens.find((token) => { return token.address === address }) @@ -372,23 +393,24 @@ export default class PreferencesController { tokens.push(newEntry) } assetImages[address] = image - this._updateAccountTokens(tokens, assetImages) + this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens) return Promise.resolve(tokens) } /** - * Removes a specified token from the tokens array. + * Removes a specified token from the tokens array and adds it to hiddenTokens array * * @param {string} rawAddress - Hex address of the token contract to remove. * @returns {Promise} The new array of AddedToken objects * */ removeToken(rawAddress) { - const { tokens } = this.store.getState() + const { tokens, hiddenTokens } = this.store.getState() const assetImages = this.getAssetImages() const updatedTokens = tokens.filter((token) => token.address !== rawAddress) + const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()] delete assetImages[rawAddress] - this._updateAccountTokens(updatedTokens, assetImages) + this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens) return Promise.resolve(updatedTokens) } @@ -643,47 +665,59 @@ export default class PreferencesController { */ _subscribeProviderType() { this.network.providerStore.subscribe(() => { - const { tokens } = this._getTokenRelatedStates() - this.store.updateState({ tokens }) + const { tokens, hiddenTokens } = this._getTokenRelatedStates() + this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens) }) } /** - * Updates `accountTokens` and `tokens` of current account and network according to it. + * Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it. * - * @param {Array} tokens - Array of tokens to be updated. + * @param {array} tokens - Array of tokens to be updated. + * @param {array} assetImages - Array of assets objects related to assets added + * @param {array} hiddenTokens - Array of tokens hidden by user * */ - _updateAccountTokens(tokens, assetImages) { + _updateAccountTokens(tokens, assetImages, hiddenTokens) { const { accountTokens, providerType, selectedAddress, + accountHiddenTokens, } = this._getTokenRelatedStates() accountTokens[selectedAddress][providerType] = tokens - this.store.updateState({ accountTokens, tokens, assetImages }) + accountHiddenTokens[selectedAddress][providerType] = hiddenTokens + this.store.updateState({ + accountTokens, + tokens, + assetImages, + accountHiddenTokens, + hiddenTokens, + }) } /** - * Updates `tokens` of current account and network. + * Updates `tokens` and `hiddenTokens` of current account and network. * * @param {string} selectedAddress - Account address to be updated with. * */ _updateTokens(selectedAddress) { - const { tokens } = this._getTokenRelatedStates(selectedAddress) - this.store.updateState({ tokens }) + const { tokens, hiddenTokens } = this._getTokenRelatedStates( + selectedAddress, + ) + this.store.updateState({ tokens, hiddenTokens }) } /** - * A getter for `tokens` and `accountTokens` related states. + * A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states. * * @param {string} [selectedAddress] - A new hex address for an account * @returns {Object.} States to interact with tokens in `accountTokens` * */ _getTokenRelatedStates(selectedAddress) { - const { accountTokens } = this.store.getState() + const { accountTokens, accountHiddenTokens } = this.store.getState() if (!selectedAddress) { // eslint-disable-next-line no-param-reassign selectedAddress = this.store.getState().selectedAddress @@ -692,11 +726,25 @@ export default class PreferencesController { if (!(selectedAddress in accountTokens)) { accountTokens[selectedAddress] = {} } + if (!(selectedAddress in accountHiddenTokens)) { + accountHiddenTokens[selectedAddress] = {} + } if (!(providerType in accountTokens[selectedAddress])) { accountTokens[selectedAddress][providerType] = [] } + if (!(providerType in accountHiddenTokens[selectedAddress])) { + accountHiddenTokens[selectedAddress][providerType] = [] + } const tokens = accountTokens[selectedAddress][providerType] - return { tokens, accountTokens, providerType, selectedAddress } + const hiddenTokens = accountHiddenTokens[selectedAddress][providerType] + return { + tokens, + accountTokens, + hiddenTokens, + accountHiddenTokens, + providerType, + selectedAddress, + } } /** diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index e230b8d63..0f599bb07 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -1,7 +1,7 @@ import { ethers } from 'ethers' import log from 'loglevel' import BigNumber from 'bignumber.js' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { mapValues, cloneDeep } from 'lodash' import abi from 'human-standard-token-abi' import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util' diff --git a/app/scripts/controllers/threebox.js b/app/scripts/controllers/threebox.js index ec73e2651..d7503b589 100644 --- a/app/scripts/controllers/threebox.js +++ b/app/scripts/controllers/threebox.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /* eslint-disable import/first,import/order */ const Box = process.env.IN_TEST diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index b943c540f..9011b1d01 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import { normalize as normalizeAddress } from 'eth-sig-util' import ethUtil from 'ethereumjs-util' diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 4a4488fc1..670fce56f 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,5 +1,5 @@ import EventEmitter from 'safe-event-emitter' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import Transaction from 'ethereumjs-tx' import EthQuery from 'ethjs-query' diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 456a1db98..0c94bd270 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'safe-event-emitter' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import createId from '../../lib/random-id' import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction' diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 8982a93f2..c7619f77e 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -33,11 +33,7 @@ cleanContextForImports() /* eslint-disable import/first */ import log from 'loglevel' import LocalMessageDuplexStream from 'post-message-stream' -import { initProvider } from '@metamask/inpage-provider' - -// TODO:deprecate:2020 -import setupWeb3 from './lib/setupWeb3' -/* eslint-enable import/first */ +import { initializeProvider } from '@metamask/inpage-provider' restoreContextAfterImports() @@ -49,24 +45,12 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') // setup background connection const metamaskStream = new LocalMessageDuplexStream({ - name: 'inpage', - target: 'contentscript', + name: 'metamask-inpage', + target: 'metamask-contentscript', }) -initProvider({ +initializeProvider({ connectionStream: metamaskStream, + logger: log, + shouldShimWeb3: true, }) - -// TODO:deprecate:2020 -// Setup web3 - -if (typeof window.web3 === 'undefined') { - // proxy web3, assign to window, and set up site auto reload - setupWeb3(log) -} else { - log.warn(`MetaMask detected another web3. - MetaMask will not work reliably with another web3 extension. - This usually happens if you have two MetaMasks installed, - or MetaMask and another web3 extension. Please remove one - and try again.`) -} diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js index a943b31b1..3661259bd 100644 --- a/app/scripts/lib/ComposableObservableStore.js +++ b/app/scripts/lib/ComposableObservableStore.js @@ -1,4 +1,4 @@ -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' /** * An ObservableStore that can composes a flat diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index a22d56760..850c39291 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -9,7 +9,7 @@ import EthQuery from 'eth-query' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import log from 'loglevel' import pify from 'pify' import Web3 from 'web3' diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js index dc73b80f8..fc8ce03ce 100644 --- a/app/scripts/lib/decrypt-message-manager.js +++ b/app/scripts/lib/decrypt-message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js index 5079c8be1..d563cae0b 100644 --- a/app/scripts/lib/encryption-public-key-manager.js +++ b/app/scripts/lib/encryption-public-key-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' import createId from './random-id' diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js index d93030f2c..a6b7a99de 100644 --- a/app/scripts/lib/enums.js +++ b/app/scripts/lib/enums.js @@ -23,7 +23,8 @@ const MESSAGE_TYPE = { ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey', ETH_SIGN: 'eth_sign', ETH_SIGN_TYPED_DATA: 'eth_signTypedData', - LOG_WEB3_USAGE: 'metamask_logInjectedWeb3Usage', + GET_PROVIDER_STATE: 'metamask_getProviderState', + LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage', PERSONAL_SIGN: 'personal_sign', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', diff --git a/app/scripts/lib/fetch-with-timeout.js b/app/scripts/lib/fetch-with-timeout.js index bca8d652d..a9e895b12 100644 --- a/app/scripts/lib/fetch-with-timeout.js +++ b/app/scripts/lib/fetch-with-timeout.js @@ -1,10 +1,10 @@ const fetchWithTimeout = ({ timeout = 120000 } = {}) => { return async function _fetch(url, opts) { const abortController = new window.AbortController() - const abortSignal = abortController.signal + const { signal } = abortController const f = window.fetch(url, { ...opts, - signal: abortSignal, + signal, }) const timer = setTimeout(() => abortController.abort(), timeout) diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 6f6a208e2..872f97b0d 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import createId from './random-id' diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 4e8b66de7..52d20588f 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -1,5 +1,5 @@ import EventEmitter from 'events' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ethUtil from 'ethereumjs-util' import { ethErrors } from 'eth-json-rpc-errors' import log from 'loglevel' diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index b87047cd2..fdbc265e9 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -21,7 +21,6 @@ const handlerMap = handlers.reduce((map, handler) => { * Eventually, we'll want to extract this middleware into its own package. * * @param {Object} opts - The middleware options - * @param {string} opts.origin - The origin for the middleware stack * @param {Function} opts.sendMetrics - A function for sending a metrics event * @returns {(req: Object, res: Object, next: Function, end: Function) => void} */ diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js new file mode 100644 index 000000000..32b3c18c6 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js @@ -0,0 +1,46 @@ +import { MESSAGE_TYPE } from '../../enums' + +/** + * This RPC method gets background state relevant to the provider. + * The background sends RPC notifications on state changes, but the provider + * first requests state on initialization. + */ + +const getProviderState = { + methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE], + implementation: getProviderStateHandler, +} +export default getProviderState + +/** + * @typedef {Object} ProviderStateHandlerResult + * @property {string} chainId - The current chain ID. + * @property {boolean} isUnlocked - Whether the extension is unlocked or not. + * @property {string} networkVersion - The current network ID. + */ + +/** + * @typedef {Object} ProviderStateHandlerOptions + * @property {() => ProviderStateHandlerResult} getProviderState - A function that + * gets the current provider state. + */ + +/** + * @param {import('json-rpc-engine').JsonRpcRequest<[]>} req - The JSON-RPC request object. + * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {Function} _next - The json-rpc-engine 'next' callback. + * @param {Function} end - The json-rpc-engine 'end' callback. + * @param {ProviderStateHandlerOptions} options + */ +async function getProviderStateHandler( + req, + res, + _next, + end, + { getProviderState: _getProviderState }, +) { + res.result = { + ...(await _getProviderState(req.origin)), + } + return end() +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.js index 74e26b675..41011778a 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.js @@ -1,5 +1,6 @@ -import logWeb3Usage from './log-web3-usage' +import getProviderState from './get-provider-state' +import logWeb3ShimUsage from './log-web3-shim-usage' import watchAsset from './watch-asset' -const handlers = [logWeb3Usage, watchAsset] +const handlers = [getProviderState, logWeb3ShimUsage, watchAsset] export default handlers diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js new file mode 100644 index 000000000..21e908000 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js @@ -0,0 +1,57 @@ +import { MESSAGE_TYPE } from '../../enums' + +/** + * This RPC method is called by the inpage provider whenever it detects the + * accessing of a non-existent property on our window.web3 shim. + * We collect this data to understand which sites are breaking due to the + * removal of our window.web3. + */ + +const logWeb3ShimUsage = { + methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], + implementation: logWeb3ShimUsageHandler, +} +export default logWeb3ShimUsage + +/** + * @typedef {Object} LogWeb3ShimUsageOptions + * @property {Function} sendMetrics - A function that registers a metrics event. + * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim + * usage state for the given origin. + * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim + * usage for a particular origin. + */ + +/** + * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. + * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {Function} _next - The json-rpc-engine 'next' callback. + * @param {Function} end - The json-rpc-engine 'end' callback. + * @param {LogWeb3ShimUsageOptions} options + */ +function logWeb3ShimUsageHandler( + req, + res, + _next, + end, + { sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, +) { + const { origin } = req + if (getWeb3ShimUsageState(origin) === undefined) { + setWeb3ShimUsageRecorded(origin) + + sendMetrics({ + event: `Website Accessed window.web3 Shim`, + category: 'inpage_provider', + eventContext: { + referrer: { + url: origin, + }, + }, + excludeMetaMetricsId: true, + }) + } + + res.result = true + return end() +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js deleted file mode 100644 index c80303223..000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js +++ /dev/null @@ -1,63 +0,0 @@ -import { MESSAGE_TYPE } from '../../enums' - -/** - * This RPC method is called by our inpage web3 proxy whenever window.web3 is - * accessed. We're collecting data on window.web3 usage so that we can warn - * website maintainers, and possibly our users, before we remove window.web3 - * by November 16, 2020. - */ - -const logWeb3Usage = { - methodNames: [MESSAGE_TYPE.LOG_WEB3_USAGE], - implementation: logWeb3UsageHandler, -} -export default logWeb3Usage - -const recordedWeb3Usage = {} - -/** - * @typedef {Object} LogWeb3UsageOptions - * @property {string} origin - The origin of the request. - * @property {Function} sendMetrics - A function that registers a metrics event. - */ - -/** - * @typedef {Object} LogWeb3UsageParam - * @property {string} action - The action taken (get or set). - * @property {string} name - The window.web3 property name subject to the action. - */ - -/** - * @param {import('json-rpc-engine').JsonRpcRequest<[LogWeb3UsageParam]>} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. - * @param {Function} _next - The json-rpc-engine 'next' callback. - * @param {Function} end - The json-rpc-engine 'end' callback. - * @param {LogWeb3UsageOptions} options - */ -function logWeb3UsageHandler(req, res, _next, end, { origin, sendMetrics }) { - const { action, path } = req.params[0] - - if (!recordedWeb3Usage[origin]) { - recordedWeb3Usage[origin] = {} - } - if (!recordedWeb3Usage[origin][path]) { - recordedWeb3Usage[origin][path] = true - - sendMetrics( - { - event: `Website Used window.web3`, - category: 'inpage_provider', - properties: { action, web3Path: path }, - referrer: { - url: origin, - }, - }, - { - excludeMetaMetricsId: true, - }, - ) - } - - res.result = true - return end() -} diff --git a/app/scripts/lib/setupWeb3.js b/app/scripts/lib/setupWeb3.js deleted file mode 100644 index 9dc74ddb2..000000000 --- a/app/scripts/lib/setupWeb3.js +++ /dev/null @@ -1,251 +0,0 @@ -/*global Web3*/ - -// TODO:deprecate:2020 -// Delete this file - -import web3Entitites from './web3-entities.json' -import 'web3/dist/web3.min' - -const shouldLogUsage = ![ - 'docs.metamask.io', - 'metamask.github.io', - 'metamask.io', -].includes(window.location.hostname) - -/** - * To understand how we arrived at this implementation, please see: - * https://github.com/ethereum/web3.js/blob/0.20.7/DOCUMENTATION.md - */ -export default function setupWeb3(log) { - // export web3 as a global, checking for usage - let reloadInProgress = false - let lastTimeUsed - let lastSeenNetwork - let hasBeenWarned = false - - const web3 = new Web3(window.ethereum) - web3.setProvider = function () { - log.debug('MetaMask - overrode web3.setProvider') - } - Object.defineProperty(web3, '__isMetaMaskShim__', { - value: true, - enumerable: false, - configurable: false, - writable: false, - }) - - Object.defineProperty(window.ethereum, '_web3Ref', { - enumerable: false, - writable: true, - configurable: true, - value: web3.eth, - }) - - // Setup logging of nested property usage - if (shouldLogUsage) { - // web3 namespaces with common and uncommon dapp actions - const includedTopKeys = [ - 'eth', - 'db', - 'shh', - 'net', - 'personal', - 'bzz', - 'version', - ] - - // For each top-level property, create appropriate Proxy traps for all of - // their properties - includedTopKeys.forEach((topKey) => { - const applyTrapKeys = new Map() - const getTrapKeys = new Map() - - Object.keys(web3[topKey]).forEach((key) => { - const path = `web3.${topKey}.${key}` - - if (web3Entitites[path]) { - if (web3Entitites[path] === 'function') { - applyTrapKeys.set(key, path) - } else { - getTrapKeys.set(key, path) - } - } - }) - - // Create apply traps for function properties - for (const [key, path] of applyTrapKeys) { - web3[topKey][key] = new Proxy(web3[topKey][key], { - apply: (...params) => { - try { - window.ethereum.request({ - method: 'metamask_logInjectedWeb3Usage', - params: [ - { - action: 'apply', - path, - }, - ], - }) - } catch (error) { - log.debug('Failed to log web3 usage.', error) - } - - // Call function normally - return Reflect.apply(...params) - }, - }) - } - - // Create get trap for non-function properties - web3[topKey] = new Proxy(web3[topKey], { - get: (web3Prop, key, ...params) => { - const name = stringifyKey(key) - - if (getTrapKeys.has(name)) { - try { - window.ethereum.request({ - method: 'metamask_logInjectedWeb3Usage', - params: [ - { - action: 'get', - path: getTrapKeys.get(name), - }, - ], - }) - } catch (error) { - log.debug('Failed to log web3 usage.', error) - } - } - - // return value normally - return Reflect.get(web3Prop, key, ...params) - }, - }) - }) - - const topLevelFunctions = [ - 'isConnected', - 'setProvider', - 'reset', - 'sha3', - 'toHex', - 'toAscii', - 'fromAscii', - 'toDecimal', - 'fromDecimal', - 'fromWei', - 'toWei', - 'toBigNumber', - 'isAddress', - ] - - // apply-trap top-level functions - topLevelFunctions.forEach((key) => { - // This type check is probably redundant, but we've been burned before. - if (typeof web3[key] === 'function') { - web3[key] = new Proxy(web3[key], { - apply: (...params) => { - try { - window.ethereum.request({ - method: 'metamask_logInjectedWeb3Usage', - params: [ - { - action: 'apply', - path: `web3.${key}`, - }, - ], - }) - } catch (error) { - log.debug('Failed to log web3 usage.', error) - } - - // Call function normally - return Reflect.apply(...params) - }, - }) - } - }) - } - - const web3Proxy = new Proxy(web3, { - get: (...params) => { - // get the time of use - lastTimeUsed = Date.now() - - // show warning once on web3 access - if (!hasBeenWarned) { - console.warn( - `MetaMask: We will stop injecting web3 in Q4 2020.\nPlease see this article for more information: https://medium.com/metamask/no-longer-injecting-web3-js-4a899ad6e59e`, - ) - hasBeenWarned = true - } - - // return value normally - return Reflect.get(...params) - }, - }) - - Object.defineProperty(window, 'web3', { - enumerable: false, - writable: true, - configurable: true, - value: web3Proxy, - }) - log.debug('MetaMask - injected web3') - - window.ethereum._publicConfigStore.subscribe((state) => { - // if the auto refresh on network change is false do not - // do anything - if (!window.ethereum.autoRefreshOnNetworkChange) { - return - } - - // if reload in progress, no need to check reload logic - if (reloadInProgress) { - return - } - - const currentNetwork = state.networkVersion - - // set the initial network - if (!lastSeenNetwork) { - lastSeenNetwork = currentNetwork - return - } - - // skip reload logic if web3 not used - if (!lastTimeUsed) { - return - } - - // if network did not change, exit - if (currentNetwork === lastSeenNetwork) { - return - } - - // initiate page reload - reloadInProgress = true - const timeSinceUse = Date.now() - lastTimeUsed - // if web3 was recently used then delay the reloading of the page - if (timeSinceUse > 500) { - triggerReset() - } else { - setTimeout(triggerReset, 500) - } - }) -} - -// reload the page -function triggerReset() { - window.location.reload() -} - -/** - * Returns a "stringified" key. Keys that are already strings are returned - * unchanged, and any non-string values are returned as "typeof ". - * - * @param {any} key - The key to stringify - */ -function stringifyKey(key) { - return typeof key === 'string' ? key : `typeof ${typeof key}` -} diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index c4f5fd54a..6bcfe846b 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -1,6 +1,6 @@ import EventEmitter from 'events' import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ethErrors } from 'eth-json-rpc-errors' import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util' import { isValidAddress } from 'ethereumjs-util' diff --git a/app/scripts/lib/web3-entities.json b/app/scripts/lib/web3-entities.json deleted file mode 100644 index 0e9b435bc..000000000 --- a/app/scripts/lib/web3-entities.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "web3.bzz.blockNetworkRead": "function", - "web3.bzz.download": "function", - "web3.bzz.get": "function", - "web3.bzz.getHive": "function", - "web3.bzz.getInfo": "function", - "web3.bzz.hive": "TRAP", - "web3.bzz.info": "TRAP", - "web3.bzz.modify": "function", - "web3.bzz.put": "function", - "web3.bzz.retrieve": "function", - "web3.bzz.store": "function", - "web3.bzz.swapEnabled": "function", - "web3.bzz.syncEnabled": "function", - "web3.bzz.upload": "function", - "web3.db.getHex": "function", - "web3.db.getString": "function", - "web3.db.putHex": "function", - "web3.db.putString": "function", - "web3.eth.accounts": "object", - "web3.eth.blockNumber": "TRAP", - "web3.eth.call": "function", - "web3.eth.coinbase": "object", - "web3.eth.compile": "object", - "web3.eth.estimateGas": "function", - "web3.eth.gasPrice": "TRAP", - "web3.eth.getAccounts": "function", - "web3.eth.getBalance": "function", - "web3.eth.getBlock": "function", - "web3.eth.getBlockNumber": "function", - "web3.eth.getBlockTransactionCount": "function", - "web3.eth.getBlockUncleCount": "function", - "web3.eth.getCode": "function", - "web3.eth.getCoinbase": "function", - "web3.eth.getCompilers": "function", - "web3.eth.getGasPrice": "function", - "web3.eth.getHashrate": "function", - "web3.eth.getMining": "function", - "web3.eth.getProtocolVersion": "function", - "web3.eth.getStorageAt": "function", - "web3.eth.getSyncing": "function", - "web3.eth.getTransaction": "function", - "web3.eth.getTransactionCount": "function", - "web3.eth.getTransactionFromBlock": "function", - "web3.eth.getTransactionReceipt": "function", - "web3.eth.getUncle": "function", - "web3.eth.getWork": "function", - "web3.eth.hashrate": "TRAP", - "web3.eth.iban": "function", - "web3.eth.mining": "TRAP", - "web3.eth.protocolVersion": "TRAP", - "web3.eth.sendIBANTransaction": "function", - "web3.eth.sendRawTransaction": "function", - "web3.eth.sendTransaction": "function", - "web3.eth.sign": "function", - "web3.eth.signTransaction": "function", - "web3.eth.submitWork": "function", - "web3.eth.syncing": "TRAP", - "web3.net.getListening": "function", - "web3.net.getPeerCount": "function", - "web3.net.listening": "TRAP", - "web3.net.peerCount": "TRAP", - "web3.personal.ecRecover": "function", - "web3.personal.getListAccounts": "function", - "web3.personal.importRawKey": "function", - "web3.personal.listAccounts": "TRAP", - "web3.personal.lockAccount": "function", - "web3.personal.newAccount": "function", - "web3.personal.sendTransaction": "function", - "web3.personal.sign": "function", - "web3.personal.unlockAccount": "function", - "web3.providers.HttpProvider": "function", - "web3.providers.IpcProvider": "function", - "web3.shh.addPrivateKey": "function", - "web3.shh.addSymKey": "function", - "web3.shh.deleteKeyPair": "function", - "web3.shh.deleteSymKey": "function", - "web3.shh.generateSymKeyFromPassword": "function", - "web3.shh.getPrivateKey": "function", - "web3.shh.getPublicKey": "function", - "web3.shh.getSymKey": "function", - "web3.shh.hasKeyPair": "function", - "web3.shh.hasSymKey": "function", - "web3.shh.info": "function", - "web3.shh.markTrustedPeer": "function", - "web3.shh.newKeyPair": "function", - "web3.shh.newSymKey": "function", - "web3.shh.post": "function", - "web3.shh.setMaxMessageSize": "function", - "web3.shh.setMinPoW": "function", - "web3.shh.version": "function", - "web3.version.api": "string", - "web3.version.ethereum": "TRAP", - "web3.version.getEthereum": "function", - "web3.version.getNetwork": "function", - "web3.version.getNode": "function", - "web3.version.getWhisper": "function", - "web3.version.network": "string", - "web3.version.node": "TRAP", - "web3.version.whisper": "TRAP" -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b1af9cbc2..9df882bf5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,9 +1,6 @@ import EventEmitter from 'events' - import pump from 'pump' import Dnode from 'dnode' -import ObservableStore from 'obs-store' -import asStream from 'obs-store/lib/asStream' import { JsonRpcEngine } from 'json-rpc-engine' import { debounce } from 'lodash' import createEngineStream from 'json-rpc-middleware-stream/engineStream' @@ -21,6 +18,7 @@ import nanoid from 'nanoid' import contractMap from '@metamask/contract-metadata' import { AddressBookController, + ApprovalController, CurrencyRateController, PhishingController, } from '@metamask/controllers' @@ -53,6 +51,7 @@ import TokenRatesController from './controllers/token-rates' import DetectTokensController from './controllers/detect-tokens' import SwapsController from './controllers/swaps' import { PermissionsController } from './controllers/permissions' +import { NOTIFICATION_NAMES } from './controllers/permissions/enums' import getRestrictedMethods from './controllers/permissions/restrictedMethods' import nodeify from './lib/nodeify' import accountImporter from './account-import-strategies' @@ -104,6 +103,11 @@ export default class MetamaskController extends EventEmitter { // next, we will initialize the controllers // controller initialization order matters + this.approvalController = new ApprovalController({ + showApprovalRequest: opts.showUserConfirmation, + defaultApprovalType: 'NO_TYPE', + }) + this.networkController = new NetworkController(initState.NetworkController) this.networkController.setInfuraProjectId(opts.infuraProjectId) @@ -219,13 +223,15 @@ export default class MetamaskController extends EventEmitter { initState: initState.KeyringController, encryptor: opts.encryptor || undefined, }) - this.keyringController.memStore.subscribe((s) => - this._onKeyringControllerUpdate(s), + this.keyringController.memStore.subscribe((state) => + this._onKeyringControllerUpdate(state), ) this.keyringController.on('unlock', () => this.emit('unlock')) + this.keyringController.on('lock', () => this._onLock()) this.permissionsController = new PermissionsController( { + approvals: this.approvalController, getKeyringAccounts: this.keyringController.getAccounts.bind( this.keyringController, ), @@ -233,6 +239,7 @@ export default class MetamaskController extends EventEmitter { getUnlockPromise: this.appStateController.getUnlockPromise.bind( this.appStateController, ), + isUnlocked: this.isUnlocked.bind(this), notifyDomain: this.notifyConnections.bind(this), notifyAllDomains: this.notifyAllConnections.bind(this), preferences: this.preferencesController.store, @@ -348,6 +355,9 @@ export default class MetamaskController extends EventEmitter { tokenRatesStore: this.tokenRatesController.store, }) + // ensure isClientOpenAndUnlocked is updated when memState updates + this.on('update', (memState) => this._onStateUpdate(memState)) + this.store.updateStructure({ AppStateController: this.appStateController.store, TransactionController: this.txController.store, @@ -390,8 +400,8 @@ export default class MetamaskController extends EventEmitter { PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, SwapsController: this.swapsController.store, - // ENS Controller EnsController: this.ensController.store, + ApprovalController: this.approvalController, }) this.memStore.subscribe(this.sendUpdate.bind(this)) @@ -450,38 +460,37 @@ export default class MetamaskController extends EventEmitter { } /** - * Constructor helper: initialize a public config store. - * This store is used to make some config info available to Dapps synchronously. - */ - createPublicConfigStore() { - // subset of state for metamask inpage provider - const publicConfigStore = new ObservableStore() - const { networkController } = this - - // setup memStore subscription hooks - this.on('update', updatePublicConfigStore) - updatePublicConfigStore(this.getState()) - - publicConfigStore.destroy = () => { - this.removeEventListener && - this.removeEventListener('update', updatePublicConfigStore) - } - - function updatePublicConfigStore(memState) { - const chainId = networkController.getCurrentChainId() - if (memState.network !== 'loading') { - publicConfigStore.putState(selectPublicState(chainId, memState)) - } + * Gets relevant state for the provider of an external origin. + * + * @param {string} origin - The origin to get the provider state for. + * @returns {Promise<{ + * isUnlocked: boolean, + * networkVersion: string, + * chainId: string, + * accounts: string[], + * }>} An object with relevant state properties. + */ + async getProviderState(origin) { + return { + isUnlocked: this.isUnlocked(), + ...this.getProviderNetworkState(), + accounts: await this.permissionsController.getAccounts(origin), } + } - function selectPublicState(chainId, { isUnlocked, network }) { - return { - isUnlocked, - chainId, - networkVersion: network, - } + /** + * Gets network state relevant for external providers. + * + * @param {Object} [memState] - The MetaMask memState. If not provided, + * this function will retrieve the most recent state. + * @returns {Object} An object with relevant network state properties. + */ + getProviderNetworkState(memState) { + const { network } = memState || this.getState() + return { + chainId: this.networkController.getCurrentChainId(), + networkVersion: network, } - return publicConfigStore } //============================================================================= @@ -512,16 +521,16 @@ export default class MetamaskController extends EventEmitter { */ getApi() { const { + alertController, keyringController, + metaMetricsController, networkController, onboardingController, - alertController, permissionsController, preferencesController, + swapsController, threeBoxController, txController, - swapsController, - metaMetricsController, } = this return { @@ -704,8 +713,12 @@ export default class MetamaskController extends EventEmitter { alertController, ), setUnconnectedAccountAlertShown: nodeify( - this.alertController.setUnconnectedAccountAlertShown, - this.alertController, + alertController.setUnconnectedAccountAlertShown, + alertController, + ), + setWeb3ShimUsageAlertDismissed: nodeify( + alertController.setWeb3ShimUsageAlertDismissed, + alertController, ), // 3Box @@ -1813,8 +1826,7 @@ export default class MetamaskController extends EventEmitter { const mux = setupMultiplex(connectionStream) // messages between inpage and background - this.setupProviderConnection(mux.createStream('provider'), sender) - this.setupPublicConfig(mux.createStream('publicConfig')) + this.setupProviderConnection(mux.createStream('metamask-provider'), sender) } /** @@ -1971,12 +1983,19 @@ export default class MetamaskController extends EventEmitter { engine.push( createMethodMiddleware({ origin, + getProviderState: this.getProviderState.bind(this), sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( this.preferencesController, ), + getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( + this.alertController, + ), + setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind( + this.alertController, + ), }), ) // filter and subscription polyfills @@ -1993,29 +2012,6 @@ export default class MetamaskController extends EventEmitter { return engine } - /** - * A method for providing our public config info over a stream. - * This includes info we like to be synchronous if possible, like - * the current selected account, and network ID. - * - * Since synchronous methods have been deprecated in web3, - * this is a good candidate for deprecation. - * - * @param {*} outStream - The stream to provide public config over. - */ - setupPublicConfig(outStream) { - const configStore = this.createPublicConfigStore() - const configStream = asStream(configStore) - - pump(configStream, outStream, (err) => { - configStore.destroy() - configStream.destroy() - if (err) { - log.error(err) - } - }) - } - /** * Adds a reference to a connection by origin. Ignores the 'metamask' origin. * Caller must ensure that the returned id is stored such that the reference @@ -2066,37 +2062,51 @@ export default class MetamaskController extends EventEmitter { /** * Causes the RPC engines associated with the connections to the given origin * to emit a notification event with the given payload. - * Does nothing if the extension is locked or the origin is unknown. + * + * The caller is responsible for ensuring that only permitted notifications + * are sent. + * + * Ignores unknown origins. * * @param {string} origin - The connection's origin string. * @param {any} payload - The event payload. */ notifyConnections(origin, payload) { const connections = this.connections[origin] - if (!this.isUnlocked() || !connections) { - return - } - Object.values(connections).forEach((conn) => { - conn.engine && conn.engine.emit('notification', payload) - }) + if (connections) { + Object.values(connections).forEach((conn) => { + if (conn.engine) { + conn.engine.emit('notification', payload) + } + }) + } } /** * Causes the RPC engines associated with all connections to emit a * notification event with the given payload. - * Does nothing if the extension is locked. * - * @param {any} payload - The event payload. + * If the "payload" parameter is a function, the payload for each connection + * will be the return value of that function called with the connection's + * origin. + * + * The caller is responsible for ensuring that only permitted notifications + * are sent. + * + * @param {any} payload - The event payload, or payload getter function. */ notifyAllConnections(payload) { - if (!this.isUnlocked()) { - return - } + const getPayload = + typeof payload === 'function' + ? (origin) => payload(origin) + : () => payload Object.values(this.connections).forEach((origin) => { Object.values(origin).forEach((conn) => { - conn.engine && conn.engine.emit('notification', payload) + if (conn.engine) { + conn.engine.emit('notification', getPayload(origin)) + } }) }) } @@ -2125,6 +2135,51 @@ export default class MetamaskController extends EventEmitter { this.accountTracker.syncWithAddresses(addresses) } + /** + * Handle global unlock, triggered by KeyringController unlock. + * Notifies all connections that the extension is unlocked. + */ + _onUnlock() { + this.notifyAllConnections((origin) => { + return { + method: NOTIFICATION_NAMES.unlockStateChanged, + params: { + isUnlocked: true, + accounts: this.permissionsController.getAccounts(origin), + }, + } + }) + this.emit('unlock') + } + + /** + * Handle global lock, triggered by KeyringController lock. + * Notifies all connections that the extension is locked. + */ + _onLock() { + this.notifyAllConnections({ + method: NOTIFICATION_NAMES.unlockStateChanged, + params: { + isUnlocked: false, + }, + }) + this.emit('lock') + } + + /** + * Handle memory state updates. + * - Ensure isClientOpenAndUnlocked is updated + * - Notifies all connections with the new provider network state + * - The external providers handle diffing the state + */ + _onStateUpdate(newState) { + this.isClientOpenAndUnlocked = newState.isUnlocked && this._isClientOpen + this.notifyAllConnections({ + method: NOTIFICATION_NAMES.chainChanged, + params: this.getProviderNetworkState(newState), + }) + } + // misc /** diff --git a/babel.config.js b/babel.config.js index 9f3af9523..f340966ab 100644 --- a/babel.config.js +++ b/babel.config.js @@ -6,7 +6,7 @@ module.exports = function (api) { '@babel/preset-env', { targets: { - browsers: ['chrome >= 58', 'firefox >= 56.2'], + browsers: ['chrome >= 63', 'firefox >= 56.2'], }, }, ], diff --git a/development/build/index.js b/development/build/index.js index facf87b61..7c5aeb6a2 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -79,6 +79,7 @@ function defineAllTasks() { clean, styleTasks.prod, composeParallel(scriptTasks.test, staticTasks.prod, manifestTasks.test), + zip, ), ) diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index fd514fa8d..1bea36d73 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -55,6 +55,9 @@ async function start() { }) .join(', ') + const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html` + const coverageLink = `Report` + // links to bundle browser builds const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/deps-viz/background/index.html` const depVizLink = `background` @@ -65,6 +68,7 @@ async function start() { const contentRows = [ `builds: ${buildLinks}`, `bundle viz: ${bundleLinks}`, + `code coverage: ${coverageLink}`, `dep viz: ${depVizLink}`, `all artifacts`, ] diff --git a/package.json b/package.json index f2e806440..2282a5013 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", "test:coverage:strict": "nyc --check-coverage yarn test:unit:strict", "test:coverage:path": "nyc --check-coverage yarn test:unit:path", - "test:coveralls-upload": "if [ \"$COVERALLS_REPO_TOKEN\" ]; then nyc report --reporter=text-lcov | coveralls; fi", "ganache:start": "./development/run-ganache", "sentry:publish": "node ./development/sentry-publish.js", "lint": "prettier --check ./**/*.json && eslint . --ext js && yarn lint:styles", @@ -76,14 +75,15 @@ "@formatjs/intl-relativetimeformat": "^5.2.6", "@fortawesome/fontawesome-free": "^5.13.0", "@material-ui/core": "^4.11.0", - "@metamask/contract-metadata": "^1.19.0", + "@metamask/contract-metadata": "^1.20.0", "@metamask/controllers": "^5.1.0", "@metamask/eth-ledger-bridge-keyring": "^0.2.6", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^1.4.0", - "@metamask/inpage-provider": "^6.1.0", + "@metamask/inpage-provider": "^8.0.1", "@metamask/jazzicon": "^2.0.0", "@metamask/logo": "^2.5.0", + "@metamask/obs-store": "^5.0.0", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.3.2", "@sentry/browser": "^5.26.0", @@ -141,7 +141,6 @@ "nanoid": "^2.1.6", "nonce-tracker": "^1.0.0", "obj-multiplex": "^1.0.0", - "obs-store": "^4.0.3", "pify": "^5.0.0", "post-message-stream": "^3.0.0", "promise-to-callback": "^1.0.0", @@ -215,7 +214,6 @@ "chromedriver": "^79.0.0", "concurrently": "^5.2.0", "copy-webpack-plugin": "^6.0.3", - "coveralls": "^3.0.0", "cross-spawn": "^7.0.3", "css-loader": "^2.1.1", "del": "^3.0.0", @@ -235,7 +233,7 @@ "fs-extra": "^8.1.0", "ganache-cli": "^6.12.1", "ganache-core": "^2.13.1", - "geckodriver": "^1.19.1", + "geckodriver": "^1.21.0", "get-port": "^5.1.0", "gulp": "^4.0.2", "gulp-autoprefixer": "^5.0.0", @@ -277,7 +275,7 @@ "remotedev-server": "^0.3.1", "resolve-url-loader": "^3.1.2", "sass-loader": "^7.0.1", - "selenium-webdriver": "^4.0.0-alpha.5", + "selenium-webdriver": "4.0.0-alpha.7", "serve-handler": "^6.1.2", "ses": "0.11.0", "sesify": "^4.2.1", diff --git a/shared/constants/alerts.js b/shared/constants/alerts.js new file mode 100644 index 000000000..4b2bed147 --- /dev/null +++ b/shared/constants/alerts.js @@ -0,0 +1,18 @@ +export const ALERT_TYPES = { + unconnectedAccount: 'unconnectedAccount', + web3ShimUsage: 'web3ShimUsage', + invalidCustomNetwork: 'invalidCustomNetwork', +} + +/** + * Alerts that can be enabled or disabled by the user. + */ +export const TOGGLEABLE_ALERT_TYPES = [ + ALERT_TYPES.unconnectedAccount, + ALERT_TYPES.web3ShimUsage, +] + +export const WEB3_SHIM_USAGE_ALERT_STATES = { + RECORDED: 1, + DISMISSED: 2, +} diff --git a/test/e2e/webdriver/chrome.js b/test/e2e/webdriver/chrome.js index f68237601..75e031475 100644 --- a/test/e2e/webdriver/chrome.js +++ b/test/e2e/webdriver/chrome.js @@ -5,8 +5,8 @@ const chrome = require('selenium-webdriver/chrome') * A wrapper around a {@code WebDriver} instance exposing Chrome-specific functionality */ class ChromeDriver { - static async build({ extensionPath, responsive, port }) { - const args = [`load-extension=${extensionPath}`] + static async build({ responsive, port }) { + const args = [`load-extension=dist/chrome`] if (responsive) { args.push('--auto-open-devtools-for-tabs') } diff --git a/test/e2e/webdriver/firefox.js b/test/e2e/webdriver/firefox.js index 0ce300da9..63c037b02 100644 --- a/test/e2e/webdriver/firefox.js +++ b/test/e2e/webdriver/firefox.js @@ -3,7 +3,7 @@ const os = require('os') const path = require('path') const { Builder, By, until } = require('selenium-webdriver') const firefox = require('selenium-webdriver/firefox') -const { Command } = require('selenium-webdriver/lib/command') +const { version } = require('../../../app/manifest/_base.json') /** * The prefix for temporary Firefox profiles. All Firefox profiles used for e2e tests @@ -12,20 +12,16 @@ const { Command } = require('selenium-webdriver/lib/command') */ const TEMP_PROFILE_PATH_PREFIX = path.join(os.tmpdir(), 'MetaMask-Fx-Profile') -const GeckoDriverCommand = { - INSTALL_ADDON: 'install addon', -} - /** * A wrapper around a {@code WebDriver} instance exposing Firefox-specific functionality */ class FirefoxDriver { /** * Builds a {@link FirefoxDriver} instance - * @param {{extensionPath: string}} options - the options for the build + * @param {Object} options - the options for the build * @returns {Promise<{driver: !ThenableWebDriver, extensionUrl: string, extensionId: string}>} */ - static async build({ extensionPath, responsive, port }) { + static async build({ responsive, port }) { const templateProfile = fs.mkdtempSync(TEMP_PROFILE_PATH_PREFIX) const options = new firefox.Options().setProfile(templateProfile) const builder = new Builder() @@ -38,9 +34,9 @@ class FirefoxDriver { const driver = builder.build() const fxDriver = new FirefoxDriver(driver) - await fxDriver.init() - - const extensionId = await fxDriver.installExtension(extensionPath) + const extensionId = await fxDriver.installExtension( + `builds/metamask-firefox-${version}.zip`, + ) const internalExtensionId = await fxDriver.getInternalId() if (responsive) { @@ -62,31 +58,13 @@ class FirefoxDriver { this._driver = driver } - /** - * Initializes the driver - * @returns {Promise} - */ - async init() { - await this._driver - .getExecutor() - .defineCommand( - GeckoDriverCommand.INSTALL_ADDON, - 'POST', - '/session/:sessionId/moz/addon/install', - ) - } - /** * Installs the extension at the given path * @param {string} addonPath - the path to the unpacked extension or XPI * @returns {Promise} the extension ID */ async installExtension(addonPath) { - const cmd = new Command(GeckoDriverCommand.INSTALL_ADDON) - .setParameter('path', path.resolve(addonPath)) - .setParameter('temporary', true) - - return await this._driver.execute(cmd) + return await this._driver.installAddon(addonPath, true) } /** diff --git a/test/e2e/webdriver/index.js b/test/e2e/webdriver/index.js index 024478065..8a41aac8a 100644 --- a/test/e2e/webdriver/index.js +++ b/test/e2e/webdriver/index.js @@ -6,13 +6,12 @@ const FirefoxDriver = require('./firefox') async function buildWebDriver({ responsive, port } = {}) { const browser = process.env.SELENIUM_BROWSER - const extensionPath = `dist/${browser}` const { driver: seleniumDriver, extensionId, extensionUrl, - } = await buildBrowserWebDriver(browser, { extensionPath, responsive, port }) + } = await buildBrowserWebDriver(browser, { responsive, port }) await setupFetchMocking(seleniumDriver) const driver = new Driver(seleniumDriver, browser, extensionUrl) diff --git a/test/unit/app/ComposableObservableStore.js b/test/unit/app/ComposableObservableStore.js index 961be1dc0..205e0a59b 100644 --- a/test/unit/app/ComposableObservableStore.js +++ b/test/unit/app/ComposableObservableStore.js @@ -1,5 +1,5 @@ import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import ComposableObservableStore from '../../../app/scripts/lib/ComposableObservableStore' describe('ComposableObservableStore', function () { diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js index 82be8808a..b7ab22ed6 100644 --- a/test/unit/app/controllers/detect-tokens-test.js +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import contracts from '@metamask/contract-metadata' import BigNumber from 'bignumber.js' @@ -85,6 +85,53 @@ describe('DetectTokensController', function () { sandbox.assert.notCalled(stub) }) + it('should skip adding tokens listed in hiddenTokens array', async function () { + sandbox.useFakeTimers() + network.setProviderType(MAINNET) + const controller = new DetectTokensController({ + preferences, + network, + keyringMemStore, + }) + controller.isOpen = true + controller.isUnlocked = true + + const contractAddresses = Object.keys(contracts) + const erc20ContractAddresses = contractAddresses.filter( + (contractAddress) => contracts[contractAddress].erc20 === true, + ) + + const existingTokenAddress = erc20ContractAddresses[0] + const existingToken = contracts[existingTokenAddress] + await preferences.addToken( + existingTokenAddress, + existingToken.symbol, + existingToken.decimals, + ) + + const tokenAddressToSkip = erc20ContractAddresses[1] + + sandbox + .stub(controller, '_getTokenBalances') + .callsFake((tokensToDetect) => + tokensToDetect.map((token) => + token === tokenAddressToSkip ? new BigNumber(10) : 0, + ), + ) + + await preferences.removeToken(tokenAddressToSkip) + + await controller.detectNewTokens() + + assert.deepEqual(preferences.store.getState().tokens, [ + { + address: existingTokenAddress.toLowerCase(), + decimals: existingToken.decimals, + symbol: existingToken.symbol, + }, + ]) + }) + it('should check and add tokens while on main network', async function () { sandbox.useFakeTimers() network.setProviderType(MAINNET) diff --git a/test/unit/app/controllers/ens-controller-test.js b/test/unit/app/controllers/ens-controller-test.js index 0fbaf33ac..dd116e69f 100644 --- a/test/unit/app/controllers/ens-controller-test.js +++ b/test/unit/app/controllers/ens-controller-test.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import EnsController from '../../../../app/scripts/controllers/ens' const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 99bd3f957..2e1c4021d 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -1022,7 +1022,7 @@ describe('MetaMaskController', function () { } streamTest.write( { - name: 'provider', + name: 'metamask-provider', data: message, }, null, @@ -1061,7 +1061,7 @@ describe('MetaMaskController', function () { } streamTest.write( { - name: 'provider', + name: 'metamask-provider', data: message, }, null, diff --git a/test/unit/app/controllers/permissions/mocks.js b/test/unit/app/controllers/permissions/mocks.js index 9040f6b12..48937faf2 100644 --- a/test/unit/app/controllers/permissions/mocks.js +++ b/test/unit/app/controllers/permissions/mocks.js @@ -1,6 +1,8 @@ import { ethErrors, ERROR_CODES } from 'eth-json-rpc-errors' import deepFreeze from 'deep-freeze-strict' +import { ApprovalController } from '@metamask/controllers' + import _getRestrictedMethods from '../../../../../app/scripts/controllers/permissions/restrictedMethods' import { @@ -32,8 +34,6 @@ const keyringAccounts = deepFreeze([ '0xcc74c7a59194e5d9268476955650d1e285be703c', ]) -const getKeyringAccounts = async () => [...keyringAccounts] - const getIdentities = () => { return keyringAccounts.reduce((identities, address, index) => { identities[address] = { address, name: `Account ${index}` } @@ -62,8 +62,6 @@ const getRestrictedMethods = (permController) => { } } -const getUnlockPromise = () => Promise.resolve() - /** * Gets default mock constructor options for a permissions controller. * @@ -71,10 +69,14 @@ const getUnlockPromise = () => Promise.resolve() */ export function getPermControllerOpts() { return { - showPermissionRequest: noop, - getKeyringAccounts, - getUnlockPromise, + approvals: new ApprovalController({ + showApprovalRequest: noop, + defaultApprovalType: 'NO_TYPE', + }), + getKeyringAccounts: async () => [...keyringAccounts], + getUnlockPromise: () => Promise.resolve(), getRestrictedMethods, + isUnlocked: () => true, notifyDomain: noop, notifyAllDomains: noop, preferences: { @@ -86,6 +88,7 @@ export function getPermControllerOpts() { }, subscribe: noop, }, + showPermissionRequest: noop, } } @@ -426,9 +429,9 @@ export const getters = deepFreeze({ message: `Pending approval with id '${id}' or origin '${origin}' already exists.`, } }, - requestAlreadyPending: () => { + requestAlreadyPending: (origin) => { return { - message: 'Permissions request already pending; please wait.', + message: `Request of type 'wallet_requestPermissions' already pending for origin ${origin}. Please wait.`, } }, }, @@ -467,7 +470,7 @@ export const getters = deepFreeze({ removedAccounts: () => { return { method: NOTIFICATION_NAMES.accountsChanged, - result: [], + params: [], } }, @@ -480,7 +483,7 @@ export const getters = deepFreeze({ newAccounts: (accounts) => { return { method: NOTIFICATION_NAMES.accountsChanged, - result: accounts, + params: accounts, } }, }, @@ -586,17 +589,17 @@ export const getters = deepFreeze({ }, /** - * Gets a wallet_sendDomainMetadata RPC request object. + * Gets a metamask_sendDomainMetadata RPC request object. * * @param {string} origin - The origin of the request * @param {Object} name - The domainMetadata name * @param {Array} [args] - Any other data for the request's domainMetadata * @returns {Object} An RPC request object */ - wallet_sendDomainMetadata: (origin, name, ...args) => { + metamask_sendDomainMetadata: (origin, name, ...args) => { return { origin, - method: 'wallet_sendDomainMetadata', + method: 'metamask_sendDomainMetadata', domainMetadata: { ...args, name, diff --git a/test/unit/app/controllers/permissions/permissions-controller-test.js b/test/unit/app/controllers/permissions/permissions-controller-test.js index 5f79bb443..2eee261f1 100644 --- a/test/unit/app/controllers/permissions/permissions-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-controller-test.js @@ -1,23 +1,17 @@ import { strict as assert } from 'assert' import { find } from 'lodash' -import nanoid from 'nanoid' import sinon from 'sinon' import { METADATA_STORE_KEY, METADATA_CACHE_MAX_SIZE, - WALLET_PREFIX, } from '../../../../../app/scripts/controllers/permissions/enums' -import { - PermissionsController, - addInternalMethodPrefix, -} from '../../../../../app/scripts/controllers/permissions' +import { PermissionsController } from '../../../../../app/scripts/controllers/permissions' import { getRequestUserApprovalHelper, grantPermissions } from './helpers' import { - noop, constants, getters, getNotifyDomain, @@ -53,6 +47,15 @@ const initPermController = (notifications = initNotifications()) => { } describe('permissions controller', function () { + describe('constructor', function () { + it('throws on undefined argument', function () { + assert.throws( + () => new PermissionsController(), + 'should throw on undefined argument', + ) + }) + }) + describe('getAccounts', function () { let permController @@ -187,7 +190,7 @@ describe('permissions controller', function () { assert.deepEqual( notifications[origin], [NOTIFICATIONS.removedAccounts()], - 'origin should have single wallet_accountsChanged:[] notification', + 'origin should have single metamask_accountsChanged:[] notification', ) }) @@ -1010,12 +1013,6 @@ describe('permissions controller', function () { }) it('does nothing if called on non-existing request', async function () { - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty on init', - ) - sinon.spy(permController, 'finalizePermissionsRequest') const request = PERMS.approvedRequest(REQUEST_IDS.a, null) @@ -1029,12 +1026,6 @@ describe('permissions controller', function () { permController.finalizePermissionsRequest.notCalled, 'should not call finalizePermissionRequest', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should still be empty after request', - ) }) it('rejects request with bad accounts param', async function () { @@ -1051,12 +1042,6 @@ describe('permissions controller', function () { await permController.approvePermissionsRequest(request, null) await rejectionPromise - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('rejects request with no permissions', async function () { @@ -1073,12 +1058,6 @@ describe('permissions controller', function () { ACCOUNTS.a.permitted, ) await requestRejection - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('approves valid request', async function () { @@ -1104,12 +1083,6 @@ describe('permissions controller', function () { PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted), 'should produce expected approved permissions', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approval', - ) }) it('approves valid requests regardless of order', async function () { @@ -1165,12 +1138,6 @@ describe('permissions controller', function () { PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted), 'second request should produce expected approved permissions', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approvals', - ) }) }) @@ -1183,22 +1150,14 @@ describe('permissions controller', function () { }) it('does nothing if called on non-existing request', async function () { - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty on init', + permController.approvals.add = sinon.fake.throws( + new Error('should not call add'), ) await assert.doesNotReject( permController.rejectPermissionsRequest(REQUEST_IDS.a), 'should not throw on non-existing request', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should still be empty after request', - ) }) it('rejects single existing request', async function () { @@ -1210,12 +1169,6 @@ describe('permissions controller', function () { await permController.rejectPermissionsRequest(REQUEST_IDS.a) await requestRejection - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after rejection', - ) }) it('rejects requests regardless of order', async function () { @@ -1239,12 +1192,6 @@ describe('permissions controller', function () { await requestRejection1 await requestRejection2 - - assert.equal( - permController.pendingApprovals.size, - 0, - 'pending approvals should be empty after approval', - ) }) }) @@ -1325,11 +1272,18 @@ describe('permissions controller', function () { }) it('notifyAccountsChanged records history and sends notification', async function () { + sinon.spy(permController, '_isUnlocked') + permController.notifyAccountsChanged( DOMAINS.a.origin, ACCOUNTS.a.permitted, ) + assert.ok( + permController._isUnlocked.calledOnce, + '_isUnlocked should have been called once', + ) + assert.ok( permController.permissionsLog.updateAccountsHistory.calledOnce, 'permissionsLog.updateAccountsHistory should have been called once', @@ -1342,6 +1296,25 @@ describe('permissions controller', function () { ) }) + it('notifyAccountsChanged does nothing if _isUnlocked returns false', async function () { + permController._isUnlocked = sinon.fake.returns(false) + + permController.notifyAccountsChanged( + DOMAINS.a.origin, + ACCOUNTS.a.permitted, + ) + + assert.ok( + permController._isUnlocked.calledOnce, + '_isUnlocked should have been called once', + ) + + assert.ok( + permController.permissionsLog.updateAccountsHistory.notCalled, + 'permissionsLog.updateAccountsHistory should not have been called', + ) + }) + it('notifyAccountsChanged throws on invalid origin', async function () { assert.throws( () => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted), @@ -1546,13 +1519,8 @@ describe('permissions controller', function () { }) describe('miscellanea and edge cases', function () { - let permController - - beforeEach(function () { - permController = initPermController() - }) - it('requestAccountsPermissionWithId calls _requestAccountsPermission with an explicit request ID', async function () { + const permController = initPermController() const _requestPermissions = sinon .stub(permController, '_requestPermissions') .resolves() @@ -1566,49 +1534,5 @@ describe('permissions controller', function () { ) _requestPermissions.restore() }) - - it('_addPendingApproval: should throw if adding origin twice', function () { - const id = nanoid() - const origin = DOMAINS.a - - permController._addPendingApproval(id, origin, noop, noop) - - const otherId = nanoid() - - assert.throws( - () => permController._addPendingApproval(otherId, origin, noop, noop), - ERRORS.pendingApprovals.duplicateOriginOrId(otherId, origin), - 'should throw expected error', - ) - - assert.equal( - permController.pendingApprovals.size, - 1, - 'pending approvals should have single entry', - ) - - assert.equal( - permController.pendingApprovalOrigins.size, - 1, - 'pending approval origins should have single item', - ) - - assert.deepEqual( - permController.pendingApprovals.get(id), - { origin, resolve: noop, reject: noop }, - 'pending approvals should have expected entry', - ) - - assert.ok( - permController.pendingApprovalOrigins.has(origin), - 'pending approval origins should have expected item', - ) - }) - - it('addInternalMethodPrefix', function () { - const str = 'foo' - const res = addInternalMethodPrefix(str) - assert.equal(res, WALLET_PREFIX + str, 'should prefix correctly') - }) }) }) diff --git a/test/unit/app/controllers/permissions/permissions-log-controller-test.js b/test/unit/app/controllers/permissions/permissions-log-controller-test.js index ac498a0e0..d98db8453 100644 --- a/test/unit/app/controllers/permissions/permissions-log-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-log-controller-test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import nanoid from 'nanoid' import { useFakeTimers } from 'sinon' @@ -286,7 +286,7 @@ describe('permissions log', function () { assert.equal(log.length, 0, 'log should be empty') const res = { foo: 'bar' } - const req1 = RPC_REQUESTS.wallet_sendDomainMetadata( + const req1 = RPC_REQUESTS.metamask_sendDomainMetadata( DOMAINS.c.origin, 'foobar', ) diff --git a/test/unit/app/controllers/permissions/permissions-middleware-test.js b/test/unit/app/controllers/permissions/permissions-middleware-test.js index 2888d3da5..9e2769ed6 100644 --- a/test/unit/app/controllers/permissions/permissions-middleware-test.js +++ b/test/unit/app/controllers/permissions/permissions-middleware-test.js @@ -18,6 +18,20 @@ const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters const { ACCOUNTS, DOMAINS, PERM_NAMES } = constants +const initPermController = () => { + return new PermissionsController({ + ...getPermControllerOpts(), + }) +} + +const createApprovalSpies = (permController) => { + sinon.spy(permController.approvals, '_add') +} + +const getNextApprovalId = (permController) => { + return permController.approvals._approvals.keys().next().value +} + const validatePermission = (perm, name, origin, caveats) => { assert.equal( name, @@ -36,12 +50,6 @@ const validatePermission = (perm, name, origin, caveats) => { } } -const initPermController = () => { - return new PermissionsController({ - ...getPermControllerOpts(), - }) -} - describe('permissions middleware', function () { describe('wallet_requestPermissions', function () { let permController @@ -52,6 +60,8 @@ describe('permissions middleware', function () { }) it('grants permissions on user approval', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -72,13 +82,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) const approvedReq = PERMS.approvedRequest( id, PERMS.requests.eth_accounts(), @@ -150,7 +159,7 @@ describe('permissions middleware', function () { await userApprovalPromise - const id1 = permController.pendingApprovals.keys().next().value + const id1 = getNextApprovalId(permController) const approvedReq1 = PERMS.approvedRequest( id1, PERMS.requests.eth_accounts(), @@ -219,7 +228,7 @@ describe('permissions middleware', function () { await userApprovalPromise - const id2 = permController.pendingApprovals.keys().next().value + const id2 = getNextApprovalId(permController) const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 }) await permController.approvePermissionsRequest( @@ -275,6 +284,8 @@ describe('permissions middleware', function () { }) it('rejects permissions on user rejection', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -298,13 +309,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) await permController.rejectPermissionsRequest(id) await requestRejection @@ -328,6 +338,8 @@ describe('permissions middleware', function () { }) it('rejects requests with unknown permissions', async function () { + createApprovalSpies(permController) + const aMiddleware = getPermissionsMiddleware( permController, DOMAINS.a.origin, @@ -349,10 +361,9 @@ describe('permissions middleware', function () { 'request should be rejected with correct error', ) - assert.equal( - permController.pendingApprovals.size, - 0, - 'perm controller should have no pending approvals', + assert.ok( + permController.approvals._add.notCalled, + 'no approval requests should have been added', ) assert.ok( @@ -367,7 +378,7 @@ describe('permissions middleware', function () { }) it('accepts only a single pending permissions request per origin', async function () { - const expectedError = ERRORS.pendingApprovals.requestAlreadyPending() + createApprovalSpies(permController) // two middlewares for two origins @@ -414,10 +425,9 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 2, - 'perm controller should have expected number of pending approvals', + assert.ok( + permController.approvals._add.calledTwice, + 'should have added two approval requests', ) // create and start processing second request for first origin, @@ -431,6 +441,10 @@ describe('permissions middleware', function () { userApprovalPromise = getUserApprovalPromise(permController) + const expectedError = ERRORS.pendingApprovals.requestAlreadyPending( + DOMAINS.a.origin, + ) + const requestApprovalFail = assert.rejects( aMiddleware(reqA2, resA2), expectedError, @@ -447,17 +461,20 @@ describe('permissions middleware', function () { 'response should have expected error and no result', ) - // first requests for both origins should remain - assert.equal( - permController.pendingApprovals.size, + permController.approvals._add.callCount, + 3, + 'should have attempted to create three pending approvals', + ) + assert.equal( + permController.approvals._approvals.size, 2, - 'perm controller should have expected number of pending approvals', + 'should only have created two pending approvals', ) // now, remaining pending requests should be approved without issue - for (const id of permController.pendingApprovals.keys()) { + for (const id of permController.approvals._approvals.keys()) { await permController.approvePermissionsRequest( PERMS.approvedRequest(id, PERMS.requests.test_method()), ) @@ -484,12 +501,6 @@ describe('permissions middleware', function () { 1, 'second origin should have single approved permission', ) - - assert.equal( - permController.pendingApprovals.size, - 0, - 'perm controller should have expected number of pending approvals', - ) }) }) @@ -609,6 +620,8 @@ describe('permissions middleware', function () { }) it('requests accounts for unpermitted origin, and approves on user approval', async function () { + createApprovalSpies(permController) + const userApprovalPromise = getUserApprovalPromise(permController) const aMiddleware = getPermissionsMiddleware( @@ -626,13 +639,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) const approvedReq = PERMS.approvedRequest( id, PERMS.requests.eth_accounts(), @@ -685,6 +697,8 @@ describe('permissions middleware', function () { }) it('requests accounts for unpermitted origin, and rejects on user rejection', async function () { + createApprovalSpies(permController) + const userApprovalPromise = getUserApprovalPromise(permController) const aMiddleware = getPermissionsMiddleware( @@ -705,13 +719,12 @@ describe('permissions middleware', function () { await userApprovalPromise - assert.equal( - permController.pendingApprovals.size, - 1, - 'perm controller should have single pending approval', + assert.ok( + permController.approvals._add.calledOnce, + 'should have added single approval request', ) - const id = permController.pendingApprovals.keys().next().value + const id = getNextApprovalId(permController) await permController.rejectPermissionsRequest(id) await requestRejection @@ -788,7 +801,7 @@ describe('permissions middleware', function () { // this will reject because of the already pending request await assert.rejects( cMiddleware({ ...req }, {}), - ERRORS.eth_requestAccounts.requestAlreadyPending(), + ERRORS.eth_requestAccounts.requestAlreadyPending(DOMAINS.c.origin), ) // now unlock and let through the first request @@ -808,7 +821,7 @@ describe('permissions middleware', function () { }) }) - describe('wallet_sendDomainMetadata', function () { + describe('metamask_sendDomainMetadata', function () { let permController, clock beforeEach(function () { @@ -828,7 +841,10 @@ describe('permissions middleware', function () { DOMAINS.c.origin, ) - const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) + const req = RPC_REQUESTS.metamask_sendDomainMetadata( + DOMAINS.c.origin, + name, + ) const res = {} await assert.doesNotReject(cMiddleware(req, res), 'should not reject') @@ -861,7 +877,10 @@ describe('permissions middleware', function () { extensionId, ) - const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) + const req = RPC_REQUESTS.metamask_sendDomainMetadata( + DOMAINS.c.origin, + name, + ) const res = {} await assert.doesNotReject(cMiddleware(req, res), 'should not reject') @@ -885,7 +904,10 @@ describe('permissions middleware', function () { DOMAINS.c.origin, ) - const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) + const req = RPC_REQUESTS.metamask_sendDomainMetadata( + DOMAINS.c.origin, + name, + ) const res = {} await assert.doesNotReject(cMiddleware(req, res), 'should not reject') @@ -907,7 +929,7 @@ describe('permissions middleware', function () { DOMAINS.c.origin, ) - const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin) + const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin) delete req.domainMetadata const res = {} diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 6a831c3e7..065d35782 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import sinon from 'sinon' import PreferencesController from '../../../../app/scripts/controllers/preferences' diff --git a/test/unit/app/controllers/swaps-test.js b/test/unit/app/controllers/swaps-test.js index 6aa3e0476..38f948088 100644 --- a/test/unit/app/controllers/swaps-test.js +++ b/test/unit/app/controllers/swaps-test.js @@ -4,7 +4,7 @@ import sinon from 'sinon' import { ethers } from 'ethers' import { mapValues } from 'lodash' import BigNumber from 'bignumber.js' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, diff --git a/test/unit/app/controllers/token-rates-controller.js b/test/unit/app/controllers/token-rates-controller.js index 1f342abe5..58e0009a1 100644 --- a/test/unit/app/controllers/token-rates-controller.js +++ b/test/unit/app/controllers/token-rates-controller.js @@ -1,6 +1,6 @@ import assert from 'assert' import sinon from 'sinon' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import TokenRatesController from '../../../../app/scripts/controllers/token-rates' describe('TokenRatesController', function () { diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 0e39f791d..e37c44a47 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -2,7 +2,7 @@ import { strict as assert } from 'assert' import EventEmitter from 'events' import ethUtil from 'ethereumjs-util' import EthTx from 'ethereumjs-tx' -import ObservableStore from 'obs-store' +import { ObservableStore } from '@metamask/obs-store' import sinon from 'sinon' import TransactionController from '../../../../../app/scripts/controllers/transactions' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index bdf987c8a..cacb39b09 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -321,7 +321,7 @@ function calcCustomGasLimit(customGasLimitInHex) { } function sumHexWEIsToRenderableEth(hexWEIs) { - const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes) + const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes) return formatETHFee( getValueFromWeiHex({ value: hexWEIsSum, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss index fd09c01ee..be2dcc3ed 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss @@ -35,6 +35,7 @@ color: #4eade7; position: absolute; + font-size: 0.75rem; top: 4px; right: 16px; cursor: pointer; diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js index 2bb89eafe..0ae4d85f9 100644 --- a/ui/app/components/app/home-notification/home-notification.component.js +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -1,101 +1,105 @@ -import React, { PureComponent } from 'react' +import React, { useState } from 'react' import classnames from 'classnames' -import { Tooltip as ReactTippy } from 'react-tippy' import PropTypes from 'prop-types' import Button from '../../ui/button' +import Checkbox from '../../ui/check-box' +import Tooltip from '../../ui/tooltip' -export default class HomeNotification extends PureComponent { - static contextTypes = { - metricsEvent: PropTypes.func, - } +const HomeNotification = ({ + acceptText, + checkboxText, + checkboxTooltipText, + classNames = [], + descriptionText, + ignoreText, + infoText, + onAccept, + onIgnore, +}) => { + const [checkboxState, setCheckBoxState] = useState(false) - static defaultProps = { - onAccept: null, - ignoreText: null, - onIgnore: null, - infoText: null, - } + const checkboxElement = checkboxText && ( + setCheckBoxState((checked) => !checked)} + /> + ) - static propTypes = { - acceptText: PropTypes.node.isRequired, - onAccept: PropTypes.func, - ignoreText: PropTypes.node, - onIgnore: PropTypes.func, - descriptionText: PropTypes.node.isRequired, - infoText: PropTypes.node, - classNames: PropTypes.array, - } - - handleAccept = () => { - this.props.onAccept() - } - - handleIgnore = () => { - this.props.onIgnore() - } - - render() { - const { - descriptionText, - acceptText, - onAccept, - ignoreText, - onIgnore, - infoText, - classNames = [], - } = this.props - - return ( -
-
-
- -
{descriptionText}
-
- {infoText ? ( - {infoText}

- } - offset={-36} - distance={36} - animation="none" - position="top" - arrow - theme="tippy-tooltip-home" - > - -
- ) : null} + return ( +
+
+
+
{descriptionText}
-
- {onAccept && acceptText ? ( - - ) : null} - {onIgnore && ignoreText ? ( -
+
+ {onAccept && acceptText ? ( + + ) : null} + {onIgnore && ignoreText ? ( + + ) : null} + {checkboxText ? ( +
+ {checkboxTooltipText ? ( + + {checkboxElement} + + ) : ( + checkboxElement + )} +
+ {checkboxText} + +
+ ) : null}
- ) - } +
+ ) } + +HomeNotification.propTypes = { + acceptText: PropTypes.node, + checkboxText: PropTypes.node, + checkboxTooltipText: PropTypes.node, + classNames: PropTypes.array, + descriptionText: PropTypes.node.isRequired, + ignoreText: PropTypes.node, + infoText: PropTypes.node, + onAccept: PropTypes.func, + onIgnore: PropTypes.func, +} + +export default HomeNotification diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss index 452681ac2..38173145b 100644 --- a/ui/app/components/app/home-notification/index.scss +++ b/ui/app/components/app/home-notification/index.scss @@ -1,14 +1,7 @@ -.tippy-tooltip { - // This looks weird, but its repeating the class name - // using interpolation for higher specificity. - &#{&}-home-theme { - background: rgba(36, 41, 46, 0.9); - color: $white; - border-radius: 8px; - } -} - .home-notification { + display: flex; + flex-flow: column; + justify-content: space-between; background: rgba(36, 41, 46, 0.9); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); border-radius: 8px; @@ -19,37 +12,62 @@ min-width: 472px; } - display: flex; - flex-flow: column; - justify-content: space-between; - - &__header-container { + &__content-container { display: flex; } - &__header { + &__content { display: flex; align-items: center; justify-content: space-between; } - &__icon { - height: 16px; - align-self: center; - } - &__text { @include H7; color: $white; - margin-left: 10px; - margin-right: 8px; + } + + &__text-link { + @include H7; + + color: $primary-blue; + cursor: pointer; } .fa-info-circle { color: #6a737d; } + & &__checkbox-wrapper { + display: flex; + flex-direction: row; + align-items: center; + + @media screen and (max-width: 575px) { + width: 160px; + } + } + + & &__checkbox { + height: 13px; + width: 13px; + font-size: 16px; + cursor: pointer; + } + + & &__checkbox-label { + @include H7; + + color: $white; + margin-left: 10px; + margin-top: 1px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + cursor: pointer; + } + & &__ignore-button { border-color: #6a737d; box-sizing: border-box; @@ -102,23 +120,14 @@ &__buttons { display: flex; width: 100%; - margin-top: 10px; - justify-content: flex-start; + padding-top: 10px; + align-items: center; + justify-content: space-between; flex-direction: row-reverse; } -} -.home-notification-tooltip { - &__tooltip-container { + &__tooltip-wrapper { display: flex; - } - - &__content { - @include H7; - - color: $white; - text-align: left; - display: inline-block; - width: 200px; + margin-left: 10px; } } diff --git a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js index e8dcebf13..9d16d1dfe 100644 --- a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js +++ b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js @@ -21,7 +21,7 @@ export default class MultipleNotifications extends PureComponent { const { showAll } = this.state const { children, classNames } = this.props - const childrenToRender = children.filter((child) => child) + const childrenToRender = children.filter(Boolean) if (childrenToRender.length === 0) { return null } diff --git a/ui/app/components/ui/page-container/page-container.component.js b/ui/app/components/ui/page-container/page-container.component.js index eb1280b3b..79126e454 100644 --- a/ui/app/components/ui/page-container/page-container.component.js +++ b/ui/app/components/ui/page-container/page-container.component.js @@ -67,7 +67,7 @@ export default class PageContainer extends PureComponent { renderActiveTabContent() { const { tabsComponent } = this.props let { children } = tabsComponent.props - children = children.filter((child) => child) + children = children.filter(Boolean) const { activeTabIndex } = this.state return children[activeTabIndex] diff --git a/ui/app/components/ui/url-icon/index.scss b/ui/app/components/ui/url-icon/index.scss index 482f327d3..14d5524cb 100644 --- a/ui/app/components/ui/url-icon/index.scss +++ b/ui/app/components/ui/url-icon/index.scss @@ -20,9 +20,9 @@ border-radius: 50%; background: #bbc0c5; flex: 0 1 auto; + display: flex; justify-content: center; align-items: center; - text-align: center; padding-top: 2px; } } diff --git a/ui/app/components/ui/url-icon/url-icon.js b/ui/app/components/ui/url-icon/url-icon.js index 6dcfcc06f..115eb5fe1 100644 --- a/ui/app/components/ui/url-icon/url-icon.js +++ b/ui/app/components/ui/url-icon/url-icon.js @@ -3,13 +3,13 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import IconWithFallback from '../icon-with-fallback' -export default function UrlIcon({ url, className, name }) { +export default function UrlIcon({ url, className, name, fallbackClassName }) { return ( ) } @@ -18,4 +18,5 @@ UrlIcon.propTypes = { url: PropTypes.string, className: PropTypes.string, name: PropTypes.string, + fallbackClassName: PropTypes.string, } diff --git a/ui/app/ducks/alerts/invalid-custom-network.js b/ui/app/ducks/alerts/invalid-custom-network.js index aa7ba3cdb..be2aa20e1 100644 --- a/ui/app/ducks/alerts/invalid-custom-network.js +++ b/ui/app/ducks/alerts/invalid-custom-network.js @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import { ALERT_STATE } from './enums' // Constants diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js index 58ca9a569..563af43d2 100644 --- a/ui/app/ducks/alerts/unconnected-account.js +++ b/ui/app/ducks/alerts/unconnected-account.js @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit' import { captureException } from '@sentry/browser' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import * as actionConstants from '../../store/actionConstants' import { addPermittedAccount, @@ -101,7 +101,7 @@ export const dismissAndDisableAlert = () => { return async (dispatch) => { try { await dispatch(disableAlertRequested()) - await dispatch(setAlertEnabledness(name, false)) + await setAlertEnabledness(name, false) await dispatch(disableAlertSucceeded()) } catch (error) { console.error(error) diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index f414a0892..42fccab06 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -1,5 +1,5 @@ import { combineReducers } from 'redux' -import { ALERT_TYPES } from '../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../shared/constants/alerts' import metamaskReducer from './metamask/metamask' import localeMessagesReducer from './locale/locale' import sendReducer from './send/send.duck' diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 61abf2559..24180f0d3 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -1,5 +1,5 @@ import * as actionConstants from '../../store/actionConstants' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -375,12 +375,12 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale export const getAlertEnabledness = (state) => state.metamask.alertEnabledness -export const getInvalidCustomNetworkAlertEnabledness = (state) => - getAlertEnabledness(state)[ALERT_TYPES.invalidCustomNetwork] - export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount] +export const getWeb3ShimUsageAlertEnabledness = (state) => + getAlertEnabledness(state)[ALERT_TYPES.web3ShimUsage] + export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index f6d36c204..876bd50ec 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -606,7 +606,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { customSwapsGas || (usedQuote?.gasEstimate ? estimatedGasLimitWithMultiplier - : usedQuote?.maxGas) + : `0x${decimalToHex(usedQuote?.maxGas || 0)}`) const usedGasPrice = getUsedSwapsGasPrice(state) usedTradeTxParams.gas = maxGasLimit diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js index e4cf21717..9e0f97681 100644 --- a/ui/app/helpers/utils/conversions.util.js +++ b/ui/app/helpers/utils/conversions.util.js @@ -172,7 +172,7 @@ export function addHexes(aHexWEI, bHexWEI) { } export function sumHexWEIs(hexWEIs) { - return hexWEIs.filter((n) => n).reduce(addHexes) + return hexWEIs.filter(Boolean).reduce(addHexes) } export function sumHexWEIsToUnformattedFiat( diff --git a/ui/app/hooks/useTokensToSearch.js b/ui/app/hooks/useTokensToSearch.js index 8df33e55e..a4e0e94f4 100644 --- a/ui/app/hooks/useTokensToSearch.js +++ b/ui/app/hooks/useTokensToSearch.js @@ -145,9 +145,7 @@ export function useTokensToSearch({ return new BigNumber(rawFiat || 0).gt(secondRawFiat || 0) ? -1 : 1 }, ) - tokensToSearchBuckets.top = tokensToSearchBuckets.top.filter( - (token) => token, - ) + tokensToSearchBuckets.top = tokensToSearchBuckets.top.filter(Boolean) return [ ...tokensToSearchBuckets.owned, ...tokensToSearchBuckets.top, diff --git a/ui/app/pages/add-token/token-list/token-list.component.js b/ui/app/pages/add-token/token-list/token-list.component.js index 83e9c352d..54006136e 100644 --- a/ui/app/pages/add-token/token-list/token-list.component.js +++ b/ui/app/pages/add-token/token-list/token-list.component.js @@ -4,7 +4,7 @@ import classnames from 'classnames' import { checkExistingAddresses } from '../../../helpers/utils/util' import TokenListPlaceholder from './token-list-placeholder' -export default class InfoBox extends Component { +export default class TokenList extends Component { static contextTypes = { t: PropTypes.func, } diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 17ddeb3cf..92c8d685d 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -31,6 +31,8 @@ import { const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension' +const LEGACY_WEB3_URL = + 'https://metamask.zendesk.com/hc/en-us/articles/360053147012' export default class Home extends PureComponent { static contextTypes = { @@ -42,7 +44,7 @@ export default class Home extends PureComponent { forgottenPassword: PropTypes.bool, suggestedTokens: PropTypes.object, unconfirmedTransactionsCount: PropTypes.number, - shouldShowSeedPhraseReminder: PropTypes.bool, + shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, isPopup: PropTypes.bool, isNotification: PropTypes.bool.isRequired, threeBoxSynced: PropTypes.bool, @@ -66,6 +68,10 @@ export default class Home extends PureComponent { swapsFetchParams: PropTypes.object, swapsEnabled: PropTypes.bool, isMainnet: PropTypes.bool, + shouldShowWeb3ShimUsageNotification: PropTypes.bool.isRequired, + setWeb3ShimUsageAlertDismissed: PropTypes.func.isRequired, + originOfCurrentTab: PropTypes.string, + disableWeb3ShimUsageAlert: PropTypes.func.isRequired, } state = { @@ -161,10 +167,39 @@ export default class Home extends PureComponent { setShowRestorePromptToFalse, showRestorePrompt, threeBoxLastUpdated, + shouldShowWeb3ShimUsageNotification, + setWeb3ShimUsageAlertDismissed, + originOfCurrentTab, + disableWeb3ShimUsageAlert, } = this.props return ( + {shouldShowWeb3ShimUsageNotification ? ( + + global.platform.openTab({ url: LEGACY_WEB3_URL }) + } + > + {t('here')} + , + ])} + ignoreText={t('dismiss')} + onIgnore={(disable) => { + setWeb3ShimUsageAlertDismissed(originOfCurrentTab) + if (disable) { + disableWeb3ShimUsageAlert() + } + }} + checkboxText={t('dontShowThisAgain')} + checkboxTooltipText={t('canToggleInSettings')} + key="home-web3ShimUsageNotification" + /> + ) : null} {shouldShowSeedPhraseReminder ? ( { @@ -58,6 +68,14 @@ const mapStateToProps = (state) => { ? firstPermissionsRequest.metadata.id : null + const originOfCurrentTab = getOriginOfCurrentTab(state) + const shouldShowWeb3ShimUsageNotification = + isPopup && + getWeb3ShimUsageAlertEnabledness(state) && + activeTabHasPermissions(state) && + getWeb3ShimUsageStateForOrigin(state, originOfCurrentTab) === + WEB3_SHIM_USAGE_ALERT_STATES.RECORDED + return { forgottenPassword, suggestedTokens, @@ -81,6 +99,8 @@ const mapStateToProps = (state) => { swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', isMainnet: getIsMainnet(state), + originOfCurrentTab, + shouldShowWeb3ShimUsageNotification, } } @@ -103,6 +123,10 @@ const mapDispatchToProps = (dispatch) => ({ onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)), setSwapsWelcomeMessageHasBeenShown: () => dispatch(setSwapsWelcomeMessageHasBeenShown()), + setWeb3ShimUsageAlertDismissed: (origin) => + setWeb3ShimUsageAlertDismissed(origin), + disableWeb3ShimUsageAlert: () => + setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), }) export default compose( diff --git a/ui/app/pages/send/send.scss b/ui/app/pages/send/send.scss index decf358cf..e416b41b2 100644 --- a/ui/app/pages/send/send.scss +++ b/ui/app/pages/send/send.scss @@ -21,6 +21,7 @@ position: absolute; right: 1rem; width: min-content; + font-size: 0.75rem; } } diff --git a/ui/app/pages/settings/alerts-tab/alerts-tab.js b/ui/app/pages/settings/alerts-tab/alerts-tab.js index 6bc6b5327..832a4ec17 100644 --- a/ui/app/pages/settings/alerts-tab/alerts-tab.js +++ b/ui/app/pages/settings/alerts-tab/alerts-tab.js @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' -import { ALERT_TYPES } from '../../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../../shared/constants/alerts' import Tooltip from '../../../components/ui/tooltip' import ToggleButton from '../../../components/ui/toggle-button' import { setAlertEnabledness } from '../../../store/actions' @@ -11,7 +11,6 @@ import { useI18nContext } from '../../../hooks/useI18nContext' const AlertSettingsEntry = ({ alertId, description, title }) => { const t = useI18nContext() - const dispatch = useDispatch() const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]) return ( @@ -27,7 +26,7 @@ const AlertSettingsEntry = ({ alertId, description, title }) => { dispatch(setAlertEnabledness(alertId, !isEnabled))} + onToggle={() => setAlertEnabledness(alertId, !isEnabled)} value={isEnabled} /> @@ -48,6 +47,10 @@ const AlertsTab = () => { title: t('alertSettingsUnconnectedAccount'), description: t('alertSettingsUnconnectedAccountDescription'), }, + [ALERT_TYPES.web3ShimUsage]: { + title: t('alertSettingsWeb3ShimUsage'), + description: t('alertSettingsWeb3ShimUsageDescription'), + }, } return ( diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js index 4c0ab5462..0271abcfc 100644 --- a/ui/app/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -55,7 +55,7 @@ const defaultNetworksData = [ rpcUrl: `https://kovan.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, chainId: KOVAN_CHAIN_ID, ticker: 'ETH', - blockExplorerUrl: 'https://etherscan.io', + blockExplorerUrl: 'https://kovan.etherscan.io', }, ] diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index 13d873b30..4714f79ad 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -418,6 +418,7 @@ export default function BuildQuote({ Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE } hideCancel + showTermsOfService />
) diff --git a/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js index fbb268d65..e70ffd8e7 100644 --- a/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js +++ b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import classnames from 'classnames' import { calcTokenAmount } from '../../../helpers/utils/token-util' -import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util' +import { formatSwapsValueForDisplay } from '../swaps.util' export default function ExchangeRateDisplay({ primaryTokenValue, @@ -13,6 +13,7 @@ export default function ExchangeRateDisplay({ secondaryTokenDecimals = 18, secondaryTokenSymbol, arrowColor = 'black', + boldSymbols = true, className, }) { const [showPrimaryToSecondary, setShowPrimaryToSecondary] = useState(true) @@ -57,16 +58,24 @@ export default function ExchangeRateDisplay({ } else if (new BigNumber(rate, 10).lt('0.000001', 10)) { rateToDisplay = rate } else { - rateToDisplay = toPrecisionWithoutTrailingZeros(rate, 9) + rateToDisplay = formatSwapsValueForDisplay(rate) } return (
1 - {baseSymbol} + + {baseSymbol} + {comparisonSymbol} {rateToDisplay} - {ratiodSymbol} + + {ratiodSymbol} +
+
+
+ {bestQuoteText && ( +

{bestQuoteText}

+ )} +
+

+ {t('swapNQuotes', [numberOfQuotes])} +

+
+ +
+
+
+
@@ -83,26 +113,39 @@ export default function FeeCard({
{!hideTokenApprovalRow && ( -
+
{t('swapThisWillAllowApprove', [tokenApprovalTextComponent])}
-
onTokenApprovalClick()} - > - {t('swapEditLimit')} -
+
onTokenApprovalClick()} + > + {t('swapEditLimit')} +
)} +
+
+
+ {t('swapQuoteIncludesRate', [metaMaskFee])} +
+ +
+
) @@ -122,4 +165,9 @@ FeeCard.propTypes = { tokenApprovalTextComponent: PropTypes.node, tokenApprovalSourceTokenSymbol: PropTypes.string, onTokenApprovalClick: PropTypes.func, + metaMaskFee: PropTypes.string.isRequired, + isBestQuote: PropTypes.bool, + onQuotesClick: PropTypes.func.isRequired, + numberOfQuotes: PropTypes.number.isRequired, + tokenConversionRate: PropTypes.number, } diff --git a/ui/app/pages/swaps/fee-card/fee-card.stories.js b/ui/app/pages/swaps/fee-card/fee-card.stories.js index af57849f3..aaaf43d0f 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.stories.js +++ b/ui/app/pages/swaps/fee-card/fee-card.stories.js @@ -1,6 +1,6 @@ import React from 'react' import { action } from '@storybook/addon-actions' -import { text } from '@storybook/addon-knobs' +import { text, boolean, number, object } from '@storybook/addon-knobs' import FeeCard from './fee-card' const tokenApprovalTextComponent = ( @@ -35,6 +35,13 @@ export const WithAllProps = () => { tokenApprovalSourceTokenSymbol="ABC" onTokenApprovalClick={action('Clicked third row link')} hideTokenApprovalRow={false} + metaMaskFee="0.875" + savings={object('savings 1', { total: '8.55' })} + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={number('numberOfQuotes', 6)} + isBestQuote={boolean('isBestQuote', true)} + conversionRate={300} + currentCurrency="usd" />
) @@ -55,6 +62,11 @@ export const WithoutThirdRow = () => { }} onFeeCardMaxRowClick={action('Clicked max fee row link')} hideTokenApprovalRow + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={number('numberOfQuotes', 1)} + isBestQuote={boolean('isBestQuote', true)} + savings={object('savings 1', { total: '8.55' })} + metaMaskFee="0.875" />
) @@ -70,6 +82,9 @@ export const WithOnlyRequiredProps = () => { }} onFeeCardMaxRowClick={action('Clicked max fee row link')} hideTokenApprovalRow + metaMaskFee="0.875" + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={2} />
) diff --git a/ui/app/pages/swaps/fee-card/index.scss b/ui/app/pages/swaps/fee-card/index.scss index 0a86f6ab7..d1807fc0d 100644 --- a/ui/app/pages/swaps/fee-card/index.scss +++ b/ui/app/pages/swaps/fee-card/index.scss @@ -1,11 +1,69 @@ .fee-card { - border-radius: 8px; - border: 1px solid $Grey-100; width: 100%; @include H7; + &__savings-and-quotes-header { + display: flex; + position: relative; + align-items: center; + } + + &__savings-and-quotes-row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + left: 58px; + height: 39px; + background: $Blue-000; + border: 1px solid $Blue-500; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + border-bottom: 0; + padding-left: 8px; + padding-right: 8px; + } + + &__savings-text { + @include H6; + + font-weight: bold; + color: $Blue-500; + } + + &__quote-link-container { + display: flex; + align-items: center; + cursor: pointer; + } + + &__quote-link-text { + @include H7; + + color: $Blue-500; + } + + &__caret-right { + color: $Blue-500; + width: 6px; + height: 6px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 6px; + + i { + transform: rotate(90deg); + } + } + &__main { + border: 1px solid $Blue-500; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + width: 100%; + max-width: 311px; padding: 16px 16px 12px 16px; } @@ -31,6 +89,10 @@ cursor: pointer; } + &__row-header-text--bold { + color: $Black-100; + } + &__row, &__top-bordered-row { display: flex; @@ -51,7 +113,6 @@ img { height: 10px; width: 10px; - margin-left: 4px; cursor: pointer; } } @@ -60,7 +121,12 @@ height: 10px; width: 10px; justify-content: center; - margin-top: 2px; + + div { + // Needed to override the style property added by the react-tippy library + display: flex !important; + height: 10px; + } } &__info-tooltip-paragraph { @@ -111,11 +177,14 @@ margin-right: 12px; } - &__row-header-primary, - &__row-header-primary--bold { + &__row-header-primary { color: $Grey-500; } + &__row-header-primary--bold { + color: $Black-100; + } + &__row-header-text--bold, &__row-header-secondary--bold, &__row-header-primary--bold { @@ -125,6 +194,11 @@ &__bold { font-weight: bold; } + + &__tilde { + font-family: Roboto, Helvetica, Arial, sans-serif; + margin-right: -3.5px; + } } .info-tooltip { diff --git a/ui/app/pages/swaps/fee-card/pig-icon.js b/ui/app/pages/swaps/fee-card/pig-icon.js new file mode 100644 index 000000000..ef7677ae4 --- /dev/null +++ b/ui/app/pages/swaps/fee-card/pig-icon.js @@ -0,0 +1,54 @@ +import React from 'react' + +export default function PigIcon() { + return ( + + + + + + + + + + ) +} diff --git a/ui/app/pages/swaps/index.scss b/ui/app/pages/swaps/index.scss index 1346eb364..cb08d457b 100644 --- a/ui/app/pages/swaps/index.scss +++ b/ui/app/pages/swaps/index.scss @@ -71,7 +71,7 @@ font-weight: bold; color: $Black-100; - margin-top: 3px; + margin-top: -5px; } &__header { diff --git a/ui/app/pages/swaps/main-quote-summary/index.scss b/ui/app/pages/swaps/main-quote-summary/index.scss index 7993ea724..dfdc882f1 100644 --- a/ui/app/pages/swaps/main-quote-summary/index.scss +++ b/ui/app/pages/swaps/main-quote-summary/index.scss @@ -1,28 +1,75 @@ .main-quote-summary { display: flex; flex-flow: column; + justify-content: center; align-items: center; position: relative; - height: 196px; + max-height: 196px; + min-height: 196px; width: 100%; - color: $white; + color: $Black-100; - &__quote-backdrop-with-top-tab, - &__quote-backdrop { - position: absolute; - box-shadow: 0 10px 39px rgba(3, 125, 214, 0.15); - border-radius: 8px; - background: #fafcff; + &__source-row, + &__destination-row { + width: 100%; + display: flex; + align-items: flex-start; + justify-content: center; + + @include H6; + + color: $Grey-500; + } + + &__source-row { + align-items: center; } - &__quote-backdrop-with-top-tab { - width: 348px; - height: 215px; + &__source-row-value, + &__source-row-symbol { + // Each of these spans can be half their container width minus the space + // needed for the token icon and the span margins + max-width: calc(50% - 13px); } - &__quote-backdrop { - width: 310px; - height: 164px; + + &__source-row-value { + margin-right: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &__source-row-symbol { + margin-left: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &__destination-row { + margin-top: 6px; + } + + &__destination-row-symbol { + margin-left: 5px; + color: $Black-100; + } + + &__icon, + &__icon-fallback { + height: 16px; + width: 16px; + } + + &__icon-fallback { + padding-top: 0; + font-size: 12px; + line-height: 16px; + } + + &__down-arrow { + margin-top: 5px; } &__details { @@ -33,62 +80,24 @@ position: relative; } - &__best-quote { - @include H7; - - font-weight: bold; - position: relative; - display: flex; - padding-top: 6px; - letter-spacing: 0.12px; - min-height: 16px; - - > span { - margin-left: 4px; - } - } - &__quote-details-top { - height: 94px; display: flex; flex-flow: column; justify-content: center; align-items: center; width: 100%; - padding: 12px; - padding-top: 2px; - margin-top: 4px; - } - - &__bold { - font-weight: 900; - } - - &__quote-small-white { - white-space: nowrap; - width: 100%; - text-align: center; - font-size: 14px; - margin-bottom: 8px; - margin-top: 6px; } &__quote-large { display: flex; - align-items: flex-end; + align-items: flex-start; + margin-top: 8px; + height: 50px; } &__quote-large-number { - font-size: 40px; - line-height: 32px; - margin-right: 6px; - } - - &__quote-large-symbol { - display: flex; - align-items: flex-end; - font-size: 32px; - line-height: 32px; + font-size: 60px; + line-height: 48px; } &__quote-large-white { @@ -104,7 +113,10 @@ justify-content: center; align-items: center; width: 287px; - border-top: 1px solid rgba(255, 255, 255, 0.2); - height: 42px; + margin-top: 14px; + } + + &__exchange-rate-display { + color: $Grey-500; } } diff --git a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js index 1ae2e7d51..20aadf6fc 100644 --- a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js +++ b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.js @@ -1,62 +1,33 @@ -import React, { useContext } from 'react' +import React from 'react' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' -import classnames from 'classnames' -import { I18nContext } from '../../../contexts/i18n' import { calcTokenAmount } from '../../../helpers/utils/token-util' import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util' import Tooltip from '../../../components/ui/tooltip' -import SunCheckIcon from '../../../components/ui/icon/sun-check-icon.component' +import UrlIcon from '../../../components/ui/url-icon' import ExchangeRateDisplay from '../exchange-rate-display' import { formatSwapsValueForDisplay } from '../swaps.util' -import QuoteBackdrop from './quote-backdrop' -function getFontSizes(fontSizeScore) { - if (fontSizeScore <= 11) { - return [40, 32] - } - if (fontSizeScore <= 16) { - return [30, 24] - } - return [24, 14] -} - -function getLineHeight(fontSizeScore) { - if (fontSizeScore <= 11) { - return 32 +function getFontSizesAndLineHeights(fontSizeScore) { + if (fontSizeScore <= 9) { + return [60, 48] } - if (fontSizeScore <= 16) { - return 26 + if (fontSizeScore <= 13) { + return [40, 32] } - return 18 -} - -// Returns a numerical value based on the length of the two passed strings: amount and symbol. -// The returned value equals the number of digits in the amount string plus a value calculated -// from the length of the symbol string. The returned number will be passed to the getFontSizes function -// to determine the font size to apply to the amount and symbol strings when rendered. The -// desired maximum digits and letters to show in the ultimately rendered string is 20, and in -// such cases there can also be ellipsis shown and a decimal, combinding for a rendered "string" -// length of ~22. As the symbol will always have a smaller font size than the amount, the -// additive value of the symbol length to the font size score is corrected based on the total -// number of alphanumeric characters in both strings and the desired rendered length of 22. -function getFontSizeScore(amount, symbol) { - const amountLength = amount.match(/\d+/gu).join('').length - const symbolModifier = Math.min((amountLength + symbol.length) / 22, 1) - return amountLength + symbol.length * symbolModifier + return [26, 15] } export default function MainQuoteSummary({ - isBestQuote, sourceValue, sourceSymbol, sourceDecimals, + sourceIconUrl, destinationValue, destinationSymbol, destinationDecimals, + destinationIconUrl, }) { - const t = useContext(I18nContext) - const sourceAmount = toPrecisionWithoutTrailingZeros( calcTokenAmount(sourceValue, sourceDecimals).toString(10), 12, @@ -67,43 +38,55 @@ export default function MainQuoteSummary({ ) const amountToDisplay = formatSwapsValueForDisplay(destinationAmount) - const fontSizeScore = getFontSizeScore(amountToDisplay, destinationSymbol) - const [numberFontSize, symbolFontSize] = getFontSizes(fontSizeScore) - const lineHeight = getLineHeight(fontSizeScore) - + const amountDigitLength = amountToDisplay.match(/\d+/gu).join('').length + const [numberFontSize, lineHeight] = getFontSizesAndLineHeights( + amountDigitLength, + ) let ellipsedAmountToDisplay = amountToDisplay - if (fontSizeScore > 20) { - ellipsedAmountToDisplay = `${amountToDisplay.slice( - 0, - amountToDisplay.length - (fontSizeScore - 20), - )}...` + + if (amountDigitLength > 20) { + ellipsedAmountToDisplay = `${amountToDisplay.slice(0, 20)}...` } return (
-
- -
-
- {isBestQuote && } - {isBestQuote && t('swapsBestQuote')} -
- - {t('swapsConvertToAbout', [ - - {`${sourceAmount} ${sourceSymbol}`} - , - ])} - +
+ + {formatSwapsValueForDisplay(sourceAmount)} + + + + {sourceSymbol} + +
+ +
+ + + {destinationSymbol} + +
- - {`${destinationSymbol}`} -
@@ -141,8 +115,9 @@ export default function MainQuoteSummary({ secondaryTokenValue={destinationValue} secondaryTokenDecimals={destinationDecimals} secondaryTokenSymbol={destinationSymbol} - className="exchange-rate-display--white" - arrowColor="white" + arrowColor="#037DD6" + boldSymbols={false} + className="main-quote-summary__exchange-rate-display" />
@@ -151,7 +126,6 @@ export default function MainQuoteSummary({ } MainQuoteSummary.propTypes = { - isBestQuote: PropTypes.bool, sourceValue: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(BigNumber), @@ -167,4 +141,6 @@ MainQuoteSummary.propTypes = { PropTypes.number, ]), destinationSymbol: PropTypes.string.isRequired, + sourceIconUrl: PropTypes.string, + destinationIconUrl: PropTypes.string, } diff --git a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js index ea267eb17..0f5ca998d 100644 --- a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js +++ b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.stories.js @@ -1,5 +1,5 @@ import React from 'react' -import { text, number, boolean } from '@storybook/addon-knobs' +import { text, number } from '@storybook/addon-knobs' import MainQuoteSummary from './main-quote-summary' export default { @@ -8,28 +8,24 @@ export default { export const BestQuote = () => { return ( - - ) -} - -export const NotBestQuote = () => { - return ( - +
+ +
) } diff --git a/ui/app/pages/swaps/swaps-footer/swaps-footer.js b/ui/app/pages/swaps/swaps-footer/swaps-footer.js index 870ce028b..62a3e2a70 100644 --- a/ui/app/pages/swaps/swaps-footer/swaps-footer.js +++ b/ui/app/pages/swaps/swaps-footer/swaps-footer.js @@ -31,7 +31,10 @@ export default function SwapsFooter({ onSubmit={onSubmit} submitText={submitText} submitButtonType="confirm" - footerClassName="swaps-footer__custom-page-container-footer-class" + footerClassName={classnames( + 'swaps-footer__custom-page-container-footer-class', + className, + )} footerButtonClassName={classnames( 'swaps-footer__custom-page-container-footer-button-class', { diff --git a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js index d123eb3d6..deea5c4b0 100644 --- a/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js +++ b/ui/app/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js @@ -159,7 +159,7 @@ export default connect( )(SwapsGasCustomizationModalComponent) function sumHexWEIsToRenderableEth(hexWEIs) { - const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes) + const hexWEIsSum = hexWEIs.filter(Boolean).reduce(addHexes) return formatETHFee( getValueFromWeiHex({ value: hexWEIsSum, diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index e64cbf8fd..81876d947 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -533,6 +533,7 @@ export function quotesToRenderableData( destinationTokenDecimals: destinationTokenInfo.decimals, destinationTokenSymbol: destinationTokenInfo.symbol, destinationTokenValue: formatSwapsValueForDisplay(destinationValue), + destinationIconUrl: destinationTokenInfo.iconUrl, isBestQuote: quote.isBestQuote, liquiditySourceKey, feeInEth, @@ -544,6 +545,7 @@ export function quotesToRenderableData( sourceTokenDecimals: sourceTokenInfo.decimals, sourceTokenSymbol: sourceTokenInfo.symbol, sourceTokenValue: sourceValue, + sourceTokenIconUrl: sourceTokenInfo.iconUrl, ethValueOfTrade, minimumAmountReceived, metaMaskFee: fee, diff --git a/ui/app/pages/swaps/view-quote/index.scss b/ui/app/pages/swaps/view-quote/index.scss index 474eced60..397748c2a 100644 --- a/ui/app/pages/swaps/view-quote/index.scss +++ b/ui/app/pages/swaps/view-quote/index.scss @@ -17,7 +17,7 @@ @media screen and (max-width: 576px) { overflow-y: auto; - max-height: 388px; + max-height: 428px; } } @@ -44,13 +44,13 @@ display: flex; align-items: center; justify-content: center; + min-height: 46px; } &__view-other-button, &__view-other-button-fade { display: flex; align-items: center; - margin-bottom: 16px; position: absolute; @include H7; @@ -133,6 +133,9 @@ width: 100%; align-items: center; justify-content: center; + width: intrinsic; /* Safari/WebKit uses a non-standard name */ + width: max-content; + max-width: 340px; margin-top: 8px; @media screen and (min-width: 576px) { @@ -149,57 +152,29 @@ } &__countdown-timer-container { - @media screen and (max-width: 576px) { - margin-top: 12px; - margin-bottom: 16px; - - &--thin { - margin-top: 8px; - margin-bottom: 8px; - - > div { - margin-top: 0; - margin-bottom: 0; - } - } - } - - @media screen and (min-width: 576px) { - &--thin { - margin-top: 6px; - } - } + width: 152px; + min-height: 32px; + display: flex; + justify-content: center; + border-radius: 42px; + background: #f2f3f4; + margin-top: 8px; } &__fee-card-container { + display: flex; + align-items: center; width: 100%; - margin-top: 8px; + max-width: 311px; margin-bottom: 8px; @media screen and (min-width: 576px) { margin-bottom: 0; - - &--three-rows { - margin-bottom: -16px; - } - } - } - - &__main-quote-summary-container { - margin-top: 24px; - - @media screen and (max-width: 576px) { - margin-top: 0; - } - - &--thin { - margin-top: 8px; } } &__metamask-rate { display: flex; - margin-top: 8%; } &__metamask-rate-text { @@ -214,5 +189,9 @@ &__thin-swaps-footer { max-height: 82px; + + @media screen and (min-width: 576px) { + height: 72px; + } } } diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index ea004a9f0..bdceee91f 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -74,7 +74,6 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker' import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps' import CountdownTimer from '../countdown-timer' import SwapsFooter from '../swaps-footer' -import InfoTooltip from '../../../components/ui/info-tooltip' import ViewQuotePriceDifference from './view-quote-price-difference' export default function ViewQuote() { @@ -119,6 +118,7 @@ export default function ViewQuote() { const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime) const { isBestQuote } = usedQuote + const fetchParamsSourceToken = fetchParams?.sourceToken const usedGasLimit = @@ -191,9 +191,11 @@ export default function ViewQuote() { destinationTokenDecimals, destinationTokenSymbol, destinationTokenValue, + destinationIconUrl, sourceTokenDecimals, sourceTokenSymbol, sourceTokenValue, + sourceTokenIconUrl, } = renderableDataForUsedQuote const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote( @@ -508,11 +510,7 @@ export default function ViewQuote() { /> )}
-
+
-
- -
-
-
- {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
{ - allAvailableQuotesOpened() - setSelectQuotePopoverShown(true) - }} - > - {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
-
-

- {t('swapQuoteIncludesRate', [metaMaskFee])} -

- -
+
{ + allAvailableQuotesOpened() + setSelectQuotePopoverShown(true) + }} + tokenConversionRate={ + destinationTokenSymbol === 'ETH' + ? 1 + : memoizedTokenConversionRates[destinationToken.address] + } />
@@ -604,7 +582,6 @@ export default function ViewQuote() { onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} disabled={balanceError || gasPrice === null || gasPrice === undefined} className={isShowingWarning && 'view-quote__thin-swaps-footer'} - showTermsOfService showTopBorder /> diff --git a/ui/app/selectors/permissions.js b/ui/app/selectors/permissions.js index 776db2234..a463d09d0 100644 --- a/ui/app/selectors/permissions.js +++ b/ui/app/selectors/permissions.js @@ -244,6 +244,13 @@ export function getPermissionsForActiveTab(state) { }) } +export function activeTabHasPermissions(state) { + const { activeTab, metamask } = state + const { domains = {} } = metamask + + return Boolean(domains[activeTab.origin]?.permissions?.length > 0) +} + export function getLastConnectedInfo(state) { const { permissionsHistory = {} } = state.metamask return Object.keys(permissionsHistory).reduce((acc, origin) => { diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index d9fd7129a..5607476a8 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -356,3 +356,7 @@ export function getIpfsGateway(state) { export function getUSDConversionRate(state) { return state.metamask.usdConversionRate } + +export function getWeb3ShimUsageStateForOrigin(state, origin) { + return state.metamask.web3ShimUsageOrigins[origin] +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 1cbf2bcdc..93c13f6ba 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2510,16 +2510,18 @@ export function setSwapsWelcomeMessageHasBeenShown() { } } -export function setAlertEnabledness(alertId, enabledness) { - return async () => { - await promisifiedBackground.setAlertEnabledness(alertId, enabledness) - } +export async function setAlertEnabledness(alertId, enabledness) { + await promisifiedBackground.setAlertEnabledness(alertId, enabledness) } export async function setUnconnectedAccountAlertShown(origin) { await promisifiedBackground.setUnconnectedAccountAlertShown(origin) } +export async function setWeb3ShimUsageAlertDismissed(origin) { + await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin) +} + export function loadingMethodDataStarted() { return { type: actionConstants.LOADING_METHOD_DATA_STARTED, diff --git a/ui/index.js b/ui/index.js index 192165280..6c4541f17 100644 --- a/ui/index.js +++ b/ui/index.js @@ -4,7 +4,7 @@ import { clone } from 'lodash' import React from 'react' import { render } from 'react-dom' import { getEnvironmentType } from '../app/scripts/lib/util' -import { ALERT_TYPES } from '../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../shared/constants/alerts' import { SENTRY_STATE } from '../app/scripts/lib/setupSentry' import { ENVIRONMENT_TYPE_POPUP } from '../app/scripts/lib/enums' import Root from './app/pages' diff --git a/yarn.lock b/yarn.lock index 56802930e..4a308380e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2014,7 +2014,7 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@metamask/contract-metadata@^1.19.0": +"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.20.0.tgz#8a127ca08edeb2c19b2f725b439b1630926c8aa9" integrity sha512-Htb2yUwmAe4GrABpqahNDpYR3ErcCcaavF6tKamqvyymICeywi6HV0y/A7rbe86gLy5UW6wJG9yP4pCpQxs4RQ== @@ -2114,20 +2114,19 @@ resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd" integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw== -"@metamask/inpage-provider@^6.1.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-6.3.0.tgz#92d965e20912c24adbf973efbd07dbf339547741" - integrity sha512-n7E06+8hWdYKmgJo84WFvgX6/BSqaOQEOMIrcbrP48LdkkZNEAChx6D8oUb2lYDQiWgahR+f20jsJoN4WmOjxw== +"@metamask/inpage-provider@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-8.0.1.tgz#67b1f0733ae7c0e0e429dc5c067ba9d2dd6d66da" + integrity sha512-dN3IpiJtaHeiPzF01UXnrQ6TxXbXbkU54kiOHuIUe9e8s7vyPzgDgN2nj84xjmIkqxL0MKY90Wcp0obFKnNj+Q== dependencies: - eth-rpc-errors "^2.1.1" + "@metamask/safe-event-emitter" "^2.0.0" + eth-rpc-errors "^4.0.2" fast-deep-equal "^2.0.1" is-stream "^2.0.0" - json-rpc-engine "^5.2.0" + json-rpc-engine "^6.1.0" json-rpc-middleware-stream "^2.1.1" obj-multiplex "^1.0.0" - obs-store "^4.0.3" pump "^3.0.0" - safe-event-emitter "^1.0.1" "@metamask/jazzicon@^2.0.0": version "2.0.0" @@ -2145,6 +2144,15 @@ gl-mat4 "1.1.4" gl-vec3 "1.0.3" +"@metamask/obs-store@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-5.0.0.tgz#cf2213874c62186a8a4c80f0c402af62a0a877a9" + integrity sha512-9Az33hULCDVUVpBzNAdjPPgNTfs6paC38LYZv4xazt03mJt4jwWFFTP79TtF4grXfiAtUs/UZOktW4Vp+r37rw== + dependencies: + "@metamask/safe-event-emitter" "^2.0.0" + readable-stream "^2.2.2" + through2 "^2.0.3" + "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" @@ -3579,10 +3587,10 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adm-zip@0.4.11: - version "0.4.11" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" - integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== +adm-zip@0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" + integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== aes-js@3.0.0: version "3.0.0" @@ -5700,12 +5708,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.4.6: - version "3.4.6" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" - integrity sha1-AdqNgh2HgT0ViWfnQ9X+bGLPjA8= - -bluebird@^3.3.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6730,7 +6733,7 @@ chokidar@^3.3.0, chokidar@^3.4.1: optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1: +chownr@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== @@ -7491,17 +7494,6 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -coveralls@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.0.tgz#22ef730330538080d29b8c151dc9146afde88a99" - integrity sha512-ZppXR9y5PraUOrf/DzHJY6gzNUhXYE3b9D43xEXs4QYZ7/Oe0Gy0CS+IPKWFfvQFXB3RG9QduaQUFehzSpGAFw== - dependencies: - js-yaml "^3.6.1" - lcov-parse "^0.0.10" - log-driver "^1.2.5" - minimist "^1.2.0" - request "^2.79.0" - cp-file@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" @@ -9888,13 +9880,6 @@ eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2: json-rpc-random-id "^1.0.0" xtend "^4.0.1" -eth-rpc-errors@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-2.1.1.tgz#00a7d6c8a9c864a8ab7d0356be20964e5bee4b13" - integrity sha512-MY3zAa5ZF8hvgQu1HOF9agaK5GgigBRGpTJ8H0oVlE0NqMu13CW6syyjLXdeIDCGQTbUeHliU1z9dVmvMKx1Tg== - dependencies: - fast-safe-stringify "^2.0.6" - eth-rpc-errors@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10" @@ -11783,16 +11768,16 @@ gc-stats@^1.2.1: nan "^2.13.2" node-pre-gyp "^0.13.0" -geckodriver@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.19.1.tgz#556f95fd6451b553cec89f81f81abbefce10d6e5" - integrity sha512-xWL/+eEhQ6+t98rc1c+xVM3hshDJibXtZf9WJA3sshxq4k5L1PBwfmswyBmmlKUfBr4xuC256gLVC2RxFhiCsQ== +geckodriver@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.21.0.tgz#1f04780ebfb451ffd08fa8fddc25cc26e37ac4a2" + integrity sha512-NamdJwGIWpPiafKQIvGman95BBi/SBqHddRXAnIEpFNFCFToTW0sEA0nUckMKCBNn1DVIcLfULfyFq/sTn9bkA== dependencies: - adm-zip "0.4.11" - bluebird "3.4.6" + adm-zip "0.4.16" + bluebird "3.7.2" got "5.6.0" - https-proxy-agent "3.0.0" - tar "4.4.2" + https-proxy-agent "5.0.0" + tar "6.0.2" generate-function@^2.0.0: version "2.3.1" @@ -13155,15 +13140,7 @@ https-did-resolver@^0.1.0: did-resolver "0.0.6" xmlhttprequest "^1.8.0" -https-proxy-agent@3.0.0, https-proxy-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" - integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -13171,6 +13148,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" + integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + human-standard-collectible-abi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-standard-collectible-abi/-/human-standard-collectible-abi-1.0.2.tgz#077bae9ed1b0b0b82bc46932104b4b499c941aa0" @@ -13455,9 +13440,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== inline-source-map@~0.6.0: version "0.6.2" @@ -15123,7 +15108,7 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@3.13.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.5.1, js-yaml@^3.6.1, js-yaml@^3.9.0: +js-yaml@3.13.1, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.5.1, js-yaml@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -15220,7 +15205,7 @@ json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0: promise-to-callback "^1.0.0" safe-event-emitter "^1.0.1" -json-rpc-engine@^5.2.0, json-rpc-engine@^5.3.0: +json-rpc-engine@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.3.0.tgz#7dc7291766b28766ebda33eb6d3f4c6301c44ff4" integrity sha512-+diJ9s8rxB+fbJhT7ZEf8r8spaLRignLd8jTgQ/h5JSGppAHGtNMZtCoabipCaleR1B3GTGxbXBOqhaJSGmPGQ== @@ -15468,10 +15453,10 @@ jsx-ast-utils@^2.4.1: array-includes "^3.1.1" object.assign "^4.1.0" -jszip@^3.1.5: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== +jszip@^3.2.2: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" + integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -15809,11 +15794,6 @@ lcid@^3.0.0: dependencies: invert-kv "^3.0.0" -lcov-parse@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" - integrity sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM= - lead@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" @@ -16771,11 +16751,6 @@ lodash@4.17.20, lodash@=3.10.1, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lo resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -log-driver@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" - integrity sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY= - log-symbols@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" @@ -17527,7 +17502,7 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" -minipass@^2.2.1, minipass@^2.2.4, minipass@^2.6.4: +minipass@^2.2.1, minipass@^2.6.4: version "2.6.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.6.5.tgz#1c245f9f2897f70fd4a219066261ce6c29f80b18" integrity sha512-ewSKOPFH9blOLXx0YSE+mbrNMBFPS+11a2b03QZ+P4LVrUHW/GAlqeYC7DBknDyMWkHzrzTpDhUvy7MUxqyrPA== @@ -17542,7 +17517,7 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -minizlib@^1.1.0, minizlib@^1.2.1: +minizlib@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.2.tgz#6f0ccc82fa53e1bf2ff145f220d2da9fa6e3a166" integrity sha512-hR3At21uSrsjjDTWrbu0IMLTpnkpv8IIMFDFaoz43Tmu4LkmAXfH44vNNzpTnf+OAQQCHrb91y/wc2J4x5XgSQ== @@ -22229,7 +22204,7 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -22449,7 +22424,7 @@ sass-loader@^7.0.1: neo-async "^2.5.0" pify "^3.0.0" -sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: +sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -22660,15 +22635,14 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -selenium-webdriver@^4.0.0-alpha.5: - version "4.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.5.tgz#e4683b3dbf827d70df09a7e43bf02ebad20fa7c1" - integrity sha512-hktl3DSrhzM59yLhWzDGHIX9o56DvA+cVK7Dw6FcJR6qQ4CGzkaHeXQPcdrslkWMTeq0Ci9AmCxq0EMOvm2Rkg== +selenium-webdriver@4.0.0-alpha.7: + version "4.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" + integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw== dependencies: - jszip "^3.1.5" - rimraf "^2.6.3" + jszip "^3.2.2" + rimraf "^2.7.1" tmp "0.0.30" - xml2js "^0.4.19" semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: version "1.1.0" @@ -24300,18 +24274,17 @@ tar-stream@^2.0.0, tar-stream@^2.0.1: inherits "^2.0.3" readable-stream "^3.1.1" -tar@4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.2.tgz#60685211ba46b38847b1ae7ee1a24d744a2cd462" - integrity sha512-BfkE9CciGGgDsATqkikUHrQrraBCO+ke/1f6SFAEMnxyyfN9lxC+nW1NFWMpqH865DhHIy9vQi682gk1X7friw== +tar@6.0.2, tar@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" + integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== dependencies: - chownr "^1.0.1" - fs-minipass "^1.2.5" - minipass "^2.2.4" - minizlib "^1.1.0" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.0" + mkdirp "^1.0.3" + yallist "^4.0.0" tar@^2.0.0: version "2.2.2" @@ -24335,18 +24308,6 @@ tar@^4, tar@^4.0.2: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" - integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.0" - mkdirp "^1.0.3" - yallist "^4.0.0" - tarn@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/tarn/-/tarn-1.1.5.tgz#7be88622e951738b9fa3fb77477309242cdddc2d" @@ -26522,19 +26483,6 @@ xml-name-validator@^2.0.1: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" integrity sha1-TYuPHszTQZqjYgYb7O9RXh5VljU= -xml2js@^0.4.19: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"