mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge remote-tracking branch 'origin/develop' into sync-master
* origin/develop: (66 commits) Disable swaps based on chainId, instead of network id (#10155) improve handling of last selected provider (#10093) Add links to Community Forum to README (#10152) @metamask/contract-metadata@1.21.0 (#10142) Prevent malformed next nonce warning (#10143) add module resolution for node-analytics/axios (#10139) Ensure that gas for swap tx submitted at same time as approval is in hex (#10135) Fix useTransactionDisplayData unit tests (#10134) Fix network settings Kovan block explorer link (#10117) Use destructured signal (#10115) throw a new wrapped error instead of default one from segment (#10118) @metamask/contract-metadata@1.20.0 (#10116) Use late-bound noop function when disabling console (#10110) Bump @metamask/contract-metadata from 1.19.0 to 1.20.0 (#10104) Remove unnecessary swaps footer space when in dropdown mode (#10100) Tighten up loading indication logic (#10103) Skip reporting of successive persistence failures (#10099) Update `@metamask/controllers` to v5.1.0 (#10096) @metamask/obs-store@5.0.0 (#10092) set last provider when switching to a customRPC (#10084) ...
This commit is contained in:
commit
672a387779
@ -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
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
|
||||
mkdir -p build-artifacts/yarn-install-har
|
||||
mv ./*.har build-artifacts/yarn-install-har/
|
@ -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
|
||||
|
@ -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'
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -41,7 +41,6 @@ ui/app/css/output/
|
||||
|
||||
notes.txt
|
||||
|
||||
.coveralls.yml
|
||||
.nyc_output
|
||||
|
||||
.metamaskrc
|
||||
|
@ -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.11 Thu Jan 07 2021
|
||||
- [#10155](https://github.com/MetaMask/metamask-extension/pull/10155): Disable swaps when the current network's chainId does not match the mainnet chain ID, instead of disabling based on network ID
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
You can find the latest version of MetaMask on [our official website](https://metamask.io/). For help using MetaMask, visit our [User Support Site](https://metamask.zendesk.com/hc/en-us).
|
||||
|
||||
For [general questions](https://metamask.zendesk.com/hc/en-us/community/topics/360000682532-General), [feature requests](https://metamask.zendesk.com/hc/en-us/community/topics/360000682552-Feature-Requests-Ideas), or [developer questions](https://metamask.zendesk.com/hc/en-us/community/topics/360001751291-Developer-Questions), visit our [Community Forum](https://metamask.zendesk.com/hc/en-us/community/topics).
|
||||
|
||||
MetaMask supports Firefox, Google Chrome, and Chromium-based browsers. We recommend using the latest available browser version.
|
||||
|
||||
For up to the minute news, follow our [Twitter](https://twitter.com/metamask_io) or [Medium](https://medium.com/metamask) pages.
|
||||
|
@ -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"
|
||||
},
|
||||
|
3
app/images/down-arrow-grey.svg
Normal file
3
app/images/down-arrow-grey.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00163 15.2045C6.21347 15.1962 6.45833 15.109 6.61275 14.9637L11.6498 10.2232C11.9385 9.90194 12.0577 9.28597 11.7239 8.92693C11.3952 8.57328 10.7576 8.5838 10.4276 8.93611L6.89052 12.2693L6.89052 1.87164C6.89052 1.38076 6.49254 0.982788 6.00163 0.982788C5.51071 0.982788 5.11274 1.38076 5.11274 1.87164L5.11274 12.2693L1.57565 8.93611C1.27181 8.63281 0.611843 8.57675 0.279352 8.92693C-0.0531095 9.27702 0.0531145 9.91513 0.353459 10.2232L5.3905 14.9637C5.56288 15.126 5.76513 15.205 6.00163 15.2045Z" fill="#D6D9DC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 636 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#fff"><path d="m8.00002 9.57037c.93765 0 1.69776-.76011 1.69776-1.69777 0-.93765-.76011-1.69776-1.69776-1.69776-.93766 0-1.69777.76011-1.69777 1.69776 0 .93766.76011 1.69777 1.69777 1.69777z"/><path d="m11.0582 11.6586c-.1862 0-.3725-.071-.5145-.2131-.2842-.2841-.2842-.7448 0-1.029.6795-.67946 1.0538-1.58294 1.0538-2.54391 0-.96098-.3743-1.86446-1.0538-2.54394-.2842-.28417-.2842-.74484 0-1.02901.2841-.2841.7449-.2841 1.029 0 .9543.95434 1.48 2.22329 1.48 3.57295 0 1.34965-.5257 2.61861-1.48 3.57291-.1421.1421-.3283.2131-.5145.2131z"/><path d="m4.94175 11.6586c-.18622 0-.37246-.071-.51451-.2131-.95434-.9543-1.47997-2.22326-1.47997-3.57291 0-1.34966.52563-2.61861 1.47997-3.57295.28411-.2841.74491-.2841 1.02902 0 .28417.28417.28417.74484 0 1.02901-.67954.67948-1.05376 1.58296-1.05376 2.54394 0 .96097.37422 1.86445 1.05376 2.54391.28417.2842.28417.7449 0 1.029-.14206.1421-.32828.2131-.51451.2131z"/><path d="m13.1451 13.7453c-.1862 0-.3724-.0711-.5145-.2131-.2842-.2842-.2842-.7449 0-1.0291 2.5533-2.55325 2.5533-6.70772 0-9.26101-.2842-.28417-.2842-.74484 0-1.02901.2841-.28411.7449-.28411 1.029 0 3.1207 3.12066 3.1207 8.19842 0 11.31912-.142.142-.3283.2131-.5145.2131z"/><path d="m2.855 13.7453c-.18622 0-.37245-.0711-.5145-.2131-3.120666-3.1207-3.120666-8.19846 0-11.31912.28411-.28411.74491-.28411 1.02901 0 .28417.28417.28417.74484 0 1.02901-2.553289 2.55329-2.553289 6.70776 0 9.26101.28417.2842.28417.7449 0 1.0291-.14206.142-.32828.2131-.51451.2131z"/></g></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -3,5 +3,5 @@
|
||||
"matches": ["https://metamask.io/*"],
|
||||
"ids": ["*"]
|
||||
},
|
||||
"minimum_chrome_version": "58"
|
||||
"minimum_chrome_version": "63"
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 }),
|
||||
)
|
||||
}
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ObservableStore from 'obs-store'
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
|
||||
/**
|
||||
* @typedef {Object} CachedBalancesOptions
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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'
|
||||
@ -52,9 +51,13 @@ export default class NetworkController extends EventEmitter {
|
||||
this.providerStore = new ObservableStore(
|
||||
opts.provider || { ...defaultProviderConfig },
|
||||
)
|
||||
this.previousProviderStore = new ObservableStore(
|
||||
this.providerStore.getState(),
|
||||
)
|
||||
this.networkStore = new ObservableStore('loading')
|
||||
this.store = new ComposedStore({
|
||||
provider: this.providerStore,
|
||||
previousProviderStore: this.previousProviderStore,
|
||||
network: this.networkStore,
|
||||
})
|
||||
|
||||
@ -189,6 +192,13 @@ export default class NetworkController extends EventEmitter {
|
||||
* Sets the provider config and switches the network.
|
||||
*/
|
||||
setProviderConfig(config) {
|
||||
this.previousProviderStore.updateState(this.getProviderConfig())
|
||||
this.providerStore.updateState(config)
|
||||
this._switchNetwork(config)
|
||||
}
|
||||
|
||||
rollbackToPreviousProvider() {
|
||||
const config = this.previousProviderStore.getState()
|
||||
this.providerStore.updateState(config)
|
||||
this._switchNetwork(config)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ObservableStore from 'obs-store'
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
import log from 'loglevel'
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<string>} accounts - The accounts.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<array>} 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.<array, object, string, string>} 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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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.`)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ObservableStore from 'obs-store'
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
|
||||
/**
|
||||
* An ObservableStore that can composes a flat
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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<ProviderStateHandlerResult>} 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()
|
||||
}
|
@ -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
|
||||
|
@ -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<unknown>} req - The JSON-RPC request object.
|
||||
* @param {import('json-rpc-engine').JsonRpcResponse<true>} 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()
|
||||
}
|
@ -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<true>} 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()
|
||||
}
|
@ -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 <type>".
|
||||
*
|
||||
* @param {any} key - The key to stringify
|
||||
*/
|
||||
function stringifyKey(key) {
|
||||
return typeof key === 'string' ? key : `typeof ${typeof key}`
|
||||
}
|
@ -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'
|
||||
|
@ -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"
|
||||
}
|
@ -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.
|
||||
* 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.
|
||||
*/
|
||||
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)
|
||||
async getProviderState(origin) {
|
||||
return {
|
||||
isUnlocked: this.isUnlocked(),
|
||||
...this.getProviderNetworkState(),
|
||||
accounts: await this.permissionsController.getAccounts(origin),
|
||||
}
|
||||
}
|
||||
|
||||
function updatePublicConfigStore(memState) {
|
||||
const chainId = networkController.getCurrentChainId()
|
||||
if (memState.network !== 'loading') {
|
||||
publicConfigStore.putState(selectPublicState(chainId, memState))
|
||||
}
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
|
||||
function selectPublicState(chainId, { isUnlocked, network }) {
|
||||
return {
|
||||
isUnlocked,
|
||||
chainId,
|
||||
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 {
|
||||
@ -570,6 +579,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
networkController.setProviderType,
|
||||
networkController,
|
||||
),
|
||||
rollbackToPreviousProvider: nodeify(
|
||||
networkController.rollbackToPreviousProvider,
|
||||
networkController,
|
||||
),
|
||||
setCustomRpc: nodeify(this.setCustomRpc, this),
|
||||
updateAndSetCustomRpc: nodeify(this.updateAndSetCustomRpc, this),
|
||||
delCustomRpc: nodeify(this.delCustomRpc, this),
|
||||
@ -704,8 +717,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 +1830,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 +1987,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 +2016,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 +2066,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 +2139,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
|
||||
|
||||
/**
|
||||
|
@ -6,7 +6,7 @@ module.exports = function (api) {
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
browsers: ['chrome >= 58', 'firefox >= 56.2'],
|
||||
browsers: ['chrome >= 63', 'firefox >= 56.2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -79,6 +79,7 @@ function defineAllTasks() {
|
||||
clean,
|
||||
styleTasks.prod,
|
||||
composeParallel(scriptTasks.test, staticTasks.prod, manifestTasks.test),
|
||||
zip,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -55,6 +55,9 @@ async function start() {
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`
|
||||
const coverageLink = `<a href="${coverageUrl}">Report</a>`
|
||||
|
||||
// links to bundle browser builds
|
||||
const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/deps-viz/background/index.html`
|
||||
const depVizLink = `<a href="${depVizUrl}">background</a>`
|
||||
@ -65,6 +68,7 @@ async function start() {
|
||||
const contentRows = [
|
||||
`builds: ${buildLinks}`,
|
||||
`bundle viz: ${bundleLinks}`,
|
||||
`code coverage: ${coverageLink}`,
|
||||
`dep viz: ${depVizLink}`,
|
||||
`<a href="${allArtifactsUrl}">all artifacts</a>`,
|
||||
]
|
||||
|
12
package.json
12
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",
|
||||
@ -77,14 +76,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.21.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",
|
||||
@ -142,7 +142,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",
|
||||
@ -216,7 +215,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",
|
||||
@ -236,7 +234,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",
|
||||
@ -278,7 +276,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",
|
||||
|
18
shared/constants/alerts.js
Normal file
18
shared/constants/alerts.js
Normal file
@ -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,
|
||||
}
|
@ -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')
|
||||
}
|
||||
|
@ -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<void>}
|
||||
*/
|
||||
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<string>} 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 () {
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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<any>} [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,
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -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 = {}
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 () {
|
||||
|
@ -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'
|
||||
|
||||
|
@ -40,9 +40,6 @@ function mapDispatchToProps(dispatch) {
|
||||
setProviderType: (type) => {
|
||||
dispatch(actions.setProviderType(type))
|
||||
},
|
||||
setPreviousProvider: (type) => {
|
||||
dispatch(actions.setPreviousProvider(type))
|
||||
},
|
||||
setRpcTarget: (target, chainId, ticker, nickname) => {
|
||||
dispatch(actions.setRpcTarget(target, chainId, ticker, nickname))
|
||||
},
|
||||
@ -85,7 +82,6 @@ class NetworkDropdown extends Component {
|
||||
setRpcTarget: PropTypes.func.isRequired,
|
||||
hideNetworkDropdown: PropTypes.func.isRequired,
|
||||
setNetworksTabAddMode: PropTypes.func.isRequired,
|
||||
setPreviousProvider: PropTypes.func.isRequired,
|
||||
setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
|
||||
frequentRpcListDetail: PropTypes.array.isRequired,
|
||||
networkDropdownOpen: PropTypes.bool.isRequired,
|
||||
@ -116,10 +112,6 @@ class NetworkDropdown extends Component {
|
||||
}
|
||||
|
||||
renderCustomRpcList(rpcListDetail, provider) {
|
||||
const {
|
||||
provider: { type: providerType },
|
||||
setPreviousProvider,
|
||||
} = this.props
|
||||
const reversedRpcListDetail = rpcListDetail.slice().reverse()
|
||||
|
||||
return reversedRpcListDetail.map((entry) => {
|
||||
@ -133,7 +125,6 @@ class NetworkDropdown extends Component {
|
||||
closeMenu={() => this.props.hideNetworkDropdown()}
|
||||
onClick={() => {
|
||||
if (isPrefixedFormattedHexString(chainId)) {
|
||||
setPreviousProvider(providerType)
|
||||
this.props.setRpcTarget(rpcUrl, chainId, ticker, nickname)
|
||||
} else {
|
||||
this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl)
|
||||
|
@ -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,
|
||||
|
@ -35,6 +35,7 @@
|
||||
|
||||
color: #4eade7;
|
||||
position: absolute;
|
||||
font-size: 0.75rem;
|
||||
top: 4px;
|
||||
right: 16px;
|
||||
cursor: pointer;
|
||||
|
@ -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 && (
|
||||
<Checkbox
|
||||
id="homeNotification_checkbox"
|
||||
checked={checkboxState}
|
||||
className="home-notification__checkbox"
|
||||
onClick={() => 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 (
|
||||
<div className={classnames('home-notification', ...classNames)}>
|
||||
<div className="home-notification__header">
|
||||
<div className="home-notification__header-container">
|
||||
<img
|
||||
className="home-notification__icon"
|
||||
alt=""
|
||||
src="images/icons/connect.svg"
|
||||
/>
|
||||
<div className="home-notification__text">{descriptionText}</div>
|
||||
</div>
|
||||
{infoText ? (
|
||||
<ReactTippy
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
html={
|
||||
<p className="home-notification-tooltip__content">{infoText}</p>
|
||||
}
|
||||
offset={-36}
|
||||
distance={36}
|
||||
animation="none"
|
||||
position="top"
|
||||
arrow
|
||||
theme="tippy-tooltip-home"
|
||||
>
|
||||
<img alt="" src="images/icons/info.svg" />
|
||||
</ReactTippy>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="home-notification__buttons">
|
||||
{onAccept && acceptText ? (
|
||||
<Button
|
||||
type="primary"
|
||||
className="home-notification__accept-button"
|
||||
onClick={this.handleAccept}
|
||||
>
|
||||
{acceptText}
|
||||
</Button>
|
||||
) : null}
|
||||
{onIgnore && ignoreText ? (
|
||||
<Button
|
||||
type="secondary"
|
||||
className="home-notification__ignore-button"
|
||||
onClick={this.handleIgnore}
|
||||
>
|
||||
{ignoreText}
|
||||
</Button>
|
||||
) : null}
|
||||
return (
|
||||
<div className={classnames('home-notification', ...classNames)}>
|
||||
<div className="home-notification__content">
|
||||
<div className="home-notification__content-container">
|
||||
<div className="home-notification__text">{descriptionText}</div>
|
||||
</div>
|
||||
{infoText ? (
|
||||
<Tooltip
|
||||
position="top"
|
||||
title={infoText}
|
||||
wrapperClassName="home-notification__tooltip-wrapper"
|
||||
>
|
||||
<i className="fa fa-info-circle" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="home-notification__buttons">
|
||||
{onAccept && acceptText ? (
|
||||
<Button
|
||||
type="primary"
|
||||
className="home-notification__accept-button"
|
||||
onClick={onAccept}
|
||||
>
|
||||
{acceptText}
|
||||
</Button>
|
||||
) : null}
|
||||
{onIgnore && ignoreText ? (
|
||||
<Button
|
||||
type="secondary"
|
||||
className="home-notification__ignore-button"
|
||||
// Some onIgnore handlers use the checkboxState to determine whether
|
||||
// to disable the notification
|
||||
onClick={() => onIgnore(checkboxState)}
|
||||
>
|
||||
{ignoreText}
|
||||
</Button>
|
||||
) : null}
|
||||
{checkboxText ? (
|
||||
<div className="home-notification__checkbox-wrapper">
|
||||
{checkboxTooltipText ? (
|
||||
<Tooltip
|
||||
position="top"
|
||||
title={checkboxTooltipText}
|
||||
wrapperClassName="home-notification__checkbox-label-tooltip"
|
||||
>
|
||||
{checkboxElement}
|
||||
</Tooltip>
|
||||
) : (
|
||||
checkboxElement
|
||||
)}
|
||||
<label
|
||||
className="home-notification__checkbox-label"
|
||||
htmlFor="homeNotification_checkbox"
|
||||
>
|
||||
{checkboxText}
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -19,11 +19,8 @@ export default class LoadingNetworkScreen extends PureComponent {
|
||||
providerId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
showNetworkDropdown: PropTypes.func,
|
||||
setProviderArgs: PropTypes.array,
|
||||
lastSelectedProvider: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
]),
|
||||
setProviderType: PropTypes.func,
|
||||
rollbackToPreviousProvider: PropTypes.func,
|
||||
isLoadingNetwork: PropTypes.bool,
|
||||
}
|
||||
|
||||
@ -123,14 +120,14 @@ export default class LoadingNetworkScreen extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { lastSelectedProvider, setProviderType } = this.props
|
||||
const { rollbackToPreviousProvider } = this.props
|
||||
|
||||
return (
|
||||
<LoadingScreen
|
||||
header={
|
||||
<div
|
||||
className="page-container__header-close"
|
||||
onClick={() => setProviderType(lastSelectedProvider || 'ropsten')}
|
||||
onClick={rollbackToPreviousProvider}
|
||||
/>
|
||||
}
|
||||
showLoadingSpinner={!this.state.showErrorScreen}
|
||||
|
@ -4,7 +4,7 @@ import { getNetworkIdentifier } from '../../../selectors'
|
||||
import LoadingNetworkScreen from './loading-network-screen.component'
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loadingMessage, lastSelectedProvider } = state.appState
|
||||
const { loadingMessage } = state.appState
|
||||
const { provider, network } = state.metamask
|
||||
const { rpcUrl, chainId, ticker, nickname, type } = provider
|
||||
|
||||
@ -14,7 +14,6 @@ const mapStateToProps = (state) => {
|
||||
return {
|
||||
isLoadingNetwork: network === 'loading',
|
||||
loadingMessage,
|
||||
lastSelectedProvider,
|
||||
setProviderArgs,
|
||||
provider,
|
||||
providerId: getNetworkIdentifier(state),
|
||||
@ -26,6 +25,8 @@ const mapDispatchToProps = (dispatch) => {
|
||||
setProviderType: (type) => {
|
||||
dispatch(actions.setProviderType(type))
|
||||
},
|
||||
rollbackToPreviousProvider: () =>
|
||||
dispatch(actions.rollbackToPreviousProvider()),
|
||||
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<IconWithFallback
|
||||
className={classnames('url-icon', className)}
|
||||
icon={url}
|
||||
name={name}
|
||||
fallbackClassName="url-icon__fallback"
|
||||
fallbackClassName={classnames('url-icon__fallback', fallbackClassName)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -18,4 +18,5 @@ UrlIcon.propTypes = {
|
||||
url: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
fallbackClassName: PropTypes.string,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -42,7 +42,6 @@ export default function reduceApp(state = {}, action) {
|
||||
trezor: `m/44'/60'/0'/0`,
|
||||
ledger: `m/44'/60'/0'/0/0`,
|
||||
},
|
||||
lastSelectedProvider: null,
|
||||
networksTabSelectedRpcUrl: '',
|
||||
networksTabIsInAddMode: false,
|
||||
loadingMethodData: false,
|
||||
@ -305,15 +304,6 @@ export default function reduceApp(state = {}, action) {
|
||||
gasIsLoading: false,
|
||||
}
|
||||
|
||||
case actionConstants.SET_PREVIOUS_PROVIDER:
|
||||
if (action.value === 'loading') {
|
||||
return appState
|
||||
}
|
||||
return {
|
||||
...appState,
|
||||
lastSelectedProvider: action.value,
|
||||
}
|
||||
|
||||
case actionConstants.SET_SELECTED_SETTINGS_RPC_URL:
|
||||
return {
|
||||
...appState,
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
nextNonce !== prevNextNonce ||
|
||||
customNonceValue !== prevCustomNonceValue
|
||||
) {
|
||||
if (customNonceValue > nextNonce) {
|
||||
if (nextNonce !== null && customNonceValue > nextNonce) {
|
||||
this.setState({
|
||||
submitWarning: this.context.t('nextNonceWarning', [nextNonce]),
|
||||
})
|
||||
|
@ -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 (
|
||||
<MultipleNotifications>
|
||||
{shouldShowWeb3ShimUsageNotification ? (
|
||||
<HomeNotification
|
||||
descriptionText={t('web3ShimUsageNotification', [
|
||||
<span
|
||||
key="web3ShimUsageNotificationLink"
|
||||
className="home-notification__text-link"
|
||||
onClick={() =>
|
||||
global.platform.openTab({ url: LEGACY_WEB3_URL })
|
||||
}
|
||||
>
|
||||
{t('here')}
|
||||
</span>,
|
||||
])}
|
||||
ignoreText={t('dismiss')}
|
||||
onIgnore={(disable) => {
|
||||
setWeb3ShimUsageAlertDismissed(originOfCurrentTab)
|
||||
if (disable) {
|
||||
disableWeb3ShimUsageAlert()
|
||||
}
|
||||
}}
|
||||
checkboxText={t('dontShowThisAgain')}
|
||||
checkboxTooltipText={t('canToggleInSettings')}
|
||||
key="home-web3ShimUsageNotification"
|
||||
/>
|
||||
) : null}
|
||||
{shouldShowSeedPhraseReminder ? (
|
||||
<HomeNotification
|
||||
descriptionText={t('backupApprovalNotice')}
|
||||
|
@ -2,11 +2,14 @@ import { compose } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
unconfirmedTransactionsCountSelector,
|
||||
activeTabHasPermissions,
|
||||
getCurrentEthBalance,
|
||||
getFirstPermissionRequest,
|
||||
getTotalUnapprovedCount,
|
||||
getIsMainnet,
|
||||
getOriginOfCurrentTab,
|
||||
getTotalUnapprovedCount,
|
||||
getWeb3ShimUsageStateForOrigin,
|
||||
unconfirmedTransactionsCountSelector,
|
||||
} from '../../selectors'
|
||||
|
||||
import {
|
||||
@ -17,8 +20,11 @@ import {
|
||||
setConnectedStatusPopoverHasBeenShown,
|
||||
setDefaultHomeActiveTabName,
|
||||
setSwapsWelcomeMessageHasBeenShown,
|
||||
setWeb3ShimUsageAlertDismissed,
|
||||
setAlertEnabledness,
|
||||
} from '../../store/actions'
|
||||
import { setThreeBoxLastUpdated } from '../../ducks/app/app'
|
||||
import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'
|
||||
import {
|
||||
getSwapsWelcomeMessageSeenStatus,
|
||||
getSwapsFeatureLiveness,
|
||||
@ -28,6 +34,10 @@ import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
} from '../../../../app/scripts/lib/enums'
|
||||
import {
|
||||
ALERT_TYPES,
|
||||
WEB3_SHIM_USAGE_ALERT_STATES,
|
||||
} from '../../../../shared/constants/alerts'
|
||||
import Home from './home.component'
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@ -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(
|
||||
|
@ -21,6 +21,7 @@
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
width: min-content;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }) => {
|
||||
<ToggleButton
|
||||
offLabel={t('off')}
|
||||
onLabel={t('on')}
|
||||
onToggle={() => 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 (
|
||||
|
@ -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',
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -418,6 +418,7 @@ export default function BuildQuote({
|
||||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE
|
||||
}
|
||||
hideCancel
|
||||
showTermsOfService
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -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 (
|
||||
<div className={classnames('exchange-rate-display', className)}>
|
||||
<span>1</span>
|
||||
<span className="exchange-rate-display__bold">{baseSymbol}</span>
|
||||
<span
|
||||
className={classnames({ 'exchange-rate-display__bold': boldSymbols })}
|
||||
>
|
||||
{baseSymbol}
|
||||
</span>
|
||||
<span>{comparisonSymbol}</span>
|
||||
<span>{rateToDisplay}</span>
|
||||
<span className="exchange-rate-display__bold">{ratiodSymbol}</span>
|
||||
<span
|
||||
className={classnames({ 'exchange-rate-display__bold': boldSymbols })}
|
||||
>
|
||||
{ratiodSymbol}
|
||||
</span>
|
||||
<div
|
||||
className={classnames('exchange-rate-display__switch-arrows', {
|
||||
'exchange-rate-display__switch-arrows-rotate': rotating,
|
||||
@ -115,4 +124,5 @@ ExchangeRateDisplay.propTypes = {
|
||||
secondaryTokenSymbol: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
arrowColor: PropTypes.string,
|
||||
boldSymbols: PropTypes.bool,
|
||||
}
|
||||
|
@ -11,11 +11,41 @@ export default function FeeCard({
|
||||
tokenApprovalTextComponent,
|
||||
tokenApprovalSourceTokenSymbol,
|
||||
onTokenApprovalClick,
|
||||
metaMaskFee,
|
||||
isBestQuote,
|
||||
numberOfQuotes,
|
||||
onQuotesClick,
|
||||
tokenConversionRate,
|
||||
}) {
|
||||
const t = useContext(I18nContext)
|
||||
|
||||
let bestQuoteText = ''
|
||||
if (isBestQuote && tokenConversionRate) {
|
||||
bestQuoteText = t('swapUsingBestQuote')
|
||||
} else if (tokenConversionRate) {
|
||||
bestQuoteText = t('swapBetterQuoteAvailable')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fee-card">
|
||||
<div className="fee-card__savings-and-quotes-header">
|
||||
<div className="fee-card__savings-and-quotes-row">
|
||||
{bestQuoteText && (
|
||||
<p className="fee-card__savings-text">{bestQuoteText}</p>
|
||||
)}
|
||||
<div
|
||||
className="fee-card__quote-link-container"
|
||||
onClick={onQuotesClick}
|
||||
>
|
||||
<p className="fee-card__quote-link-text">
|
||||
{t('swapNQuotes', [numberOfQuotes])}
|
||||
</p>
|
||||
<div className="fee-card__caret-right">
|
||||
<i className="fa fa-angle-up" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fee-card__main">
|
||||
<div className="fee-card__row-header">
|
||||
<div>
|
||||
@ -83,26 +113,39 @@ export default function FeeCard({
|
||||
</div>
|
||||
</div>
|
||||
{!hideTokenApprovalRow && (
|
||||
<div className="fee-card__top-bordered-row">
|
||||
<div className="fee-card__row-header">
|
||||
<div className="fee-card__row-label">
|
||||
<div className="fee-card__row-header-text">
|
||||
{t('swapThisWillAllowApprove', [tokenApprovalTextComponent])}
|
||||
</div>
|
||||
<div
|
||||
className="fee-card__link"
|
||||
onClick={() => onTokenApprovalClick()}
|
||||
>
|
||||
{t('swapEditLimit')}
|
||||
</div>
|
||||
<InfoTooltip
|
||||
position="top"
|
||||
contentText={t('swapEnableDescription', [
|
||||
tokenApprovalSourceTokenSymbol,
|
||||
])}
|
||||
containerClassName="fee-card__info-tooltip-container"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="fee-card__link"
|
||||
onClick={() => onTokenApprovalClick()}
|
||||
>
|
||||
{t('swapEditLimit')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="fee-card__top-bordered-row">
|
||||
<div className="fee-card__row-label">
|
||||
<div className="fee-card__row-header-text">
|
||||
{t('swapQuoteIncludesRate', [metaMaskFee])}
|
||||
</div>
|
||||
<InfoTooltip
|
||||
position="top"
|
||||
contentText={t('swapMetaMaskFeeDescription', [metaMaskFee])}
|
||||
wrapperClassName="fee-card__info-tooltip-container"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -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,
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user