Merge branch 'develop' into v4.11.0
This commit is contained in:
commit
0c97fc1d9e
2
.babelrc
2
.babelrc
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"presets": [["env", { "debug": true }], "react", "stage-0"],
|
||||
"presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"],
|
||||
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ workflows:
|
|||
full_test:
|
||||
jobs:
|
||||
- prep-deps-npm
|
||||
- prep-deps-firefox
|
||||
- prep-build:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
|
@ -28,7 +27,6 @@ workflows:
|
|||
- test-e2e-firefox:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-deps-firefox
|
||||
- prep-build
|
||||
- test-e2e-beta-chrome:
|
||||
requires:
|
||||
|
@ -37,7 +35,6 @@ workflows:
|
|||
- test-e2e-beta-firefox:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-deps-firefox
|
||||
- prep-build
|
||||
- test-unit:
|
||||
requires:
|
||||
|
@ -49,7 +46,6 @@ workflows:
|
|||
- test-integration-mascara-firefox:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-deps-firefox
|
||||
- prep-scss
|
||||
- test-integration-flat-chrome:
|
||||
requires:
|
||||
|
@ -58,7 +54,6 @@ workflows:
|
|||
- test-integration-flat-firefox:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-deps-firefox
|
||||
- prep-scss
|
||||
- all-tests-pass:
|
||||
requires:
|
||||
|
@ -103,46 +98,34 @@ jobs:
|
|||
- restore_cache:
|
||||
keys:
|
||||
- v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1.0-dependency-cache-
|
||||
- run:
|
||||
name: Install npm 6 + deps via npm
|
||||
command: |
|
||||
sudo npm install -g npm@6.1.0 && npm install --no-save
|
||||
sudo npm install -g npm@6 && npm install --no-save
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- node_modules
|
||||
- save_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
prep-deps-firefox:
|
||||
docker:
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
- run:
|
||||
name: Download Firefox If needed
|
||||
command: ./.circleci/scripts/firefox-download.sh
|
||||
- save_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
paths:
|
||||
- firefox
|
||||
|
||||
prep-build:
|
||||
docker:
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: build:dist
|
||||
command: npm run dist
|
||||
- run:
|
||||
name: build:debug
|
||||
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
|
||||
- save_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- dist
|
||||
- builds
|
||||
|
@ -152,23 +135,23 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: build:dist
|
||||
command: npm run doc
|
||||
- save_cache:
|
||||
key: docs-cache-{{ .Revision }}
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- docs/jsdoc
|
||||
- docs/jsdocs
|
||||
|
||||
prep-scss:
|
||||
docker:
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
|
@ -176,8 +159,8 @@ jobs:
|
|||
- run:
|
||||
name: Build for integration tests
|
||||
command: npm run test:integration:build
|
||||
- save_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- ui/app/css/output
|
||||
|
||||
|
@ -186,8 +169,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Test
|
||||
command: npm run lint
|
||||
|
@ -197,8 +180,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Test
|
||||
command: npx nsp check
|
||||
|
@ -208,10 +191,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:e2e:chrome
|
||||
command: npm run test:e2e:chrome
|
||||
|
@ -224,15 +205,11 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
- run:
|
||||
name: Install firefox
|
||||
command: ./.circleci/scripts/firefox-install.sh
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
name: Install Firefox
|
||||
command: ./.circleci/scripts/firefox-install
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:e2e:firefox
|
||||
command: npm run test:e2e:firefox
|
||||
|
@ -245,10 +222,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:e2e:chrome:beta
|
||||
command: npm run test:e2e:chrome:beta
|
||||
|
@ -261,15 +236,11 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
- run:
|
||||
name: Install firefox
|
||||
command: ./.circleci/scripts/firefox-install.sh
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
name: Install Firefox
|
||||
command: ./.circleci/scripts/firefox-install
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:e2e:firefox:beta
|
||||
command: npm run test:e2e:firefox:beta
|
||||
|
@ -282,15 +253,13 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Test
|
||||
command: npm run test:screens
|
||||
- save_cache:
|
||||
key: job-screens-{{ .Revision }}
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- test-artifacts
|
||||
|
||||
|
@ -299,12 +268,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- restore_cache:
|
||||
key: job-screens-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- store_artifacts:
|
||||
path: dist/mascara
|
||||
destination: builds/mascara
|
||||
|
@ -326,14 +291,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: build-cache-{{ .Revision }}
|
||||
- restore_cache:
|
||||
key: docs-cache-{{ .Revision }}
|
||||
- restore_cache:
|
||||
key: job-screens-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: sentry sourcemaps upload
|
||||
command: npm run sentry:publish
|
||||
|
@ -349,32 +308,22 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:coverage
|
||||
command: npm run test:coverage
|
||||
|
||||
test-integration-flat-firefox:
|
||||
environment:
|
||||
browsers: '["Firefox"]'
|
||||
docker:
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Install firefox
|
||||
command: ./.circleci/scripts/firefox-install.sh
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
name: Install Firefox
|
||||
command: ./.circleci/scripts/firefox-install
|
||||
- run:
|
||||
name: test:integration:flat
|
||||
command: npm run test:flat
|
||||
|
@ -386,38 +335,22 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:integration:flat
|
||||
command: npm run test:flat
|
||||
|
||||
test-integration-mascara-firefox:
|
||||
environment:
|
||||
browsers: '["Firefox"]'
|
||||
docker:
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-firefox-
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Install firefox
|
||||
command: ./.circleci/scripts/firefox-install.sh
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
name: Install Firefox
|
||||
command: ./.circleci/scripts/firefox-install
|
||||
- run:
|
||||
name: test:integration:mascara
|
||||
command: npm run test:mascara
|
||||
|
@ -429,14 +362,8 @@ jobs:
|
|||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:integration:mascara
|
||||
command: npm run test:mascara
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
echo "Checking if firefox was already downloaded"
|
||||
if [ -d "firefox" ]
|
||||
then
|
||||
echo "Firefox found. No need to download"
|
||||
else
|
||||
FIREFOX_VERSION="61.0.1"
|
||||
FIREFOX_BINARY="firefox-$FIREFOX_VERSION.tar.bz2"
|
||||
echo "Downloading firefox..."
|
||||
wget "https://ftp.mozilla.org/pub/firefox/releases/$FIREFOX_VERSION/linux-x86_64/en-US/$FIREFOX_BINARY" \
|
||||
&& tar xjf "$FIREFOX_BINARY"
|
||||
echo "firefox download complete"
|
||||
fi
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
FIREFOX_VERSION='62.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'
|
||||
|
||||
printf '%s\n' "Removing old Firefox installation"
|
||||
|
||||
sudo rm -r "${FIREFOX_PATH}"
|
||||
|
||||
printf '%s\n' "Downloading & installing Firefox ${FIREFOX_VERSION}"
|
||||
|
||||
wget --quiet --show-progress -O- "${FIREFOX_BINARY_URL}" | sudo tar xj -C /opt
|
||||
|
||||
printf '%s\n' "Firefox ${FIREFOX_VERSION} installed"
|
||||
|
||||
{
|
||||
printf '%s\n' 'pref("general.config.filename", "firefox.cfg");'
|
||||
printf '%s\n' 'pref("general.config.obscure_value", 0);'
|
||||
} | sudo tee "${FIREFOX_PATH}/defaults/pref/autoconfig.js"
|
||||
|
||||
sudo cp .circleci/scripts/firefox.cfg "${FIREFOX_PATH}"
|
||||
|
||||
printf '%s\n' "Firefox ${FIREFOX_VERSION} configured"
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
echo "Installing firefox..."
|
||||
sudo rm -r /opt/firefox
|
||||
sudo mv firefox /opt/firefox61
|
||||
sudo mv /usr/bin/firefox /usr/bin/firefox-old
|
||||
sudo ln -s /opt/firefox61/firefox /usr/bin/firefox
|
||||
echo "Firefox installed."
|
|
@ -0,0 +1,13 @@
|
|||
// IMPORTANT: Start your code on the 2nd line
|
||||
|
||||
lockPref("app.update.enabled", false);
|
||||
lockPref("app.update.auto", false);
|
||||
lockPref("app.update.mode", 0);
|
||||
lockPref("app.update.service.enabled", false);
|
||||
|
||||
pref("browser.rights.3.shown", true);
|
||||
|
||||
pref("browser.startup.homepage_override.mstone","ignore");
|
||||
|
||||
lockPref("plugins.hide_infobar_for_outdated_plugin", true);
|
||||
clearPref("plugins.update.url");
|
|
@ -9,6 +9,7 @@ package
|
|||
# IDEs
|
||||
.idea
|
||||
.vscode
|
||||
.sublime-project
|
||||
|
||||
# VIM
|
||||
*.swp
|
||||
|
@ -20,6 +21,7 @@ temp
|
|||
.DS_Store
|
||||
app/.DS_Store
|
||||
|
||||
coverage/
|
||||
dist
|
||||
builds/
|
||||
disc/
|
||||
|
@ -34,6 +36,7 @@ test/bundle.js
|
|||
test/test-bundle.js
|
||||
|
||||
test-artifacts
|
||||
test-builds
|
||||
|
||||
#ignore css output and sourcemaps
|
||||
ui/app/css/output/
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"generator-mocha": {}
|
||||
}
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -2,11 +2,25 @@
|
|||
|
||||
## Current Develop Branch
|
||||
|
||||
## 4.10.0 Mon Sep 17 2018
|
||||
|
||||
- [#4803](https://github.com/MetaMask/metamask-extension/pull/4803): Implement EIP-712: Sign typed data, but continue to support v1.
|
||||
- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances.
|
||||
- [#4279](https://github.com/MetaMask/metamask-extension/pull/4279): New BlockTracker and Json-Rpc-Engine based Provider.
|
||||
- [#5050](https://github.com/MetaMask/metamask-extension/pull/5050): Add Ledger hardware wallet support.
|
||||
- [#4919](https://github.com/MetaMask/metamask-extension/pull/4919): Refactor and Redesign Transaction List.
|
||||
- [#5182](https://github.com/MetaMask/metamask-extension/pull/5182): Add Transaction Details to the Transaction List view.
|
||||
- [#5229](https://github.com/MetaMask/metamask-extension/pull/5229): Clear old seed words when importing new seed words.
|
||||
- [#5264](https://github.com/MetaMask/metamask-extension/pull/5264): Improve click area for adjustment arrows buttons.
|
||||
- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method.
|
||||
- [#5189](https://github.com/MetaMask/metamask-extension/pull/5189): Fix bug where Ropsten loading message is shown when connecting to Kovan.
|
||||
- [#5256](https://github.com/MetaMask/metamask-extension/pull/5256): Add mock EIP-1102 support
|
||||
|
||||
## 4.9.3 Wed Aug 15 2018
|
||||
|
||||
- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses.
|
||||
- (#4961)[https://github.com/MetaMask/metamask-extension/pull/4961]: Add a download seed phrase link.
|
||||
- (#5060)[https://github.com/MetaMask/metamask-extension/pull/5060]: Fix bug where gas was not updating properly.
|
||||
- [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses.
|
||||
- [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link.
|
||||
- [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly.
|
||||
|
||||
## 4.9.2 Mon Aug 09 2018
|
||||
|
||||
|
@ -33,6 +47,8 @@
|
|||
- [#4691](https://github.com/MetaMask/metamask-extension/pull/4691): Redesign of the Confirm Transaction Screen.
|
||||
- [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed.
|
||||
- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): Allow the use of HTTP prefix for custom rpc urls.
|
||||
- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case.
|
||||
- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances.
|
||||
|
||||
## 4.8.0 Thur Jun 14 2018
|
||||
|
||||
|
|
|
@ -37,6 +37,12 @@ If you're a web dapp developer, we've got two types of guides for you:
|
|||
|
||||
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read [our internal docs here](https://metamask.github.io/metamask-extension/).
|
||||
|
||||
You can re-generate the docs locally by running `npm run doc`, and contributors can update the hosted docs by running `npm run publish-docs`.
|
||||
|
||||
### Running Tests
|
||||
|
||||
Requires `mocha` installed. Run `npm install -g mocha`.
|
||||
|
|
|
@ -796,7 +796,7 @@
|
|||
"message": "Testovací faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "Komu: "
|
||||
"message": "Komu"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 na ETH přes ShapeShift",
|
||||
|
|
|
@ -775,7 +775,7 @@
|
|||
"message": "Testfaucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "An:"
|
||||
"message": "An"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 an ETH via ShapeShift",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
"accountSelectionRequired": {
|
||||
"message": "You need to select an account!"
|
||||
},
|
||||
"activityLog": {
|
||||
"message": "activity log"
|
||||
},
|
||||
"address": {
|
||||
"message": "Address"
|
||||
},
|
||||
|
@ -29,6 +32,9 @@
|
|||
"addTokens": {
|
||||
"message": "Add Tokens"
|
||||
},
|
||||
"addSuggestedTokens": {
|
||||
"message": "Add Suggested Tokens"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
|
@ -119,8 +125,8 @@
|
|||
"close": {
|
||||
"message": "Close"
|
||||
},
|
||||
"chromeRequiredForTrezor":{
|
||||
"message": "You need to use MetaMask on Google Chrome in order to connect to your TREZOR device."
|
||||
"chromeRequiredForHardwareWallets":{
|
||||
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Confirm"
|
||||
|
@ -146,15 +152,12 @@
|
|||
"connecting": {
|
||||
"message": "Connecting..."
|
||||
},
|
||||
"connectToLedger": {
|
||||
"message": "Connect to Ledger"
|
||||
},
|
||||
"connectToTrezor": {
|
||||
"message": "Connect to Trezor"
|
||||
},
|
||||
"connectToTrezorHelp": {
|
||||
"message": "MetaMask is able to access your TREZOR Ethereum accounts. First make sure your device is connected and unlocked."
|
||||
},
|
||||
"connectToTrezorTrouble": {
|
||||
"message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware."
|
||||
},
|
||||
"continue": {
|
||||
"message": "Continue"
|
||||
},
|
||||
|
@ -213,6 +216,9 @@
|
|||
"currentConversion": {
|
||||
"message": "Current Conversion"
|
||||
},
|
||||
"currentLanguage": {
|
||||
"message": "Current Language"
|
||||
},
|
||||
"currentNetwork": {
|
||||
"message": "Current Network"
|
||||
},
|
||||
|
@ -289,8 +295,8 @@
|
|||
"downloadStateLogs": {
|
||||
"message": "Download State Logs"
|
||||
},
|
||||
"dontHaveATrezorWallet": {
|
||||
"message": "Don't have a TREZOR hardware wallet?"
|
||||
"dontHaveAHardwareWallet": {
|
||||
"message": "Don’t have a hardware wallet?"
|
||||
},
|
||||
"dropped": {
|
||||
"message": "Dropped"
|
||||
|
@ -426,11 +432,11 @@
|
|||
"hardwareWalletConnected": {
|
||||
"message": "Hardware wallet connected"
|
||||
},
|
||||
"hardwareSupport": {
|
||||
"message": "Hardware Support"
|
||||
"hardwareWallets": {
|
||||
"message": "Connect a hardware wallet"
|
||||
},
|
||||
"hardwareSupportMsg": {
|
||||
"message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works."
|
||||
"hardwareWalletsMsg": {
|
||||
"message": "Select a hardware wallet you'd like to use with MetaMask"
|
||||
},
|
||||
"havingTroubleConnecting": {
|
||||
"message": "Having trouble connecting?"
|
||||
|
@ -454,6 +460,9 @@
|
|||
"hideTokenPrompt": {
|
||||
"message": "Hide Token?"
|
||||
},
|
||||
"history": {
|
||||
"message": "History"
|
||||
},
|
||||
"howToDeposit": {
|
||||
"message": "How would you like to deposit Ether?"
|
||||
},
|
||||
|
@ -538,6 +547,9 @@
|
|||
"learnMore": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"ledgerAccountRestriction": {
|
||||
"message": "You need to make use your last account before you can add a new one."
|
||||
},
|
||||
"lessThanMax": {
|
||||
"message": "must be less than or equal to $1.",
|
||||
"description": "helper for inputting hex as decimal input"
|
||||
|
@ -587,6 +599,9 @@
|
|||
"metamaskSeedWords": {
|
||||
"message": "MetaMask Seed Words"
|
||||
},
|
||||
"metamaskVersion": {
|
||||
"message": "MetaMask Version"
|
||||
},
|
||||
"min": {
|
||||
"message": "Minimum"
|
||||
},
|
||||
|
@ -651,7 +666,7 @@
|
|||
"message": "No transaction history."
|
||||
},
|
||||
"noTransactions": {
|
||||
"message": "No Transactions"
|
||||
"message": "You have no transactions"
|
||||
},
|
||||
"notFound": {
|
||||
"message": "Not Found"
|
||||
|
@ -702,6 +717,9 @@
|
|||
"pasteSeed": {
|
||||
"message": "Paste your seed phrase here!"
|
||||
},
|
||||
"pending": {
|
||||
"message": "pending"
|
||||
},
|
||||
"personalAddressDetected": {
|
||||
"message": "Personal address detected. Input the token contract address."
|
||||
},
|
||||
|
@ -730,6 +748,9 @@
|
|||
"qrCode": {
|
||||
"message": "Show QR Code"
|
||||
},
|
||||
"queue": {
|
||||
"message": "Queue"
|
||||
},
|
||||
"readdToken": {
|
||||
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
|
||||
},
|
||||
|
@ -845,6 +866,9 @@
|
|||
"save": {
|
||||
"message": "Save"
|
||||
},
|
||||
"speedUp": {
|
||||
"message": "speed up"
|
||||
},
|
||||
"speedUpTitle": {
|
||||
"message": "Speed Up Transaction"
|
||||
},
|
||||
|
@ -870,6 +894,12 @@
|
|||
"secretPhrase": {
|
||||
"message": "Enter your secret twelve word phrase here to restore your vault."
|
||||
},
|
||||
"showHexData": {
|
||||
"message": "Show Hex Data"
|
||||
},
|
||||
"showHexDataDescription": {
|
||||
"message": "Select this to show the hex data field on the send screen"
|
||||
},
|
||||
"newPassword8Chars": {
|
||||
"message": "New Password (min 8 chars)"
|
||||
},
|
||||
|
@ -882,6 +912,9 @@
|
|||
"selectCurrency": {
|
||||
"message": "Select Currency"
|
||||
},
|
||||
"selectLocale": {
|
||||
"message": "Select Locale"
|
||||
},
|
||||
"selectService": {
|
||||
"message": "Select Service"
|
||||
},
|
||||
|
@ -897,6 +930,12 @@
|
|||
"sendTokens": {
|
||||
"message": "Send Tokens"
|
||||
},
|
||||
"sentEther": {
|
||||
"message": "sent ether"
|
||||
},
|
||||
"sentTokens": {
|
||||
"message": "sent tokens"
|
||||
},
|
||||
"separateEachWord": {
|
||||
"message": "Separate each word with a single space"
|
||||
},
|
||||
|
@ -908,7 +947,10 @@
|
|||
"description": "displays token symbol"
|
||||
},
|
||||
"orderOneHere": {
|
||||
"message": "Order one here."
|
||||
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
|
||||
},
|
||||
"outgoing": {
|
||||
"message": "Outgoing"
|
||||
},
|
||||
"searchTokens": {
|
||||
"message": "Search Tokens"
|
||||
|
@ -920,7 +962,13 @@
|
|||
"message": "Select an Account"
|
||||
},
|
||||
"selectAnAccountHelp": {
|
||||
"message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask."
|
||||
"message": "Select the account to view in MetaMask"
|
||||
},
|
||||
"selectHdPath": {
|
||||
"message": "Select HD Path"
|
||||
},
|
||||
"selectPathHelp": {
|
||||
"message": "If you don't see your existing Ledger accounts below, try switching paths to \"Legacy (MEW / MyCrypto)\""
|
||||
},
|
||||
"sendTokensAnywhere": {
|
||||
"message": "Send Tokens to anyone with an Ethereum account"
|
||||
|
@ -967,6 +1015,9 @@
|
|||
"sign": {
|
||||
"message": "Sign"
|
||||
},
|
||||
"signatureRequest": {
|
||||
"message": "Signature Request"
|
||||
},
|
||||
"signed": {
|
||||
"message": "Signed"
|
||||
},
|
||||
|
@ -1019,7 +1070,7 @@
|
|||
"message": "Test Faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "To: "
|
||||
"message": "To"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 to ETH via ShapeShift",
|
||||
|
@ -1049,6 +1100,27 @@
|
|||
"total": {
|
||||
"message": "Total"
|
||||
},
|
||||
"transaction": {
|
||||
"message": "transaction"
|
||||
},
|
||||
"transactionConfirmed": {
|
||||
"message": "Transaction confirmed on $2."
|
||||
},
|
||||
"transactionCreated": {
|
||||
"message": "Transaction created with a value of $1 on $2."
|
||||
},
|
||||
"transactionDropped": {
|
||||
"message": "Transaction dropped on $2."
|
||||
},
|
||||
"transactionSubmitted": {
|
||||
"message": "Transaction submitted on $2."
|
||||
},
|
||||
"transactionUpdated": {
|
||||
"message": "Transaction updated on $2."
|
||||
},
|
||||
"transactionUpdatedGas": {
|
||||
"message": "Transaction updated with a gas price of $1 on $2."
|
||||
},
|
||||
"transactions": {
|
||||
"message": "transactions"
|
||||
},
|
||||
|
@ -1095,6 +1167,9 @@
|
|||
"unavailable": {
|
||||
"message": "Unavailable"
|
||||
},
|
||||
"units": {
|
||||
"message": "units"
|
||||
},
|
||||
"unknown": {
|
||||
"message": "Unknown"
|
||||
},
|
||||
|
@ -1122,6 +1197,9 @@
|
|||
"unlockMessage": {
|
||||
"message": "The decentralized web awaits"
|
||||
},
|
||||
"updatedWithDate": {
|
||||
"message": "Updated $1"
|
||||
},
|
||||
"uriErrorMsg": {
|
||||
"message": "URIs require the appropriate HTTP/HTTPS prefix."
|
||||
},
|
||||
|
|
|
@ -772,7 +772,7 @@
|
|||
"message": "Probar Faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "Para:"
|
||||
"message": "Para"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 a ETH via ShapeShift",
|
||||
|
|
|
@ -490,7 +490,7 @@
|
|||
"message": "Sélectionner un service"
|
||||
},
|
||||
"send": {
|
||||
"message": "Envoyé"
|
||||
"message": "Envoyer"
|
||||
},
|
||||
"sendTokens": {
|
||||
"message": "Envoyer des jetons"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -784,7 +784,7 @@
|
|||
"message": "Тестовый кран"
|
||||
},
|
||||
"to": {
|
||||
"message": "Получатель: "
|
||||
"message": "Получатель"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 в ETH через ShapeShift",
|
||||
|
|
|
@ -796,7 +796,7 @@
|
|||
"message": "சோதனை குழாய்"
|
||||
},
|
||||
"to": {
|
||||
"message": "பெறுநர்: "
|
||||
"message": "பெறுநர்"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$ 1 முதல் ETH வரை வடிவம்",
|
||||
|
|
|
@ -796,7 +796,7 @@
|
|||
"message": "Test Musluğu"
|
||||
},
|
||||
"to": {
|
||||
"message": "Kime: "
|
||||
"message": "Kime"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "ShapeShift üstünden $1'dan ETH'e",
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
"addTokens": {
|
||||
"message": "添加代币"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "在Metamask上添加已用的代币"
|
||||
},
|
||||
"amount": {
|
||||
"message": "数量"
|
||||
},
|
||||
|
@ -116,6 +119,9 @@
|
|||
"confirmTransaction": {
|
||||
"message": "确认交易"
|
||||
},
|
||||
"connectHardwareWallet": {
|
||||
"message": "链接硬件钱包"
|
||||
},
|
||||
"continue": {
|
||||
"message": "继续"
|
||||
},
|
||||
|
@ -714,6 +720,9 @@
|
|||
"search": {
|
||||
"message": "搜索"
|
||||
},
|
||||
"searchResults": {
|
||||
"message": "搜索结果"
|
||||
},
|
||||
"secretPhrase": {
|
||||
"message": "输入12位助记词以恢复金库."
|
||||
},
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.67589 0.641872C8.65169 0.642635 8.62756 0.644749 8.6036 0.648202H4.79279C4.55863 0.644896 4.34082 0.767704 4.22278 0.969601C4.10473 1.1715 4.10473 1.4212 4.22278 1.6231C4.34082 1.825 4.55863 1.9478 4.79279 1.9445H7.12113L0.437932 8.61587C0.268309 8.77843 0.19998 9.01984 0.259298 9.24697C0.318616 9.47411 0.496311 9.65149 0.723852 9.71071C0.951393 9.76992 1.19322 9.70171 1.35608 9.53239L8.03927 2.86102V5.18524C8.03596 5.41898 8.15899 5.6364 8.36124 5.75424C8.56349 5.87208 8.81364 5.87208 9.0159 5.75424C9.21815 5.6364 9.34118 5.41898 9.33787 5.18524V1.37863C9.36404 1.18976 9.30558 0.998955 9.17804 0.857009C9.0505 0.715062 8.86682 0.636369 8.67589 0.641872Z" fill="#359BDD"/>
|
||||
</svg>
|
After Width: | Height: | Size: 795 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1916.3 516.8" width="2500" height="674"><style>.st0{fill:#333745}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>
|
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
@ -0,0 +1,115 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1672.1 241.2"
|
||||
style="enable-background:new 0 0 1672.1 241.2;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#161616;}
|
||||
.st1{fill:#E17726;stroke:#E17726;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st2{fill:#E27625;stroke:#E27625;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st3{fill:#D5BFB2;stroke:#D5BFB2;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st4{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st5{fill:#CC6228;stroke:#CC6228;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st6{fill:#E27525;stroke:#E27525;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st7{fill:#F5841F;stroke:#F5841F;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st8{fill:#C0AC9D;stroke:#C0AC9D;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st9{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st10{fill:#763E1A;stroke:#763E1A;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st11{fill:#F5841F;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M1157.7,121.9c-6.8-4.5-14.3-7.7-21.4-11.7c-4.6-2.6-9.5-4.9-13.5-8.2c-6.8-5.6-5.4-16.6,1.7-21.4
|
||||
c10.2-6.8,27.1-3,28.9,10.9c0,0.3,0.3,0.5,0.6,0.5h15.4c0.4,0,0.7-0.3,0.6-0.7c-0.8-9.6-4.5-17.6-11.3-22.7
|
||||
c-6.5-4.9-13.9-7.5-21.8-7.5c-40.7,0-44.4,43.1-22.5,56.7c2.5,1.6,24,12.4,31.6,17.1s10,13.3,6.7,20.1c-3,6.2-10.8,10.5-18.6,10
|
||||
c-8.5-0.5-15.1-5.1-17.4-12.3c-0.4-1.3-0.6-3.8-0.6-4.9c0-0.3-0.3-0.6-0.6-0.6h-16.7c-0.3,0-0.6,0.3-0.6,0.6
|
||||
c0,12.1,3,18.8,11.2,24.9c7.7,5.8,16.1,8.2,24.8,8.2c22.8,0,34.6-12.9,37-26.3C1173.3,141.5,1169.4,129.7,1157.7,121.9z"/>
|
||||
<path class="st0" d="M432.6,63.3h-7.4h-8.1c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2
|
||||
c-0.1-0.3-0.3-0.4-0.6-0.4h-8.1h-7.4h-10c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6
|
||||
c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5
|
||||
c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L432.6,63.3
|
||||
L432.6,63.3z"/>
|
||||
<path class="st0" d="M902,63.3c-0.3,0-0.5,0.2-0.6,0.4l-13.7,45.2c-0.2,0.6-1,0.6-1.2,0l-13.7-45.2c-0.1-0.3-0.3-0.4-0.6-0.4h-25.4
|
||||
c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V91.6c0-0.7,1-0.8,1.2-0.2l13.8,45.5l1,3.2
|
||||
c0.1,0.3,0.3,0.4,0.6,0.4h12.8c0.3,0,0.5-0.2,0.6-0.4l1-3.2l13.8-45.5c0.2-0.7,1.2-0.5,1.2,0.2v87.7c0,0.3,0.3,0.6,0.6,0.6h16.7
|
||||
c0.3,0,0.6-0.3,0.6-0.6V63.9c0-0.3-0.3-0.6-0.6-0.6L902,63.3L902,63.3z"/>
|
||||
<path class="st0" d="M686.6,63.3h-31.1h-16.7h-31.1c-0.3,0-0.6,0.3-0.6,0.6v14.4c0,0.3,0.3,0.6,0.6,0.6h30.5v100.4
|
||||
c0,0.3,0.3,0.6,0.6,0.6h16.7c0.3,0,0.6-0.3,0.6-0.6V78.9h30.5c0.3,0,0.6-0.3,0.6-0.6V63.9C687.2,63.6,687,63.3,686.6,63.3z"/>
|
||||
<path class="st0" d="M785.1,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8L769.5,63.3c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8
|
||||
c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6
|
||||
c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C784.6,179.7,784.9,179.9,785.1,179.9z M745.2,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1
|
||||
c0.1,0.4-0.2,0.8-0.6,0.8h-24.4C745.4,129.7,745.1,129.3,745.2,128.9z"/>
|
||||
<path class="st0" d="M1044.3,179.9h15.2c0.4,0,0.7-0.4,0.6-0.8l-31.4-115.8c-0.1-0.3-0.3-0.4-0.6-0.4h-5.8h-10.2h-5.8
|
||||
c-0.3,0-0.5,0.2-0.6,0.4l-31.4,115.8c-0.1,0.4,0.2,0.8,0.6,0.8h15.2c0.3,0,0.5-0.2,0.6-0.4l9.1-33.7c0.1-0.3,0.3-0.4,0.6-0.4h33.6
|
||||
c0.3,0,0.5,0.2,0.6,0.4l9.1,33.7C1043.8,179.7,1044,179.9,1044.3,179.9z M1004.4,128.9l12.2-45.1c0.2-0.6,1-0.6,1.2,0l12.2,45.1
|
||||
c0.1,0.4-0.2,0.8-0.6,0.8H1005C1004.6,129.7,1004.3,129.3,1004.4,128.9z"/>
|
||||
<path class="st0" d="M510.8,162.8V127c0-0.3,0.3-0.6,0.6-0.6h44.5c0.3,0,0.6-0.3,0.6-0.6v-14.4c0-0.3-0.3-0.6-0.6-0.6h-44.5
|
||||
c-0.3,0-0.6-0.3-0.6-0.6V79.6c0-0.3,0.3-0.6,0.6-0.6H562c0.3,0,0.6-0.3,0.6-0.6V64c0-0.3-0.3-0.6-0.6-0.6h-51.2h-17.3
|
||||
c-0.3,0-0.6,0.3-0.6,0.6v15v31.9v15.6v37v15.8c0,0.3,0.3,0.6,0.6,0.6h17.3h53.3c0.3,0,0.6-0.3,0.6-0.6v-15.2c0-0.3-0.3-0.6-0.6-0.6
|
||||
h-52.8C511,163.4,510.8,163.2,510.8,162.8z"/>
|
||||
<path class="st0" d="M1310.3,178.9l-57.8-59.7c-0.2-0.2-0.2-0.6,0-0.8l52-54c0.4-0.4,0.1-1-0.4-1h-21.3c-0.2,0-0.3,0.1-0.4,0.2
|
||||
l-44.1,45.8c-0.4,0.4-1,0.1-1-0.4V64c0-0.3-0.3-0.6-0.6-0.6H1220c-0.3,0-0.6,0.3-0.6,0.6v115.4c0,0.3,0.3,0.6,0.6,0.6h16.7
|
||||
c0.3,0,0.6-0.3,0.6-0.6v-50.8c0-0.5,0.7-0.8,1-0.4l50,51.6c0.1,0.1,0.3,0.2,0.4,0.2h21.3C1310.4,179.9,1310.7,179.2,1310.3,178.9z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1" points="247.1,1.2 146,76.2 164.8,32 "/>
|
||||
<g>
|
||||
<polygon class="st2" points="13.9,1.2 114.1,76.9 96.2,32 "/>
|
||||
<polygon class="st2" points="210.7,175.1 183.8,216.3 241.4,232.2 257.9,176 "/>
|
||||
<polygon class="st2" points="3.2,176 19.6,232.2 77.1,216.3 50.3,175.1 "/>
|
||||
<polygon class="st2" points="74,105.5 58,129.7 115,132.3 113.1,70.8 "/>
|
||||
<polygon class="st2" points="187,105.5 147.3,70.1 146,132.3 203,129.7 "/>
|
||||
<polygon class="st2" points="77.1,216.3 111.6,199.6 81.9,176.4 "/>
|
||||
<polygon class="st2" points="149.4,199.6 183.8,216.3 179.1,176.4 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st3" points="183.8,216.3 149.4,199.6 152.2,222 151.9,231.5 "/>
|
||||
<polygon class="st3" points="77.1,216.3 109.1,231.5 108.9,222 111.6,199.6 "/>
|
||||
</g>
|
||||
<polygon class="st4" points="109.7,161.6 81.1,153.2 101.3,143.9 "/>
|
||||
<polygon class="st4" points="151.3,161.6 159.7,143.9 180,153.2 "/>
|
||||
<g>
|
||||
<polygon class="st5" points="77.1,216.3 82.1,175.1 50.3,176 "/>
|
||||
<polygon class="st5" points="178.9,175.1 183.8,216.3 210.7,176 "/>
|
||||
<polygon class="st5" points="203,129.7 146,132.3 151.3,161.6 159.7,143.9 180,153.2 "/>
|
||||
<polygon class="st5" points="81.1,153.2 101.3,143.9 109.7,161.6 115,132.3 58,129.7 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st6" points="58,129.7 81.9,176.4 81.1,153.2 "/>
|
||||
<polygon class="st6" points="180,153.2 179.1,176.4 203,129.7 "/>
|
||||
<polygon class="st6" points="115,132.3 109.7,161.6 116.4,196.2 117.9,150.6 "/>
|
||||
<polygon class="st6" points="146,132.3 143.2,150.5 144.6,196.2 151.3,161.6 "/>
|
||||
</g>
|
||||
<polygon class="st7" points="151.3,161.6 144.6,196.2 149.4,199.6 179.1,176.4 180,153.2 "/>
|
||||
<polygon class="st7" points="81.1,153.2 81.9,176.4 111.6,199.6 116.4,196.2 109.7,161.6 "/>
|
||||
<polygon class="st8" points="151.9,231.5 152.2,222 149.6,219.8 111.4,219.8 108.9,222 109.1,231.5 77.1,216.3 88.3,225.5
|
||||
111,241.2 149.9,241.2 172.7,225.5 183.8,216.3 "/>
|
||||
<polygon class="st9" points="149.4,199.6 144.6,196.2 116.4,196.2 111.6,199.6 108.9,222 111.4,219.8 149.6,219.8 152.2,222 "/>
|
||||
<g>
|
||||
<polygon class="st10" points="251.4,81.1 259.9,39.7 247.1,1.2 149.4,73.7 187,105.5 240.1,121 251.8,107.3 246.7,103.6
|
||||
254.8,96.2 248.6,91.4 256.7,85.2 "/>
|
||||
<polygon class="st10" points="1.1,39.7 9.7,81.1 4.2,85.2 12.4,91.4 6.2,96.2 14.3,103.6 9.2,107.3 20.9,121 74,105.5 111.6,73.7
|
||||
13.9,1.2 "/>
|
||||
</g>
|
||||
<polygon class="st7" points="240.1,121 187,105.5 203,129.7 179.1,176.4 210.7,176 257.9,176 "/>
|
||||
<polygon class="st7" points="74,105.5 20.9,121 3.2,176 50.3,176 81.9,176.4 58,129.7 "/>
|
||||
<polygon class="st7" points="146,132.3 149.4,73.7 164.8,32 96.2,32 111.6,73.7 115,132.3 116.3,150.7 116.4,196.2 144.6,196.2
|
||||
144.7,150.7 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st7" d="M1409.7,92.6V32.1h19.3c2.9,0,5.6,0.4,8.2,1c2.5,0.6,4.7,1.6,6.6,2.9c1.9,1.3,3.4,3,4.5,5.1
|
||||
c1.1,2.1,1.6,4.5,1.6,7.3c0,3-0.9,5.5-2.5,7.7c-1.6,2.1-3.8,3.8-6.6,4.9c1.7,0.5,3.2,1.1,4.5,2c1.3,0.9,2.4,1.9,3.3,3.1
|
||||
c0.9,1.2,1.6,2.6,2,4c0.5,1.5,0.7,3.1,0.7,4.7c0,2.9-0.5,5.4-1.6,7.6c-1.1,2.2-2.5,4-4.4,5.5c-1.9,1.5-4.1,2.6-6.6,3.3
|
||||
c-2.6,0.8-5.3,1.2-8.3,1.2H1409.7z M1419.8,57.6h9.6c1.5,0,2.9-0.2,4.2-0.6c1.3-0.4,2.4-0.9,3.3-1.7c0.9-0.7,1.7-1.6,2.2-2.7
|
||||
c0.6-1.1,0.8-2.3,0.8-3.7c0-1.5-0.3-2.8-0.8-3.9c-0.5-1.1-1.3-2-2.2-2.7c-1-0.7-2.1-1.2-3.4-1.5c-1.3-0.3-2.7-0.5-4.3-0.5h-9.5
|
||||
V57.6z M1419.8,65.2v19.3h10.9c1.6,0,3-0.3,4.3-0.7c1.3-0.5,2.4-1.1,3.4-1.9c0.9-0.8,1.7-1.8,2.2-2.9c0.5-1.2,0.8-2.5,0.8-3.9
|
||||
c0-1.5-0.2-2.9-0.7-4.1c-0.5-1.2-1.1-2.2-2-3.1c-0.9-0.8-2-1.5-3.2-1.9c-1.3-0.5-2.7-0.7-4.3-0.7H1419.8z"/>
|
||||
<path class="st7" d="M1506.3,65.5h-24.9v19h29.1v8.1h-39.1V32.1h38.8v8.2h-28.8v17.1h24.9V65.5z"/>
|
||||
<path class="st7" d="M1574.8,40.4h-18.6v52.2h-9.9V40.4h-18.4v-8.2h46.9V40.4z"/>
|
||||
<path class="st7" d="M1615.2,78.6h-18.9l-4.2,14h-10.3l19.6-60.5h8.8l19.3,60.5h-10.3L1615.2,78.6z M1598.9,70h13.9l-6.9-23.7
|
||||
L1598.9,70z"/>
|
||||
</g>
|
||||
<path class="st11" d="M1644.3,8c10.7,0,19.5,8.7,19.5,19.5v69.8c0,10.7-8.7,19.5-19.5,19.5H1395c-10.7,0-19.5-8.7-19.5-19.5V27.5
|
||||
c0-10.7,8.7-19.5,19.5-19.5H1644.3 M1644.3,0H1395c-15.2,0-27.5,12.3-27.5,27.5v69.8c0,15.2,12.3,27.5,27.5,27.5h249.2
|
||||
c15.2,0,27.5-12.3,27.5-27.5V27.5C1671.7,12.3,1659.4,0,1644.3,0L1644.3,0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="trezor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2567.5 722.3" width="2567.5" height="722.3"><style>.st0{display:none;fill:#fff;stroke:#000}</style><path id="rect25" class="st0" d="M1186 2932.6h46.2v147H1186v-147z"/><path id="path7" d="M249 0C149.9 0 69.7 80.2 69.7 179.3v67.2C34.9 252.8 0 261.2 0 272.1v350.7s0 9.7 10.9 14.3c39.5 16 194.9 71 230.6 83.6 4.6 1.7 5.9 1.7 7.1 1.7 1.7 0 2.5 0 7.1-1.7 35.7-12.6 191.5-67.6 231-83.6 10.1-4.2 10.5-13.9 10.5-13.9V272.1c0-10.9-34.4-19.7-69.3-25.6v-67.2C428.4 80.2 347.7 0 249 0zm0 85.7c58.4 0 93.7 35.3 93.7 93.7v58.4c-65.5-4.6-121.4-4.6-187.3 0v-58.4c0-58.5 35.3-93.7 93.6-93.7zm-.4 238.1c81.5 0 149.9 6.3 149.9 17.6v218.8c0 3.4-.4 3.8-3.4 5-2.9 1.3-139 50.4-139 50.4s-5.5 1.7-7.1 1.7c-1.7 0-7.1-2.1-7.1-2.1s-136.1-49.1-139-50.4-3.4-1.7-3.4-5V341c-.8-11.3 67.6-17.2 149.1-17.2z"/><g id="g3222" transform="translate(91.363 -287.434) scale(.95575)"><path id="path13" d="M666.6 890V639.3H575v-89.9h285.6v89.9h-90.7V890H666.6z"/><path id="path15" d="M1092 890l-47-107.1h-37.4V890H904.3V549.4h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5H1092zm12.2-223.9c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.2-.4 33.6-8.4 33.6-27.3z"/><path id="path17" d="M1262.9 890V549.4h258.3v89.9h-155.4v33.6h151.6v89.9h-151.6v37.4h155.4V890h-258.3z"/><path id="path19" d="M1574.9 890.4v-81.9l129.8-168.8h-129.8v-89.9h265.8v81.1l-130.2 169.7h134v89.9l-269.6-.1z"/><path id="path21" d="M1869.7 720.3c0-104.6 81.1-176.4 186.5-176.4 105 0 186.5 71.4 186.5 176.4 0 104.6-81.1 176-186.5 176s-186.5-71.4-186.5-176zm268 0c0-47.5-32.3-85.3-81.9-85.3-49.6 0-81.9 37.8-81.9 85.3s32.3 85.3 81.9 85.3c50 0 81.9-37.8 81.9-85.3z"/><path id="path23" d="M2473.6 890.4l-47-107.1h-37.4v107.1h-103.3V549.8h181.8c79.8 0 122.6 52.9 122.6 116.7 0 58.8-34 89.9-61.3 103.3l61.7 120.5h-117.1zm12.6-224.3c0-18.5-16.4-26.5-33.6-26.5h-63v53.8h63c17.3-.4 33.6-8.4 33.6-27.3z"/></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -63,7 +63,6 @@
|
|||
"activeTab",
|
||||
"webRequest",
|
||||
"*://*.eth/",
|
||||
"*://*.test/",
|
||||
"notifications"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
ga('create', 'UA-68598031-1', 'auto' {'allowLinker':true});
|
||||
ga('send', 'pageview');
|
||||
ga('require', 'linker');
|
||||
ga('linker:autoLink', ['harrydenley.com', 'metamask.io'], false, true);
|
||||
ga('linker:autoLink', ['metamask.io'], false, true);
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
|
|
@ -15,11 +15,11 @@ const asStream = require('obs-store/lib/asStream')
|
|||
const ExtensionPlatform = require('./platforms/extension')
|
||||
const Migrator = require('./lib/migrator/')
|
||||
const migrations = require('./migrations/')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const PortStream = require('extension-port-stream')
|
||||
const createStreamSink = require('./lib/createStreamSink')
|
||||
const NotificationManager = require('./lib/notification-manager.js')
|
||||
const MetamaskController = require('./metamask-controller')
|
||||
const firstTimeState = require('./first-time-state')
|
||||
const rawFirstTimeState = require('./first-time-state')
|
||||
const setupRaven = require('./lib/setupRaven')
|
||||
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
|
||||
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
|
||||
|
@ -34,6 +34,9 @@ const {
|
|||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
} = require('./lib/enums')
|
||||
|
||||
// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost
|
||||
const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG)
|
||||
|
||||
const STORAGE_KEY = 'metamask-config'
|
||||
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
|
||||
|
||||
|
@ -253,6 +256,7 @@ function setupController (initState, initLangCode) {
|
|||
showUnconfirmedMessage: triggerUi,
|
||||
unlockAccountMessage: triggerUi,
|
||||
showUnapprovedTx: triggerUi,
|
||||
showWatchAssetUi: showWatchAssetUi,
|
||||
// initial state
|
||||
initState,
|
||||
// initial locale code
|
||||
|
@ -440,9 +444,28 @@ function triggerUi () {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browser popup for user confirmation of watchAsset
|
||||
* then it waits until user interact with the UI
|
||||
*/
|
||||
function showWatchAssetUi () {
|
||||
triggerUi()
|
||||
return new Promise(
|
||||
(resolve) => {
|
||||
var interval = setInterval(() => {
|
||||
if (!notificationIsOpen) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// On first install, open a window to MetaMask website to how-it-works.
|
||||
extension.runtime.onInstalled.addListener(function (details) {
|
||||
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
|
||||
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const LocalMessageDuplexStream = require('post-message-stream')
|
|||
const PongStream = require('ping-pong-stream/pong')
|
||||
const ObjectMultiplex = require('obj-multiplex')
|
||||
const extension = require('extensionizer')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const PortStream = require('extension-port-stream')
|
||||
|
||||
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
|
||||
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
|
||||
|
|
|
@ -80,7 +80,7 @@ class BalanceController {
|
|||
}
|
||||
})
|
||||
this.accountTracker.store.subscribe(update)
|
||||
this.blockTracker.on('block', update)
|
||||
this.blockTracker.on('latest', update)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
const ObservableStore = require('obs-store')
|
||||
const extend = require('xtend')
|
||||
const log = require('loglevel')
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
|
||||
const createBlockReEmitMiddleware = require('eth-json-rpc-middleware/block-reemit')
|
||||
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
|
||||
const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
|
||||
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
|
||||
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
|
||||
const createInfuraMiddleware = require('eth-json-rpc-infura')
|
||||
const BlockTracker = require('eth-block-tracker')
|
||||
|
||||
module.exports = createInfuraClient
|
||||
|
||||
function createInfuraClient ({ network }) {
|
||||
const infuraMiddleware = createInfuraMiddleware({ network })
|
||||
const blockProvider = providerFromMiddleware(infuraMiddleware)
|
||||
const blockTracker = new BlockTracker({ provider: blockProvider })
|
||||
|
||||
const networkMiddleware = mergeMiddleware([
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightMiddleware(),
|
||||
createBlockReEmitMiddleware({ blockTracker, provider: blockProvider }),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
infuraMiddleware,
|
||||
])
|
||||
return { networkMiddleware, blockTracker }
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
|
||||
const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
|
||||
const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref')
|
||||
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
|
||||
const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
|
||||
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
|
||||
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
|
||||
const BlockTracker = require('eth-block-tracker')
|
||||
|
||||
module.exports = createJsonRpcClient
|
||||
|
||||
function createJsonRpcClient ({ rpcUrl }) {
|
||||
const fetchMiddleware = createFetchMiddleware({ rpcUrl })
|
||||
const blockProvider = providerFromMiddleware(fetchMiddleware)
|
||||
const blockTracker = new BlockTracker({ provider: blockProvider })
|
||||
|
||||
const networkMiddleware = mergeMiddleware([
|
||||
createBlockRefMiddleware({ blockTracker }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightMiddleware(),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
fetchMiddleware,
|
||||
])
|
||||
return { networkMiddleware, blockTracker }
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
|
||||
const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
|
||||
const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref')
|
||||
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
|
||||
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
|
||||
const BlockTracker = require('eth-block-tracker')
|
||||
|
||||
module.exports = createLocalhostClient
|
||||
|
||||
function createLocalhostClient () {
|
||||
const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' })
|
||||
const blockProvider = providerFromMiddleware(fetchMiddleware)
|
||||
const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 })
|
||||
|
||||
const networkMiddleware = mergeMiddleware([
|
||||
createBlockRefMiddleware({ blockTracker }),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
fetchMiddleware,
|
||||
])
|
||||
return { networkMiddleware, blockTracker }
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
|
||||
const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
|
||||
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
|
||||
const createWalletSubprovider = require('eth-json-rpc-middleware/wallet')
|
||||
|
||||
module.exports = createMetamaskMiddleware
|
||||
|
||||
function createMetamaskMiddleware ({
|
||||
version,
|
||||
getAccounts,
|
||||
processTransaction,
|
||||
processEthSignMessage,
|
||||
processTypedMessage,
|
||||
processPersonalMessage,
|
||||
getPendingNonce,
|
||||
}) {
|
||||
const metamaskMiddleware = mergeMiddleware([
|
||||
createScaffoldMiddleware({
|
||||
// staticSubprovider
|
||||
eth_syncing: false,
|
||||
web3_clientVersion: `MetaMask/v${version}`,
|
||||
}),
|
||||
createWalletSubprovider({
|
||||
getAccounts,
|
||||
processTransaction,
|
||||
processEthSignMessage,
|
||||
processTypedMessage,
|
||||
processPersonalMessage,
|
||||
}),
|
||||
createPendingNonceMiddleware({ getPendingNonce }),
|
||||
])
|
||||
return metamaskMiddleware
|
||||
}
|
||||
|
||||
function createPendingNonceMiddleware ({ getPendingNonce }) {
|
||||
return createAsyncMiddleware(async (req, res, next) => {
|
||||
if (req.method !== 'eth_getTransactionCount') return next()
|
||||
const address = req.params[0]
|
||||
const blockRef = req.params[1]
|
||||
if (blockRef !== 'pending') return next()
|
||||
req.result = await getPendingNonce(address)
|
||||
})
|
||||
}
|
|
@ -1,15 +1,17 @@
|
|||
const assert = require('assert')
|
||||
const EventEmitter = require('events')
|
||||
const createMetamaskProvider = require('web3-provider-engine/zero.js')
|
||||
const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js')
|
||||
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
|
||||
const ObservableStore = require('obs-store')
|
||||
const ComposedStore = require('obs-store/lib/composed')
|
||||
const extend = require('xtend')
|
||||
const EthQuery = require('eth-query')
|
||||
const createEventEmitterProxy = require('../../lib/events-proxy.js')
|
||||
const JsonRpcEngine = require('json-rpc-engine')
|
||||
const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine')
|
||||
const log = require('loglevel')
|
||||
const urlUtil = require('url')
|
||||
const createMetamaskMiddleware = require('./createMetamaskMiddleware')
|
||||
const createInfuraClient = require('./createInfuraClient')
|
||||
const createJsonRpcClient = require('./createJsonRpcClient')
|
||||
const createLocalhostClient = require('./createLocalhostClient')
|
||||
const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy')
|
||||
|
||||
const {
|
||||
ROPSTEN,
|
||||
RINKEBY,
|
||||
|
@ -17,7 +19,6 @@ const {
|
|||
MAINNET,
|
||||
LOCALHOST,
|
||||
} = require('./enums')
|
||||
const LOCALHOST_RPC_URL = 'http://localhost:8545'
|
||||
const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET]
|
||||
|
||||
const env = process.env.METAMASK_ENV
|
||||
|
@ -39,21 +40,27 @@ module.exports = class NetworkController extends EventEmitter {
|
|||
this.providerStore = new ObservableStore(providerConfig)
|
||||
this.networkStore = new ObservableStore('loading')
|
||||
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
|
||||
// create event emitter proxy
|
||||
this._proxy = createEventEmitterProxy()
|
||||
|
||||
this.on('networkDidChange', this.lookupNetwork)
|
||||
// provider and block tracker
|
||||
this._provider = null
|
||||
this._blockTracker = null
|
||||
// provider and block tracker proxies - because the network changes
|
||||
this._providerProxy = null
|
||||
this._blockTrackerProxy = null
|
||||
}
|
||||
|
||||
initializeProvider (_providerParams) {
|
||||
this._baseProviderParams = _providerParams
|
||||
initializeProvider (providerParams) {
|
||||
this._baseProviderParams = providerParams
|
||||
const { type, rpcTarget } = this.providerStore.getState()
|
||||
this._configureProvider({ type, rpcTarget })
|
||||
this._proxy.on('block', this._logBlock.bind(this))
|
||||
this._proxy.on('error', this.verifyNetwork.bind(this))
|
||||
this.ethQuery = new EthQuery(this._proxy)
|
||||
this.lookupNetwork()
|
||||
return this._proxy
|
||||
}
|
||||
|
||||
// return the proxies so the references will always be good
|
||||
getProviderAndBlockTracker () {
|
||||
const provider = this._providerProxy
|
||||
const blockTracker = this._blockTrackerProxy
|
||||
return { provider, blockTracker }
|
||||
}
|
||||
|
||||
verifyNetwork () {
|
||||
|
@ -75,10 +82,11 @@ module.exports = class NetworkController extends EventEmitter {
|
|||
|
||||
lookupNetwork () {
|
||||
// Prevent firing when provider is not defined.
|
||||
if (!this.ethQuery || !this.ethQuery.sendAsync) {
|
||||
return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery')
|
||||
if (!this._provider) {
|
||||
return log.warn('NetworkController - lookupNetwork aborted due to missing provider')
|
||||
}
|
||||
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
|
||||
const ethQuery = new EthQuery(this._provider)
|
||||
ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
|
||||
if (err) return this.setNetworkState('loading')
|
||||
log.info('web3.getNetwork returned ' + network)
|
||||
this.setNetworkState(network)
|
||||
|
@ -131,7 +139,7 @@ module.exports = class NetworkController extends EventEmitter {
|
|||
this._configureInfuraProvider(opts)
|
||||
// other type-based rpc endpoints
|
||||
} else if (type === LOCALHOST) {
|
||||
this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL })
|
||||
this._configureLocalhostProvider()
|
||||
// url-based rpc endpoints
|
||||
} else if (type === 'rpc') {
|
||||
this._configureStandardProvider({ rpcUrl: rpcTarget })
|
||||
|
@ -141,49 +149,47 @@ module.exports = class NetworkController extends EventEmitter {
|
|||
}
|
||||
|
||||
_configureInfuraProvider ({ type }) {
|
||||
log.info('_configureInfuraProvider', type)
|
||||
const infuraProvider = createInfuraProvider({ network: type })
|
||||
const infuraSubprovider = new SubproviderFromProvider(infuraProvider)
|
||||
const providerParams = extend(this._baseProviderParams, {
|
||||
engineParams: {
|
||||
pollingInterval: 8000,
|
||||
blockTrackerProvider: infuraProvider,
|
||||
},
|
||||
dataSubprovider: infuraSubprovider,
|
||||
})
|
||||
const provider = createMetamaskProvider(providerParams)
|
||||
this._setProvider(provider)
|
||||
log.info('NetworkController - configureInfuraProvider', type)
|
||||
const networkClient = createInfuraClient({ network: type })
|
||||
this._setNetworkClient(networkClient)
|
||||
}
|
||||
|
||||
_configureLocalhostProvider () {
|
||||
log.info('NetworkController - configureLocalhostProvider')
|
||||
const networkClient = createLocalhostClient()
|
||||
this._setNetworkClient(networkClient)
|
||||
}
|
||||
|
||||
_configureStandardProvider ({ rpcUrl }) {
|
||||
// urlUtil handles malformed urls
|
||||
rpcUrl = urlUtil.parse(rpcUrl).format()
|
||||
const providerParams = extend(this._baseProviderParams, {
|
||||
rpcUrl,
|
||||
engineParams: {
|
||||
pollingInterval: 8000,
|
||||
},
|
||||
})
|
||||
const provider = createMetamaskProvider(providerParams)
|
||||
this._setProvider(provider)
|
||||
log.info('NetworkController - configureStandardProvider', rpcUrl)
|
||||
const networkClient = createJsonRpcClient({ rpcUrl })
|
||||
this._setNetworkClient(networkClient)
|
||||
}
|
||||
|
||||
_setProvider (provider) {
|
||||
// collect old block tracker events
|
||||
const oldProvider = this._provider
|
||||
let blockTrackerHandlers
|
||||
if (oldProvider) {
|
||||
// capture old block handlers
|
||||
blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers
|
||||
// tear down
|
||||
oldProvider.removeAllListeners()
|
||||
oldProvider.stop()
|
||||
_setNetworkClient ({ networkMiddleware, blockTracker }) {
|
||||
const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams)
|
||||
const engine = new JsonRpcEngine()
|
||||
engine.push(metamaskMiddleware)
|
||||
engine.push(networkMiddleware)
|
||||
const provider = providerFromEngine(engine)
|
||||
this._setProviderAndBlockTracker({ provider, blockTracker })
|
||||
}
|
||||
// override block tracler
|
||||
provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers)
|
||||
// set as new provider
|
||||
|
||||
_setProviderAndBlockTracker ({ provider, blockTracker }) {
|
||||
// update or intialize proxies
|
||||
if (this._providerProxy) {
|
||||
this._providerProxy.setTarget(provider)
|
||||
} else {
|
||||
this._providerProxy = createSwappableProxy(provider)
|
||||
}
|
||||
if (this._blockTrackerProxy) {
|
||||
this._blockTrackerProxy.setTarget(blockTracker)
|
||||
} else {
|
||||
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { eventFilter: 'skipInternal' })
|
||||
}
|
||||
// set new provider and blockTracker
|
||||
this._provider = provider
|
||||
this._proxy.setTarget(provider)
|
||||
this._blockTracker = blockTracker
|
||||
}
|
||||
|
||||
_logBlock (block) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
const normalizeAddress = require('eth-sig-util').normalize
|
||||
const { isValidAddress } = require('ethereumjs-util')
|
||||
const extend = require('xtend')
|
||||
|
||||
|
||||
|
@ -14,6 +15,7 @@ class PreferencesController {
|
|||
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
|
||||
* @property {array} store.tokens The tokens the user wants display in their token lists
|
||||
* @property {object} store.accountTokens The tokens stored per account and then per network type
|
||||
* @property {object} store.assetImages Contains assets objects related to assets added
|
||||
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
|
||||
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
|
||||
* user wishes to see that feature
|
||||
|
@ -26,21 +28,42 @@ class PreferencesController {
|
|||
frequentRpcList: [],
|
||||
currentAccountTab: 'history',
|
||||
accountTokens: {},
|
||||
assetImages: {},
|
||||
tokens: [],
|
||||
suggestedTokens: {},
|
||||
useBlockie: false,
|
||||
featureFlags: {},
|
||||
currentLocale: opts.initLangCode,
|
||||
identities: {},
|
||||
lostIdentities: {},
|
||||
seedWords: null,
|
||||
forgottenPassword: false,
|
||||
}, opts.initState)
|
||||
|
||||
this.diagnostics = opts.diagnostics
|
||||
this.network = opts.network
|
||||
this.store = new ObservableStore(initState)
|
||||
this.showWatchAssetUi = opts.showWatchAssetUi
|
||||
this._subscribeProviderType()
|
||||
}
|
||||
// PUBLIC METHODS
|
||||
|
||||
/**
|
||||
* Sets the {@code forgottenPassword} state property
|
||||
* @param {boolean} forgottenPassword whether or not the user has forgotten their password
|
||||
*/
|
||||
setPasswordForgotten (forgottenPassword) {
|
||||
this.store.updateState({ forgottenPassword })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code seedWords} seed words
|
||||
* @param {string|null} seedWords the seed words
|
||||
*/
|
||||
setSeedWords (seedWords) {
|
||||
this.store.updateState({ seedWords })
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the `useBlockie` property
|
||||
*
|
||||
|
@ -51,6 +74,53 @@ class PreferencesController {
|
|||
this.store.updateState({ useBlockie: val })
|
||||
}
|
||||
|
||||
getSuggestedTokens () {
|
||||
return this.store.getState().suggestedTokens
|
||||
}
|
||||
|
||||
getAssetImages () {
|
||||
return this.store.getState().assetImages
|
||||
}
|
||||
|
||||
addSuggestedERC20Asset (tokenOpts) {
|
||||
this._validateERC20AssetParams(tokenOpts)
|
||||
const suggested = this.getSuggestedTokens()
|
||||
const { rawAddress, symbol, decimals, image } = tokenOpts
|
||||
const address = normalizeAddress(rawAddress)
|
||||
const newEntry = { address, symbol, decimals, image }
|
||||
suggested[address] = newEntry
|
||||
this.store.updateState({ suggestedTokens: suggested })
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC engine middleware for requesting new asset added
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param {Function} - next
|
||||
* @param {Function} - end
|
||||
*/
|
||||
async requestWatchAsset (req, res, next, end) {
|
||||
if (req.method === 'metamask_watchAsset') {
|
||||
const { type, options } = req.params
|
||||
switch (type) {
|
||||
case 'ERC20':
|
||||
const result = await this._handleWatchAssetERC20(options)
|
||||
if (result instanceof Error) {
|
||||
end(result)
|
||||
} else {
|
||||
res.result = result
|
||||
end()
|
||||
}
|
||||
break
|
||||
default:
|
||||
end(new Error(`Asset of type ${type} not supported`))
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the `useBlockie` property
|
||||
*
|
||||
|
@ -186,6 +256,13 @@ class PreferencesController {
|
|||
return selected
|
||||
}
|
||||
|
||||
removeSuggestedTokens () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.store.updateState({ suggestedTokens: {} })
|
||||
resolve({})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the `selectedAddress` property
|
||||
*
|
||||
|
@ -232,11 +309,11 @@ class PreferencesController {
|
|||
* @returns {Promise<array>} Promises the new array of AddedToken objects.
|
||||
*
|
||||
*/
|
||||
async addToken (rawAddress, symbol, decimals) {
|
||||
async addToken (rawAddress, symbol, decimals, image) {
|
||||
const address = normalizeAddress(rawAddress)
|
||||
const newEntry = { address, symbol, decimals }
|
||||
|
||||
const tokens = this.store.getState().tokens
|
||||
const assetImages = this.getAssetImages()
|
||||
const previousEntry = tokens.find((token, index) => {
|
||||
return token.address === address
|
||||
})
|
||||
|
@ -247,7 +324,8 @@ class PreferencesController {
|
|||
} else {
|
||||
tokens.push(newEntry)
|
||||
}
|
||||
this._updateAccountTokens(tokens)
|
||||
assetImages[address] = image
|
||||
this._updateAccountTokens(tokens, assetImages)
|
||||
return Promise.resolve(tokens)
|
||||
}
|
||||
|
||||
|
@ -260,8 +338,10 @@ class PreferencesController {
|
|||
*/
|
||||
removeToken (rawAddress) {
|
||||
const tokens = this.store.getState().tokens
|
||||
const assetImages = this.getAssetImages()
|
||||
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
|
||||
this._updateAccountTokens(updatedTokens)
|
||||
delete assetImages[rawAddress]
|
||||
this._updateAccountTokens(updatedTokens, assetImages)
|
||||
return Promise.resolve(updatedTokens)
|
||||
}
|
||||
|
||||
|
@ -322,7 +402,7 @@ class PreferencesController {
|
|||
|
||||
/**
|
||||
* Returns an updated rpcList based on the passed url and the current list.
|
||||
* The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
|
||||
* The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
|
||||
* end of the list. The current list is modified and returned as a promise.
|
||||
*
|
||||
* @param {string} _url The rpc url to add to the frequentRpcList.
|
||||
|
@ -338,7 +418,7 @@ class PreferencesController {
|
|||
if (_url !== 'http://localhost:8545') {
|
||||
rpcList.push(_url)
|
||||
}
|
||||
if (rpcList.length > 2) {
|
||||
if (rpcList.length > 3) {
|
||||
rpcList.shift()
|
||||
}
|
||||
return Promise.resolve(rpcList)
|
||||
|
@ -387,6 +467,7 @@ class PreferencesController {
|
|||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
|
||||
/**
|
||||
* Subscription to network provider type.
|
||||
*
|
||||
|
@ -405,10 +486,10 @@ class PreferencesController {
|
|||
* @param {array} tokens Array of tokens to be updated.
|
||||
*
|
||||
*/
|
||||
_updateAccountTokens (tokens) {
|
||||
_updateAccountTokens (tokens, assetImages) {
|
||||
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
|
||||
accountTokens[selectedAddress][providerType] = tokens
|
||||
this.store.updateState({ accountTokens, tokens })
|
||||
this.store.updateState({ accountTokens, tokens, assetImages })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -438,6 +519,47 @@ class PreferencesController {
|
|||
const tokens = accountTokens[selectedAddress][providerType]
|
||||
return { tokens, accountTokens, providerType, selectedAddress }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the suggestion of an ERC20 asset through `watchAsset`
|
||||
* *
|
||||
* @param {Promise} promise Promise according to addition of ERC20 token
|
||||
*
|
||||
*/
|
||||
async _handleWatchAssetERC20 (options) {
|
||||
const { address, symbol, decimals, image } = options
|
||||
const rawAddress = address
|
||||
try {
|
||||
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
const tokenOpts = { rawAddress, decimals, symbol, image }
|
||||
this.addSuggestedERC20Asset(tokenOpts)
|
||||
return this.showWatchAssetUi().then(() => {
|
||||
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
|
||||
return tokenAddresses.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the passed options for suggested token have all required properties.
|
||||
*
|
||||
* @param {Object} opts The options object to validate
|
||||
* @throws {string} Throw a custom error indicating that address, symbol and/or decimals
|
||||
* doesn't fulfill requirements
|
||||
*
|
||||
*/
|
||||
_validateERC20AssetParams (opts) {
|
||||
const { rawAddress, symbol, decimals } = opts
|
||||
if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
|
||||
if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
|
||||
const numDecimals = parseInt(decimals, 10)
|
||||
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
|
||||
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
|
||||
}
|
||||
if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferencesController
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
const extend = require('xtend')
|
||||
const BN = require('ethereumjs-util').BN
|
||||
const EthQuery = require('eth-query')
|
||||
const log = require('loglevel')
|
||||
const pify = require('pify')
|
||||
|
||||
class RecentBlocksController {
|
||||
|
||||
/**
|
||||
* Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled
|
||||
* upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event
|
||||
* upon the controller's construction and then the list is updated when the given block tracker gets a 'latest' event
|
||||
* (indicating that there is a new block to process).
|
||||
*
|
||||
* @typedef {Object} RecentBlocksController
|
||||
|
@ -16,7 +16,7 @@ class RecentBlocksController {
|
|||
* @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain
|
||||
* @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance.
|
||||
* @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction,
|
||||
* listens for 'block' events so that new blocks can be processed and added to storage.
|
||||
* listens for 'latest' events so that new blocks can be processed and added to storage.
|
||||
* @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider
|
||||
* @property {number} historyLength The maximum length of blocks to track
|
||||
* @property {object} store Stores the recentBlocks
|
||||
|
@ -34,7 +34,13 @@ class RecentBlocksController {
|
|||
}, opts.initState)
|
||||
this.store = new ObservableStore(initState)
|
||||
|
||||
this.blockTracker.on('block', this.processBlock.bind(this))
|
||||
this.blockTracker.on('latest', async (newBlockNumberHex) => {
|
||||
try {
|
||||
await this.processBlock(newBlockNumberHex)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
})
|
||||
this.backfill()
|
||||
}
|
||||
|
||||
|
@ -55,7 +61,11 @@ class RecentBlocksController {
|
|||
* @param {object} newBlock The new block to modify and add to the recentBlocks array
|
||||
*
|
||||
*/
|
||||
processBlock (newBlock) {
|
||||
async processBlock (newBlockNumberHex) {
|
||||
const newBlockNumber = Number.parseInt(newBlockNumberHex, 16)
|
||||
const newBlock = await this.getBlockByNumber(newBlockNumber, true)
|
||||
if (!newBlock) return
|
||||
|
||||
const block = this.mapTransactionsToPrices(newBlock)
|
||||
|
||||
const state = this.store.getState()
|
||||
|
@ -108,9 +118,9 @@ class RecentBlocksController {
|
|||
}
|
||||
|
||||
/**
|
||||
* On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks
|
||||
* On this.blockTracker's first 'latest' event after this RecentBlocksController's instantiation, the store.recentBlocks
|
||||
* array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first
|
||||
* 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
|
||||
* 'latest' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
|
||||
* the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest.
|
||||
*
|
||||
* Each iteration over the block numbers is delayed by 100 milliseconds.
|
||||
|
@ -118,18 +128,17 @@ class RecentBlocksController {
|
|||
* @returns {Promise<void>} Promises undefined
|
||||
*/
|
||||
async backfill () {
|
||||
this.blockTracker.once('block', async (block) => {
|
||||
const currentBlockNumber = Number.parseInt(block.number, 16)
|
||||
this.blockTracker.once('latest', async (blockNumberHex) => {
|
||||
const currentBlockNumber = Number.parseInt(blockNumberHex, 16)
|
||||
const blocksToFetch = Math.min(currentBlockNumber, this.historyLength)
|
||||
const prevBlockNumber = currentBlockNumber - 1
|
||||
const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index)
|
||||
await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => {
|
||||
try {
|
||||
const newBlock = await this.getBlockByNumber(targetBlockNumber)
|
||||
const newBlock = await this.getBlockByNumber(targetBlockNumber, true)
|
||||
if (!newBlock) return
|
||||
|
||||
if (newBlock) {
|
||||
this.backfillBlock(newBlock)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
|
@ -137,18 +146,6 @@ class RecentBlocksController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await
|
||||
*
|
||||
* @returns {Promise<void>} Promises undefined
|
||||
*
|
||||
*/
|
||||
async wait () {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses EthQuery to get a block that has a given block number.
|
||||
*
|
||||
|
@ -157,13 +154,8 @@ class RecentBlocksController {
|
|||
*
|
||||
*/
|
||||
async getBlockByNumber (number) {
|
||||
const bn = new BN(number)
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => {
|
||||
if (err) reject(err)
|
||||
resolve(block)
|
||||
})
|
||||
})
|
||||
const blockNumberHex = '0x' + number.toString(16)
|
||||
return await pify(this.ethQuery.getBlockByNumber).call(this.ethQuery, blockNumberHex, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
const TRANSACTION_TYPE_CANCEL = 'cancel'
|
||||
const TRANSACTION_TYPE_RETRY = 'retry'
|
||||
const TRANSACTION_TYPE_STANDARD = 'standard'
|
||||
|
||||
const TRANSACTION_STATUS_APPROVED = 'approved'
|
||||
|
||||
module.exports = {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_TYPE_RETRY,
|
||||
TRANSACTION_TYPE_STANDARD,
|
||||
TRANSACTION_STATUS_APPROVED,
|
||||
}
|
|
@ -11,6 +11,14 @@ const txUtils = require('./lib/util')
|
|||
const cleanErrorStack = require('../../lib/cleanErrorStack')
|
||||
const log = require('loglevel')
|
||||
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
|
||||
const {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_TYPE_RETRY,
|
||||
TRANSACTION_TYPE_STANDARD,
|
||||
TRANSACTION_STATUS_APPROVED,
|
||||
} = require('./enums')
|
||||
|
||||
const { hexToBn, bnToHex } = require('../../lib/util')
|
||||
|
||||
/**
|
||||
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||
|
@ -65,6 +73,7 @@ class TransactionController extends EventEmitter {
|
|||
this.store = this.txStateManager.store
|
||||
this.nonceTracker = new NonceTracker({
|
||||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
||||
})
|
||||
|
@ -78,13 +87,17 @@ class TransactionController extends EventEmitter {
|
|||
})
|
||||
|
||||
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
||||
this._setupListners()
|
||||
this._setupListeners()
|
||||
// memstore is computed from a few different stores
|
||||
this._updateMemstore()
|
||||
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
||||
this.networkStore.subscribe(() => this._updateMemstore())
|
||||
this.preferencesStore.subscribe(() => this._updateMemstore())
|
||||
|
||||
// request state update to finalize initialization
|
||||
this._updatePendingTxsAfterFirstBlock()
|
||||
}
|
||||
|
||||
/** @returns {number} the chainId*/
|
||||
getChainId () {
|
||||
const networkState = this.networkStore.getState()
|
||||
|
@ -155,7 +168,10 @@ class TransactionController extends EventEmitter {
|
|||
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
|
||||
txUtils.validateTxParams(normalizedTxParams)
|
||||
// construct txMeta
|
||||
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
|
||||
let txMeta = this.txStateManager.generateTxMeta({
|
||||
txParams: normalizedTxParams,
|
||||
type: TRANSACTION_TYPE_STANDARD,
|
||||
})
|
||||
this.addTx(txMeta)
|
||||
this.emit('newUnapprovedTx', txMeta)
|
||||
|
||||
|
@ -209,12 +225,46 @@ class TransactionController extends EventEmitter {
|
|||
txParams: originalTxMeta.txParams,
|
||||
lastGasPrice,
|
||||
loadingDefaults: false,
|
||||
type: TRANSACTION_TYPE_RETRY,
|
||||
})
|
||||
this.addTx(txMeta)
|
||||
this.emit('newUnapprovedTx', txMeta)
|
||||
return txMeta
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new approved transaction to attempt to cancel a previously submitted transaction. The
|
||||
* new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to
|
||||
* the sender's address, and has a higher gasPrice than that of the previous transaction.
|
||||
* @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
|
||||
* @param {string=} customGasPrice - the hex value to use for the cancel transaction
|
||||
* @returns {txMeta}
|
||||
*/
|
||||
async createCancelTransaction (originalTxId, customGasPrice) {
|
||||
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
||||
const { txParams } = originalTxMeta
|
||||
const { gasPrice: lastGasPrice, from, nonce } = txParams
|
||||
const newGasPrice = customGasPrice || bnToHex(hexToBn(lastGasPrice).mul(1.1))
|
||||
const newTxMeta = this.txStateManager.generateTxMeta({
|
||||
txParams: {
|
||||
from,
|
||||
to: from,
|
||||
nonce,
|
||||
gas: '0x5208',
|
||||
value: '0x0',
|
||||
gasPrice: newGasPrice,
|
||||
},
|
||||
lastGasPrice,
|
||||
loadingDefaults: false,
|
||||
status: TRANSACTION_STATUS_APPROVED,
|
||||
type: TRANSACTION_TYPE_CANCEL,
|
||||
})
|
||||
|
||||
this.addTx(newTxMeta)
|
||||
await this.approveTransaction(newTxMeta.id)
|
||||
return newTxMeta
|
||||
}
|
||||
|
||||
/**
|
||||
updates the txMeta in the txStateManager
|
||||
@param txMeta {Object} - the updated txMeta
|
||||
|
@ -311,6 +361,11 @@ class TransactionController extends EventEmitter {
|
|||
this.txStateManager.setTxStatusSubmitted(txId)
|
||||
}
|
||||
|
||||
confirmTransaction (txId) {
|
||||
this.txStateManager.setTxStatusConfirmed(txId)
|
||||
this._markNonceDuplicatesDropped(txId)
|
||||
}
|
||||
|
||||
/**
|
||||
Convenience method for the ui thats sets the transaction to rejected
|
||||
@param txId {number} - the tx's Id
|
||||
|
@ -354,6 +409,14 @@ class TransactionController extends EventEmitter {
|
|||
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
|
||||
}
|
||||
|
||||
// called once on startup
|
||||
async _updatePendingTxsAfterFirstBlock () {
|
||||
// wait for first block so we know we're ready
|
||||
await this.blockTracker.getLatestBlock()
|
||||
// get status update for all pending transactions (for the current network)
|
||||
await this.pendingTxTracker.updatePendingTxs()
|
||||
}
|
||||
|
||||
/**
|
||||
If transaction controller was rebooted with transactions that are uncompleted
|
||||
in steps of the transaction signing or user confirmation process it will either
|
||||
|
@ -375,7 +438,7 @@ class TransactionController extends EventEmitter {
|
|||
})
|
||||
|
||||
this.txStateManager.getFilteredTxList({
|
||||
status: 'approved',
|
||||
status: TRANSACTION_STATUS_APPROVED,
|
||||
}).forEach((txMeta) => {
|
||||
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
|
||||
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
|
||||
|
@ -386,14 +449,14 @@ class TransactionController extends EventEmitter {
|
|||
is called in constructor applies the listeners for pendingTxTracker txStateManager
|
||||
and blockTracker
|
||||
*/
|
||||
_setupListners () {
|
||||
_setupListeners () {
|
||||
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
||||
this._setupBlockTrackerListener()
|
||||
this.pendingTxTracker.on('tx:warning', (txMeta) => {
|
||||
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
|
||||
})
|
||||
this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
|
||||
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
|
||||
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
|
||||
this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId))
|
||||
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
|
||||
if (!txMeta.firstRetryBlockNumber) {
|
||||
txMeta.firstRetryBlockNumber = latestBlockNumber
|
||||
|
@ -405,13 +468,6 @@ class TransactionController extends EventEmitter {
|
|||
txMeta.retryCount++
|
||||
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
|
||||
})
|
||||
|
||||
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
||||
// this is a little messy but until ethstore has been either
|
||||
// removed or redone this is to guard against the race condition
|
||||
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
||||
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -435,6 +491,40 @@ class TransactionController extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
_setupBlockTrackerListener () {
|
||||
let listenersAreActive = false
|
||||
const latestBlockHandler = this._onLatestBlock.bind(this)
|
||||
const blockTracker = this.blockTracker
|
||||
const txStateManager = this.txStateManager
|
||||
|
||||
txStateManager.on('tx:status-update', updateSubscription)
|
||||
updateSubscription()
|
||||
|
||||
function updateSubscription () {
|
||||
const pendingTxs = txStateManager.getPendingTransactions()
|
||||
if (!listenersAreActive && pendingTxs.length > 0) {
|
||||
blockTracker.on('latest', latestBlockHandler)
|
||||
listenersAreActive = true
|
||||
} else if (listenersAreActive && !pendingTxs.length) {
|
||||
blockTracker.removeListener('latest', latestBlockHandler)
|
||||
listenersAreActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onLatestBlock (blockNumber) {
|
||||
try {
|
||||
await this.pendingTxTracker.updatePendingTxs()
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
try {
|
||||
await this.pendingTxTracker.resubmitPendingTxs(blockNumber)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the memStore in transaction controller
|
||||
*/
|
||||
|
|
|
@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex
|
|||
*/
|
||||
class NonceTracker {
|
||||
|
||||
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||
constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
|
||||
this.provider = provider
|
||||
this.blockTracker = blockTracker
|
||||
this.ethQuery = new EthQuery(provider)
|
||||
this.getPendingTransactions = getPendingTransactions
|
||||
this.getConfirmedTransactions = getConfirmedTransactions
|
||||
|
@ -34,7 +35,7 @@ class NonceTracker {
|
|||
* @typedef NonceDetails
|
||||
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
|
||||
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
|
||||
* @property {number} highetSuggested - The maximum between the other two, the number returned.
|
||||
* @property {number} highestSuggested - The maximum between the other two, the number returned.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -80,15 +81,6 @@ class NonceTracker {
|
|||
}
|
||||
}
|
||||
|
||||
async _getCurrentBlock () {
|
||||
const blockTracker = this._getBlockTracker()
|
||||
const currentBlock = blockTracker.getCurrentBlock()
|
||||
if (currentBlock) return currentBlock
|
||||
return await new Promise((reject, resolve) => {
|
||||
blockTracker.once('latest', resolve)
|
||||
})
|
||||
}
|
||||
|
||||
async _globalMutexFree () {
|
||||
const globalMutex = this._lookupMutex('global')
|
||||
const releaseLock = await globalMutex.acquire()
|
||||
|
@ -114,9 +106,8 @@ class NonceTracker {
|
|||
// calculate next nonce
|
||||
// we need to make sure our base count
|
||||
// and pending count are from the same block
|
||||
const currentBlock = await this._getCurrentBlock()
|
||||
const blockNumber = currentBlock.blockNumber
|
||||
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
|
||||
const blockNumber = await this.blockTracker.getLatestBlock()
|
||||
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber)
|
||||
const baseCount = baseCountBN.toNumber()
|
||||
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
|
||||
const nonceDetails = { blockNumber, baseCount }
|
||||
|
@ -165,15 +156,6 @@ class NonceTracker {
|
|||
return { name: 'local', nonce: highest, details: { startPoint, highest } }
|
||||
}
|
||||
|
||||
// this is a hotfix for the fact that the blockTracker will
|
||||
// change when the network changes
|
||||
|
||||
/**
|
||||
@returns {Object} the current blockTracker
|
||||
*/
|
||||
_getBlockTracker () {
|
||||
return this.provider._blockTracker
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NonceTracker
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const EventEmitter = require('events')
|
||||
const log = require('loglevel')
|
||||
const EthQuery = require('ethjs-query')
|
||||
|
||||
/**
|
||||
|
||||
Event emitter utility class for tracking the transactions as they<br>
|
||||
|
@ -23,55 +24,26 @@ class PendingTransactionTracker extends EventEmitter {
|
|||
super()
|
||||
this.query = new EthQuery(config.provider)
|
||||
this.nonceTracker = config.nonceTracker
|
||||
// default is one day
|
||||
this.getPendingTransactions = config.getPendingTransactions
|
||||
this.getCompletedTransactions = config.getCompletedTransactions
|
||||
this.publishTransaction = config.publishTransaction
|
||||
this._checkPendingTxs()
|
||||
this.confirmTransaction = config.confirmTransaction
|
||||
}
|
||||
|
||||
/**
|
||||
checks if a signed tx is in a block and
|
||||
if it is included emits tx status as 'confirmed'
|
||||
@param block {object}, a full block
|
||||
@emits tx:confirmed
|
||||
@emits tx:failed
|
||||
checks the network for signed txs and releases the nonce global lock if it is
|
||||
*/
|
||||
checkForTxInBlock (block) {
|
||||
const signedTxList = this.getPendingTransactions()
|
||||
if (!signedTxList.length) return
|
||||
signedTxList.forEach((txMeta) => {
|
||||
const txHash = txMeta.hash
|
||||
const txId = txMeta.id
|
||||
|
||||
if (!txHash) {
|
||||
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
||||
noTxHashErr.name = 'NoTxHashError'
|
||||
this.emit('tx:failed', txId, noTxHashErr)
|
||||
return
|
||||
async updatePendingTxs () {
|
||||
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
||||
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
|
||||
try {
|
||||
const pendingTxs = this.getPendingTransactions()
|
||||
await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)))
|
||||
} catch (err) {
|
||||
log.error('PendingTransactionTracker - Error updating pending transactions')
|
||||
log.error(err)
|
||||
}
|
||||
|
||||
|
||||
block.transactions.forEach((tx) => {
|
||||
if (tx.hash === txHash) this.emit('tx:confirmed', txId)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
asks the network for the transaction to see if a block number is included on it
|
||||
if we have skipped/missed blocks
|
||||
@param object - oldBlock newBlock
|
||||
*/
|
||||
queryPendingTxs ({ oldBlock, newBlock }) {
|
||||
// check pending transactions on start
|
||||
if (!oldBlock) {
|
||||
this._checkPendingTxs()
|
||||
return
|
||||
}
|
||||
// if we synced by more than one block, check for missed pending transactions
|
||||
const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16)
|
||||
if (diff > 1) this._checkPendingTxs()
|
||||
nonceGlobalLock.releaseLock()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,11 +51,11 @@ class PendingTransactionTracker extends EventEmitter {
|
|||
@param block {object} - a block object
|
||||
@emits tx:warning
|
||||
*/
|
||||
resubmitPendingTxs (block) {
|
||||
resubmitPendingTxs (blockNumber) {
|
||||
const pending = this.getPendingTransactions()
|
||||
// only try resubmitting if their are transactions to resubmit
|
||||
if (!pending.length) return
|
||||
pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
|
||||
pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => {
|
||||
/*
|
||||
Dont marked as failed if the error is a "known" transaction warning
|
||||
"there is already a transaction with the same sender-nonce
|
||||
|
@ -145,6 +117,7 @@ class PendingTransactionTracker extends EventEmitter {
|
|||
this.emit('tx:retry', txMeta)
|
||||
return txHash
|
||||
}
|
||||
|
||||
/**
|
||||
Ask the network for the transaction to see if it has been include in a block
|
||||
@param txMeta {Object} - the txMeta object
|
||||
|
@ -174,9 +147,8 @@ class PendingTransactionTracker extends EventEmitter {
|
|||
}
|
||||
|
||||
// get latest transaction status
|
||||
let txParams
|
||||
try {
|
||||
txParams = await this.query.getTransactionByHash(txHash)
|
||||
const txParams = await this.query.getTransactionByHash(txHash)
|
||||
if (!txParams) return
|
||||
if (txParams.blockNumber) {
|
||||
this.emit('tx:confirmed', txId)
|
||||
|
@ -190,27 +162,13 @@ class PendingTransactionTracker extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
checks the network for signed txs and releases the nonce global lock if it is
|
||||
*/
|
||||
async _checkPendingTxs () {
|
||||
const signedTxList = this.getPendingTransactions()
|
||||
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
||||
const { releaseLock } = await this.nonceTracker.getGlobalLock()
|
||||
try {
|
||||
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
||||
} catch (err) {
|
||||
log.error('PendingTransactionWatcher - Error updating pending transactions')
|
||||
log.error(err)
|
||||
}
|
||||
releaseLock()
|
||||
}
|
||||
|
||||
/**
|
||||
checks to see if a confirmed txMeta has the same nonce
|
||||
@param txMeta {Object} - txMeta object
|
||||
@returns {boolean}
|
||||
*/
|
||||
|
||||
|
||||
async _checkIfNonceIsTaken (txMeta) {
|
||||
const address = txMeta.txParams.from
|
||||
const completed = this.getCompletedTransactions(address)
|
||||
|
|
|
@ -25,7 +25,7 @@ class TxGasUtil {
|
|||
@returns {object} the txMeta object with the gas written to the txParams
|
||||
*/
|
||||
async analyzeGasUsage (txMeta) {
|
||||
const block = await this.query.getBlockByNumber('latest', true)
|
||||
const block = await this.query.getBlockByNumber('latest', false)
|
||||
let estimatedGasHex
|
||||
try {
|
||||
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
|
||||
|
|
|
@ -353,6 +353,7 @@ class TransactionStateManager extends EventEmitter {
|
|||
const txMeta = this.getTx(txId)
|
||||
txMeta.err = {
|
||||
message: err.toString(),
|
||||
rpc: err.value,
|
||||
stack: err.stack,
|
||||
}
|
||||
this.updateTx(txMeta)
|
||||
|
|
|
@ -4,7 +4,7 @@ require('web3/dist/web3.min.js')
|
|||
const log = require('loglevel')
|
||||
const LocalMessageDuplexStream = require('post-message-stream')
|
||||
const setupDappAutoReload = require('./lib/auto-reload.js')
|
||||
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
|
||||
const MetamaskInpageProvider = require('metamask-inpage-provider')
|
||||
restoreContextAfterImports()
|
||||
|
||||
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
|
||||
|
@ -22,6 +22,25 @@ var metamaskStream = new LocalMessageDuplexStream({
|
|||
// compose the inpage provider
|
||||
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
|
||||
|
||||
// Augment the provider with its enable method
|
||||
inpageProvider.enable = function (options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (options.mockRejection) {
|
||||
reject('User rejected account access')
|
||||
} else {
|
||||
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(response.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.ethereum = inpageProvider
|
||||
|
||||
//
|
||||
// setup web3
|
||||
//
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
* on each new block.
|
||||
*/
|
||||
|
||||
const async = require('async')
|
||||
const EthQuery = require('eth-query')
|
||||
const ObservableStore = require('obs-store')
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
function noop () {}
|
||||
const log = require('loglevel')
|
||||
const pify = require('pify')
|
||||
|
||||
|
||||
class AccountTracker extends EventEmitter {
|
||||
class AccountTracker {
|
||||
|
||||
/**
|
||||
* This module is responsible for tracking any number of accounts and caching their current balances & transaction
|
||||
|
@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter {
|
|||
*
|
||||
*/
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
|
||||
const initState = {
|
||||
accounts: {},
|
||||
currentBlockGasLimit: '',
|
||||
|
@ -44,12 +41,29 @@ class AccountTracker extends EventEmitter {
|
|||
this.store = new ObservableStore(initState)
|
||||
|
||||
this._provider = opts.provider
|
||||
this._query = new EthQuery(this._provider)
|
||||
this._query = pify(new EthQuery(this._provider))
|
||||
this._blockTracker = opts.blockTracker
|
||||
// subscribe to latest block
|
||||
this._blockTracker.on('block', this._updateForBlock.bind(this))
|
||||
// blockTracker.currentBlock may be null
|
||||
this._currentBlockNumber = this._blockTracker.currentBlock
|
||||
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
|
||||
this._blockTracker.once('latest', blockNumber => {
|
||||
this._currentBlockNumber = blockNumber
|
||||
})
|
||||
// bind function for easier listener syntax
|
||||
this._updateForBlock = this._updateForBlock.bind(this)
|
||||
}
|
||||
|
||||
start () {
|
||||
// remove first to avoid double add
|
||||
this._blockTracker.removeListener('latest', this._updateForBlock)
|
||||
// add listener
|
||||
this._blockTracker.addListener('latest', this._updateForBlock)
|
||||
// fetch account balances
|
||||
this._updateAccounts()
|
||||
}
|
||||
|
||||
stop () {
|
||||
// remove listener
|
||||
this._blockTracker.removeListener('latest', this._updateForBlock)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,49 +81,57 @@ class AccountTracker extends EventEmitter {
|
|||
const accounts = this.store.getState().accounts
|
||||
const locals = Object.keys(accounts)
|
||||
|
||||
const toAdd = []
|
||||
const accountsToAdd = []
|
||||
addresses.forEach((upstream) => {
|
||||
if (!locals.includes(upstream)) {
|
||||
toAdd.push(upstream)
|
||||
accountsToAdd.push(upstream)
|
||||
}
|
||||
})
|
||||
|
||||
const toRemove = []
|
||||
const accountsToRemove = []
|
||||
locals.forEach((local) => {
|
||||
if (!addresses.includes(local)) {
|
||||
toRemove.push(local)
|
||||
accountsToRemove.push(local)
|
||||
}
|
||||
})
|
||||
|
||||
toAdd.forEach(upstream => this.addAccount(upstream))
|
||||
toRemove.forEach(local => this.removeAccount(local))
|
||||
this._updateAccounts()
|
||||
this.addAccounts(accountsToAdd)
|
||||
this.removeAccount(accountsToRemove)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be
|
||||
* Adds new addresses to track the balances of
|
||||
* given a balance as long this._currentBlockNumber is defined.
|
||||
*
|
||||
* @param {string} address A hex address of a new account to store in this AccountTracker's accounts object
|
||||
* @param {array} addresses An array of hex addresses of new accounts to track
|
||||
*
|
||||
*/
|
||||
addAccount (address) {
|
||||
addAccounts (addresses) {
|
||||
const accounts = this.store.getState().accounts
|
||||
// add initial state for addresses
|
||||
addresses.forEach(address => {
|
||||
accounts[address] = {}
|
||||
})
|
||||
// save accounts state
|
||||
this.store.updateState({ accounts })
|
||||
// fetch balances for the accounts if there is block number ready
|
||||
if (!this._currentBlockNumber) return
|
||||
this._updateAccount(address)
|
||||
addresses.forEach(address => this._updateAccount(address))
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an account from this AccountTracker's accounts object
|
||||
* Removes accounts from being tracked
|
||||
*
|
||||
* @param {string} address A hex address of a the account to remove
|
||||
* @param {array} an array of hex addresses to stop tracking
|
||||
*
|
||||
*/
|
||||
removeAccount (address) {
|
||||
removeAccount (addresses) {
|
||||
const accounts = this.store.getState().accounts
|
||||
// remove each state object
|
||||
addresses.forEach(address => {
|
||||
delete accounts[address]
|
||||
})
|
||||
// save accounts state
|
||||
this.store.updateState({ accounts })
|
||||
}
|
||||
|
||||
|
@ -118,72 +140,57 @@ class AccountTracker extends EventEmitter {
|
|||
* via EthQuery
|
||||
*
|
||||
* @private
|
||||
* @param {object} block Data about the block that contains the data to update to.
|
||||
* @param {number} blockNumber the block number to update to.
|
||||
* @fires 'block' The updated state, if all account updates are successful
|
||||
*
|
||||
*/
|
||||
_updateForBlock (block) {
|
||||
this._currentBlockNumber = block.number
|
||||
const currentBlockGasLimit = block.gasLimit
|
||||
async _updateForBlock (blockNumber) {
|
||||
this._currentBlockNumber = blockNumber
|
||||
|
||||
// block gasLimit polling shouldn't be in account-tracker shouldn't be here...
|
||||
const currentBlock = await this._query.getBlockByNumber(blockNumber, false)
|
||||
if (!currentBlock) return
|
||||
const currentBlockGasLimit = currentBlock.gasLimit
|
||||
this.store.updateState({ currentBlockGasLimit })
|
||||
|
||||
async.parallel([
|
||||
this._updateAccounts.bind(this),
|
||||
], (err) => {
|
||||
if (err) return console.error(err)
|
||||
this.emit('block', this.store.getState())
|
||||
})
|
||||
try {
|
||||
await this._updateAccounts()
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this._updateAccount for each account in this.store
|
||||
*
|
||||
* @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated
|
||||
* @returns {Promise} after all account balances updated
|
||||
*
|
||||
*/
|
||||
_updateAccounts (cb = noop) {
|
||||
async _updateAccounts () {
|
||||
const accounts = this.store.getState().accounts
|
||||
const addresses = Object.keys(accounts)
|
||||
async.each(addresses, this._updateAccount.bind(this), cb)
|
||||
await Promise.all(addresses.map(this._updateAccount.bind(this)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current balance of an account. Gets an updated balance via this._getAccount.
|
||||
* Updates the current balance of an account.
|
||||
*
|
||||
* @private
|
||||
* @param {string} address A hex address of a the account to be updated
|
||||
* @param {Function} cb A callback to call once the account at address is successfully update
|
||||
* @returns {Promise} after the account balance is updated
|
||||
*
|
||||
*/
|
||||
_updateAccount (address, cb = noop) {
|
||||
this._getAccount(address, (err, result) => {
|
||||
if (err) return cb(err)
|
||||
result.address = address
|
||||
const accounts = this.store.getState().accounts
|
||||
async _updateAccount (address) {
|
||||
// query balance
|
||||
const balance = await this._query.getBalance(address)
|
||||
const result = { address, balance }
|
||||
// update accounts state
|
||||
const { accounts } = this.store.getState()
|
||||
// only populate if the entry is still present
|
||||
if (accounts[address]) {
|
||||
if (!accounts[address]) return
|
||||
accounts[address] = result
|
||||
this.store.updateState({ accounts })
|
||||
}
|
||||
cb(null, result)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current balance of an account via EthQuery.
|
||||
*
|
||||
* @private
|
||||
* @param {string} address A hex address of a the account to query
|
||||
* @param {Function} cb A callback to call once the account at address is successfully update
|
||||
*
|
||||
*/
|
||||
_getAccount (address, cb = noop) {
|
||||
const query = this._query
|
||||
async.parallel({
|
||||
balance: query.getBalance.bind(query, address),
|
||||
}, cb)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
const ethUtil = require('ethereumjs-util')
|
||||
const normalize = require('eth-sig-util').normalize
|
||||
const {
|
||||
MAINNET_RPC_URL,
|
||||
ROPSTEN_RPC_URL,
|
||||
KOVAN_RPC_URL,
|
||||
RINKEBY_RPC_URL,
|
||||
} = require('../controllers/network/enums')
|
||||
|
||||
/* The config-manager is a convenience object
|
||||
* wrapping a pojo-migrator.
|
||||
*
|
||||
* It exists mostly to allow the creation of
|
||||
* convenience methods to access and persist
|
||||
* particular portions of the state.
|
||||
*/
|
||||
module.exports = ConfigManager
|
||||
function ConfigManager (opts) {
|
||||
// ConfigManager is observable and will emit updates
|
||||
this._subs = []
|
||||
this.store = opts.store
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setConfig = function (config) {
|
||||
var data = this.getData()
|
||||
data.config = config
|
||||
this.setData(data)
|
||||
this._emitUpdates(config)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getConfig = function () {
|
||||
var data = this.getData()
|
||||
return data.config
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setData = function (data) {
|
||||
this.store.putState(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getData = function () {
|
||||
return this.store.getState()
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) {
|
||||
const data = this.getData()
|
||||
data.forgottenPassword = passwordForgottenState
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) {
|
||||
const data = this.getData()
|
||||
return data.forgottenPassword
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setWallet = function (wallet) {
|
||||
var data = this.getData()
|
||||
data.wallet = wallet
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setVault = function (encryptedString) {
|
||||
var data = this.getData()
|
||||
data.vault = encryptedString
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getVault = function () {
|
||||
var data = this.getData()
|
||||
return data.vault
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getKeychains = function () {
|
||||
return this.getData().keychains || []
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setKeychains = function (keychains) {
|
||||
var data = this.getData()
|
||||
data.keychains = keychains
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getSelectedAccount = function () {
|
||||
var config = this.getConfig()
|
||||
return config.selectedAccount
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setSelectedAccount = function (address) {
|
||||
var config = this.getConfig()
|
||||
config.selectedAccount = ethUtil.addHexPrefix(address)
|
||||
this.setConfig(config)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getWallet = function () {
|
||||
return this.getData().wallet
|
||||
}
|
||||
|
||||
// Takes a boolean
|
||||
ConfigManager.prototype.setShowSeedWords = function (should) {
|
||||
var data = this.getData()
|
||||
data.showSeedWords = should
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
|
||||
ConfigManager.prototype.getShouldShowSeedWords = function () {
|
||||
var data = this.getData()
|
||||
return data.showSeedWords
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setSeedWords = function (words) {
|
||||
var data = this.getData()
|
||||
data.seedWords = words
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getSeedWords = function () {
|
||||
var data = this.getData()
|
||||
return data.seedWords
|
||||
}
|
||||
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
|
||||
var config = this.getConfig()
|
||||
config.provider = {
|
||||
type: 'rpc',
|
||||
rpcTarget: rpcUrl,
|
||||
}
|
||||
this.setConfig(config)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setProviderType = function (type) {
|
||||
var config = this.getConfig()
|
||||
config.provider = {
|
||||
type: type,
|
||||
}
|
||||
this.setConfig(config)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.useEtherscanProvider = function () {
|
||||
var config = this.getConfig()
|
||||
config.provider = {
|
||||
type: 'etherscan',
|
||||
}
|
||||
this.setConfig(config)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getProvider = function () {
|
||||
var config = this.getConfig()
|
||||
return config.provider
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getCurrentRpcAddress = function () {
|
||||
var provider = this.getProvider()
|
||||
if (!provider) return null
|
||||
switch (provider.type) {
|
||||
|
||||
case 'mainnet':
|
||||
return MAINNET_RPC_URL
|
||||
|
||||
case 'ropsten':
|
||||
return ROPSTEN_RPC_URL
|
||||
|
||||
case 'kovan':
|
||||
return KOVAN_RPC_URL
|
||||
|
||||
case 'rinkeby':
|
||||
return RINKEBY_RPC_URL
|
||||
|
||||
default:
|
||||
return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Tx
|
||||
//
|
||||
|
||||
ConfigManager.prototype.getTxList = function () {
|
||||
var data = this.getData()
|
||||
if (data.transactions !== undefined) {
|
||||
return data.transactions
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setTxList = function (txList) {
|
||||
var data = this.getData()
|
||||
data.transactions = txList
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
|
||||
// wallet nickname methods
|
||||
|
||||
ConfigManager.prototype.getWalletNicknames = function () {
|
||||
var data = this.getData()
|
||||
const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {}
|
||||
return nicknames
|
||||
}
|
||||
|
||||
ConfigManager.prototype.nicknameForWallet = function (account) {
|
||||
const address = normalize(account)
|
||||
const nicknames = this.getWalletNicknames()
|
||||
return nicknames[address]
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
|
||||
const address = normalize(account)
|
||||
const nicknames = this.getWalletNicknames()
|
||||
nicknames[address] = nickname
|
||||
var data = this.getData()
|
||||
data.walletNicknames = nicknames
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
// observable
|
||||
|
||||
ConfigManager.prototype.getSalt = function () {
|
||||
var data = this.getData()
|
||||
return data.salt
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setSalt = function (salt) {
|
||||
var data = this.getData()
|
||||
data.salt = salt
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.subscribe = function (fn) {
|
||||
this._subs.push(fn)
|
||||
var unsubscribe = this.unsubscribe.bind(this, fn)
|
||||
return unsubscribe
|
||||
}
|
||||
|
||||
ConfigManager.prototype.unsubscribe = function (fn) {
|
||||
var index = this._subs.indexOf(fn)
|
||||
if (index !== -1) this._subs.splice(index, 1)
|
||||
}
|
||||
|
||||
ConfigManager.prototype._emitUpdates = function (state) {
|
||||
this._subs.forEach(function (handler) {
|
||||
handler(state)
|
||||
})
|
||||
}
|
||||
|
||||
ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
|
||||
var data = this.getData()
|
||||
data.lostAccounts = lostAccounts
|
||||
this.setData(data)
|
||||
}
|
||||
|
||||
ConfigManager.prototype.getLostAccounts = function () {
|
||||
var data = this.getData()
|
||||
return data.lostAccounts || []
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
const log = require('loglevel')
|
||||
|
||||
/**
|
||||
* JSON-RPC error object
|
||||
*
|
||||
* @typedef {Object} RpcError
|
||||
* @property {number} code - Indicates the error type that occurred
|
||||
* @property {Object} [data] - Contains additional information about the error
|
||||
* @property {string} [message] - Short description of the error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Middleware configuration object
|
||||
*
|
||||
* @typedef {Object} MiddlewareConfig
|
||||
* @property {boolean} [override] - Use RPC_ERRORS message in place of provider message
|
||||
*/
|
||||
|
||||
/**
|
||||
* Map of standard and non-standard RPC error codes to messages
|
||||
*/
|
||||
const RPC_ERRORS = {
|
||||
1: 'An unauthorized action was attempted.',
|
||||
2: 'A disallowed action was attempted.',
|
||||
3: 'An execution error occurred.',
|
||||
[-32600]: 'The JSON sent is not a valid Request object.',
|
||||
[-32601]: 'The method does not exist / is not available.',
|
||||
[-32602]: 'Invalid method parameter(s).',
|
||||
[-32603]: 'Internal JSON-RPC error.',
|
||||
[-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.',
|
||||
internal: 'Internal server error.',
|
||||
unknown: 'Unknown JSON-RPC error.',
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a JSON-RPC error object in-place to add a human-readable message,
|
||||
* optionally overriding any provider-supplied message
|
||||
*
|
||||
* @param {RpcError} error - JSON-RPC error object
|
||||
* @param {boolean} override - Use RPC_ERRORS message in place of provider message
|
||||
*/
|
||||
function sanitizeRPCError (error, override) {
|
||||
if (error.message && !override) { return error }
|
||||
const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code]
|
||||
error.message = message || RPC_ERRORS.unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* json-rpc-engine middleware that both logs standard and non-standard error
|
||||
* messages and ends middleware stack traversal if an error is encountered
|
||||
*
|
||||
* @param {MiddlewareConfig} [config={override:true}] - Middleware configuration
|
||||
* @returns {Function} json-rpc-engine middleware function
|
||||
*/
|
||||
function createErrorMiddleware ({ override = true } = {}) {
|
||||
return (req, res, next) => {
|
||||
next(done => {
|
||||
const { error } = res
|
||||
if (!error) { return done() }
|
||||
sanitizeRPCError(error)
|
||||
log.error(`MetaMask - RPC Error: ${error.message}`, error)
|
||||
done()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createErrorMiddleware
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* Returns an EventEmitter that proxies events from the given event emitter
|
||||
* @param {any} eventEmitter
|
||||
* @param {object} listeners - The listeners to proxy to
|
||||
* @returns {any}
|
||||
*/
|
||||
module.exports = function createEventEmitterProxy (eventEmitter, listeners) {
|
||||
let target = eventEmitter
|
||||
const eventHandlers = listeners || {}
|
||||
const proxy = /** @type {any} */ (new Proxy({}, {
|
||||
get: (_, name) => {
|
||||
// intercept listeners
|
||||
if (name === 'on') return addListener
|
||||
if (name === 'setTarget') return setTarget
|
||||
if (name === 'proxyEventHandlers') return eventHandlers
|
||||
return (/** @type {any} */ (target))[name]
|
||||
},
|
||||
set: (_, name, value) => {
|
||||
target[name] = value
|
||||
return true
|
||||
},
|
||||
}))
|
||||
function setTarget (/** @type {EventEmitter} */ eventEmitter) {
|
||||
target = eventEmitter
|
||||
// migrate listeners
|
||||
Object.keys(eventHandlers).forEach((name) => {
|
||||
/** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Attaches a function to be called whenever the specified event is emitted
|
||||
* @param {string} name
|
||||
* @param {Function} handler
|
||||
*/
|
||||
function addListener (name, handler) {
|
||||
if (!eventHandlers[name]) eventHandlers[name] = []
|
||||
eventHandlers[name].push(handler)
|
||||
target.on(name, handler)
|
||||
}
|
||||
if (listeners) proxy.setTarget(eventEmitter)
|
||||
return proxy
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
const pump = require('pump')
|
||||
const RpcEngine = require('json-rpc-engine')
|
||||
const createErrorMiddleware = require('./createErrorMiddleware')
|
||||
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
|
||||
const createStreamMiddleware = require('json-rpc-middleware-stream')
|
||||
const LocalStorageStore = require('obs-store')
|
||||
const asStream = require('obs-store/lib/asStream')
|
||||
const ObjectMultiplex = require('obj-multiplex')
|
||||
|
||||
module.exports = MetamaskInpageProvider
|
||||
|
||||
function MetamaskInpageProvider (connectionStream) {
|
||||
const self = this
|
||||
|
||||
// setup connectionStream multiplexing
|
||||
const mux = self.mux = new ObjectMultiplex()
|
||||
pump(
|
||||
connectionStream,
|
||||
mux,
|
||||
connectionStream,
|
||||
(err) => logStreamDisconnectWarning('MetaMask', err)
|
||||
)
|
||||
|
||||
// subscribe to metamask public config (one-way)
|
||||
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
|
||||
|
||||
pump(
|
||||
mux.createStream('publicConfig'),
|
||||
asStream(self.publicConfigStore),
|
||||
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
|
||||
)
|
||||
|
||||
// ignore phishing warning message (handled elsewhere)
|
||||
mux.ignoreStream('phishing')
|
||||
|
||||
// connect to async provider
|
||||
const streamMiddleware = createStreamMiddleware()
|
||||
pump(
|
||||
streamMiddleware.stream,
|
||||
mux.createStream('provider'),
|
||||
streamMiddleware.stream,
|
||||
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
|
||||
)
|
||||
|
||||
// handle sendAsync requests via dapp-side rpc engine
|
||||
const rpcEngine = new RpcEngine()
|
||||
rpcEngine.push(createIdRemapMiddleware())
|
||||
rpcEngine.push(createErrorMiddleware())
|
||||
rpcEngine.push(streamMiddleware)
|
||||
self.rpcEngine = rpcEngine
|
||||
}
|
||||
|
||||
// handle sendAsync requests via asyncProvider
|
||||
// also remap ids inbound and outbound
|
||||
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
|
||||
const self = this
|
||||
|
||||
if (payload.method === 'eth_signTypedData') {
|
||||
console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.')
|
||||
}
|
||||
|
||||
self.rpcEngine.handle(payload, cb)
|
||||
}
|
||||
|
||||
|
||||
MetamaskInpageProvider.prototype.send = function (payload) {
|
||||
const self = this
|
||||
|
||||
let selectedAddress
|
||||
let result = null
|
||||
switch (payload.method) {
|
||||
|
||||
case 'eth_accounts':
|
||||
// read from localStorage
|
||||
selectedAddress = self.publicConfigStore.getState().selectedAddress
|
||||
result = selectedAddress ? [selectedAddress] : []
|
||||
break
|
||||
|
||||
case 'eth_coinbase':
|
||||
// read from localStorage
|
||||
selectedAddress = self.publicConfigStore.getState().selectedAddress
|
||||
result = selectedAddress || null
|
||||
break
|
||||
|
||||
case 'eth_uninstallFilter':
|
||||
self.sendAsync(payload, noop)
|
||||
result = true
|
||||
break
|
||||
|
||||
case 'net_version':
|
||||
const networkVersion = self.publicConfigStore.getState().networkVersion
|
||||
result = networkVersion || null
|
||||
break
|
||||
|
||||
// throw not-supported Error
|
||||
default:
|
||||
var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client'
|
||||
var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.`
|
||||
throw new Error(message)
|
||||
|
||||
}
|
||||
|
||||
// return the result
|
||||
return {
|
||||
id: payload.id,
|
||||
jsonrpc: payload.jsonrpc,
|
||||
result: result,
|
||||
}
|
||||
}
|
||||
|
||||
MetamaskInpageProvider.prototype.isConnected = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
MetamaskInpageProvider.prototype.isMetaMask = true
|
||||
|
||||
// util
|
||||
|
||||
function logStreamDisconnectWarning (remoteLabel, err) {
|
||||
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
|
||||
if (err) warningMsg += '\n' + err.stack
|
||||
console.warn(warningMsg)
|
||||
}
|
||||
|
||||
function noop () {}
|
|
@ -34,7 +34,7 @@ module.exports = function (provider) {
|
|||
return { cancel: true }
|
||||
}
|
||||
|
||||
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
|
||||
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']})
|
||||
|
||||
return {
|
||||
remove () {
|
||||
|
|
|
@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter {
|
|||
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object possibly containing the origin
|
||||
* @returns {promise} after signature has been
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessageAsync (msgParams, req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const msgId = this.addUnapprovedMessage(msgParams, req)
|
||||
// await finished
|
||||
this.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return resolve(data.rawSig)
|
||||
case 'rejected':
|
||||
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
|
||||
default:
|
||||
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the
|
||||
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object where the origin may be specificied
|
||||
* @returns {number} The id of the newly created message.
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessage (msgParams) {
|
||||
addUnapprovedMessage (msgParams, req) {
|
||||
// add origin from request
|
||||
if (req) msgParams.origin = req.origin
|
||||
msgParams.data = normalizeMsgData(msgParams.data)
|
||||
// create txData obj with parameters and meta data
|
||||
var time = (new Date()).getTime()
|
||||
|
|
|
@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter {
|
|||
* this.memStore.
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object possibly containing the origin
|
||||
* @returns {promise} When the message has been signed or rejected
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessageAsync (msgParams, req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!msgParams.from) {
|
||||
reject(new Error('MetaMask Message Signature: from field is required.'))
|
||||
}
|
||||
const msgId = this.addUnapprovedMessage(msgParams, req)
|
||||
this.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return resolve(data.rawSig)
|
||||
case 'rejected':
|
||||
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
|
||||
default:
|
||||
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
|
||||
* the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to
|
||||
* this.memStore.
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object possibly containing the origin
|
||||
* @returns {number} The id of the newly created PersonalMessage.
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessage (msgParams) {
|
||||
addUnapprovedMessage (msgParams, req) {
|
||||
log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
|
||||
// add origin from request
|
||||
if (req) msgParams.origin = req.origin
|
||||
msgParams.data = this.normalizeMsgData(msgParams.data)
|
||||
// create txData obj with parameters and meta data
|
||||
var time = (new Date()).getTime()
|
||||
|
@ -257,4 +289,3 @@ module.exports = class PersonalMessageManager extends EventEmitter {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
const Duplex = require('readable-stream').Duplex
|
||||
const inherits = require('util').inherits
|
||||
const noop = function () {}
|
||||
|
||||
module.exports = PortDuplexStream
|
||||
|
||||
inherits(PortDuplexStream, Duplex)
|
||||
|
||||
/**
|
||||
* Creates a stream that's both readable and writable.
|
||||
* The stream supports arbitrary objects.
|
||||
*
|
||||
* @class
|
||||
* @param {Object} port Remote Port object
|
||||
*/
|
||||
function PortDuplexStream (port) {
|
||||
Duplex.call(this, {
|
||||
objectMode: true,
|
||||
})
|
||||
this._port = port
|
||||
port.onMessage.addListener(this._onMessage.bind(this))
|
||||
port.onDisconnect.addListener(this._onDisconnect.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when a message is received from
|
||||
* the remote Port associated with this Stream.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} msg - Payload from the onMessage listener of Port
|
||||
*/
|
||||
PortDuplexStream.prototype._onMessage = function (msg) {
|
||||
if (Buffer.isBuffer(msg)) {
|
||||
delete msg._isBuffer
|
||||
var data = new Buffer(msg)
|
||||
this.push(data)
|
||||
} else {
|
||||
this.push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the remote Port
|
||||
* associated with this Stream disconnects.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
PortDuplexStream.prototype._onDisconnect = function () {
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly sets read operations to a no-op
|
||||
*/
|
||||
PortDuplexStream.prototype._read = noop
|
||||
|
||||
|
||||
/**
|
||||
* Called internally when data should be written to
|
||||
* this writable stream.
|
||||
*
|
||||
* @private
|
||||
* @param {*} msg Arbitrary object to write
|
||||
* @param {string} encoding Encoding to use when writing payload
|
||||
* @param {Function} cb Called when writing is complete or an error occurs
|
||||
*/
|
||||
PortDuplexStream.prototype._write = function (msg, encoding, cb) {
|
||||
try {
|
||||
if (Buffer.isBuffer(msg)) {
|
||||
var data = msg.toJSON()
|
||||
data._isBuffer = true
|
||||
this._port.postMessage(data)
|
||||
} else {
|
||||
this._port.postMessage(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
return cb(new Error('PortDuplexStream - disconnected'))
|
||||
}
|
||||
cb()
|
||||
}
|
|
@ -70,11 +70,11 @@ function simplifyErrorMessages (report) {
|
|||
|
||||
function rewriteErrorMessages (report, rewriteFn) {
|
||||
// rewrite top level message
|
||||
if (report.message) report.message = rewriteFn(report.message)
|
||||
if (typeof report.message === 'string') report.message = rewriteFn(report.message)
|
||||
// rewrite each exception message
|
||||
if (report.exception && report.exception.values) {
|
||||
report.exception.values.forEach(item => {
|
||||
item.value = rewriteFn(item.value)
|
||||
if (typeof item.value === 'string') item.value = rewriteFn(item.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ const createId = require('./random-id')
|
|||
const assert = require('assert')
|
||||
const sigUtil = require('eth-sig-util')
|
||||
const log = require('loglevel')
|
||||
const jsonschema = require('jsonschema')
|
||||
|
||||
/**
|
||||
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
|
||||
|
@ -17,7 +18,7 @@ const log = require('loglevel')
|
|||
* @property {Object} msgParams.from The address that is making the signature request.
|
||||
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
|
||||
* @property {number} time The epoch time at which the this message was created
|
||||
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
|
||||
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored'
|
||||
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
|
||||
* always have a 'eth_signTypedData' type.
|
||||
*
|
||||
|
@ -26,17 +27,10 @@ const log = require('loglevel')
|
|||
module.exports = class TypedMessageManager extends EventEmitter {
|
||||
/**
|
||||
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
|
||||
*
|
||||
* @typedef {Object} TypedMessage
|
||||
* @param {Object} opts @deprecated
|
||||
* @property {Object} memStore The observable store where TypedMessage are saved.
|
||||
* @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state
|
||||
* @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs
|
||||
* @property {array} messages Holds all messages that have been created by this TypedMessage
|
||||
*
|
||||
*/
|
||||
constructor (opts) {
|
||||
constructor ({ networkController }) {
|
||||
super()
|
||||
this.networkController = networkController
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedTypedMessages: {},
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
|
@ -72,11 +66,43 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
* this.memStore. Before any of this is done, msgParams are validated
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object possibly containing the origin
|
||||
* @returns {promise} When the message has been signed or rejected
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessageAsync (msgParams, req, version) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const msgId = this.addUnapprovedMessage(msgParams, req, version)
|
||||
this.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return resolve(data.rawSig)
|
||||
case 'rejected':
|
||||
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
|
||||
case 'errored':
|
||||
return reject(new Error(`MetaMask Message Signature: ${data.error}`))
|
||||
default:
|
||||
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
|
||||
* the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to
|
||||
* this.memStore. Before any of this is done, msgParams are validated
|
||||
*
|
||||
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
|
||||
* @param {Object} req (optional) The original request object possibly containing the origin
|
||||
* @returns {number} The id of the newly created TypedMessage.
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessage (msgParams) {
|
||||
addUnapprovedMessage (msgParams, req, version) {
|
||||
msgParams.version = version
|
||||
this.validateParams(msgParams)
|
||||
// add origin from request
|
||||
if (req) msgParams.origin = req.origin
|
||||
|
||||
log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
|
||||
// create txData obj with parameters and meta data
|
||||
|
@ -103,6 +129,8 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
*
|
||||
*/
|
||||
validateParams (params) {
|
||||
switch (params.version) {
|
||||
case 'V1':
|
||||
assert.equal(typeof params, 'object', 'Params should ben an object.')
|
||||
assert.ok('data' in params, 'Params must include a data field.')
|
||||
assert.ok('from' in params, 'Params must include a from field.')
|
||||
|
@ -111,6 +139,23 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
assert.doesNotThrow(() => {
|
||||
sigUtil.typedSignatureHash(params.data)
|
||||
}, 'Expected EIP712 typed data')
|
||||
break
|
||||
case 'V3':
|
||||
let data
|
||||
assert.equal(typeof params, 'object', 'Params should be an object.')
|
||||
assert.ok('data' in params, 'Params must include a data field.')
|
||||
assert.ok('from' in params, 'Params must include a from field.')
|
||||
assert.equal(typeof params.from, 'string', 'From field must be a string.')
|
||||
assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.')
|
||||
assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.')
|
||||
const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA)
|
||||
assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`)
|
||||
assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.')
|
||||
const chainId = data.domain.chainId
|
||||
const activeChainId = parseInt(this.networkController.getNetworkState())
|
||||
chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -185,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
*/
|
||||
prepMsgForSigning (msgParams) {
|
||||
delete msgParams.metamaskId
|
||||
delete msgParams.version
|
||||
return Promise.resolve(msgParams)
|
||||
}
|
||||
|
||||
|
@ -198,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
this._setMsgStatus(msgId, 'rejected')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
|
||||
*
|
||||
* @param {number} msgId The id of the TypedMessage to error
|
||||
*
|
||||
*/
|
||||
errorMessage (msgId, error) {
|
||||
const msg = this.getMsg(msgId)
|
||||
msg.error = error
|
||||
this._updateMsg(msg)
|
||||
this._setMsgStatus(msgId, 'errored')
|
||||
}
|
||||
|
||||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
|
@ -221,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
|
|||
msg.status = status
|
||||
this._updateMsg(msg)
|
||||
this.emit(`${msgId}:${status}`, msg)
|
||||
if (status === 'rejected' || status === 'signed') {
|
||||
if (status === 'rejected' || status === 'signed' || status === 'errored') {
|
||||
this.emit(`${msgId}:finished`, msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,7 +127,21 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
|||
return targetBN.mul(numBN).div(denomBN)
|
||||
}
|
||||
|
||||
function applyListeners (listeners, emitter) {
|
||||
Object.keys(listeners).forEach((key) => {
|
||||
emitter.on(key, listeners[key])
|
||||
})
|
||||
}
|
||||
|
||||
function removeListeners (listeners, emitter) {
|
||||
Object.keys(listeners).forEach((key) => {
|
||||
emitter.removeListener(key, listeners[key])
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
removeListeners,
|
||||
applyListeners,
|
||||
getPlatform,
|
||||
getStack,
|
||||
getEnvironmentType,
|
||||
|
|
|
@ -36,7 +36,6 @@ const TransactionController = require('./controllers/transactions')
|
|||
const BalancesController = require('./controllers/computed-balances')
|
||||
const TokenRatesController = require('./controllers/token-rates')
|
||||
const DetectTokensController = require('./controllers/detect-tokens')
|
||||
const ConfigManager = require('./lib/config-manager')
|
||||
const nodeify = require('./lib/nodeify')
|
||||
const accountImporter = require('./account-import-strategies')
|
||||
const getBuyEthUrl = require('./lib/buy-eth-url')
|
||||
|
@ -46,9 +45,12 @@ const BN = require('ethereumjs-util').BN
|
|||
const GWEI_BN = new BN('1000000000')
|
||||
const percentile = require('percentile')
|
||||
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
|
||||
const cleanErrorStack = require('./lib/cleanErrorStack')
|
||||
const log = require('loglevel')
|
||||
const TrezorKeyring = require('eth-trezor-keyring')
|
||||
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
|
||||
const EthQuery = require('eth-query')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const sigUtil = require('eth-sig-util')
|
||||
|
||||
module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
|
@ -66,6 +68,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
const initState = opts.initState || {}
|
||||
this.recordFirstTimeInfo(initState)
|
||||
|
||||
// this keeps track of how many "controllerStream" connections are open
|
||||
// the only thing that uses controller connections are open metamask UI instances
|
||||
this.activeControllerConnections = 0
|
||||
|
||||
// platform-specific api
|
||||
this.platform = opts.platform
|
||||
|
||||
|
@ -78,15 +84,11 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
// network store
|
||||
this.networkController = new NetworkController(initState.NetworkController)
|
||||
|
||||
// config manager
|
||||
this.configManager = new ConfigManager({
|
||||
store: this.store,
|
||||
})
|
||||
|
||||
// preferences controller
|
||||
this.preferencesController = new PreferencesController({
|
||||
initState: initState.PreferencesController,
|
||||
initLangCode: opts.initLangCode,
|
||||
showWatchAssetUi: opts.showWatchAssetUi,
|
||||
network: this.networkController,
|
||||
})
|
||||
|
||||
|
@ -107,8 +109,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
this.blacklistController.scheduleUpdates()
|
||||
|
||||
// rpc provider
|
||||
this.provider = this.initializeProvider()
|
||||
this.blockTracker = this.provider._blockTracker
|
||||
this.initializeProvider()
|
||||
this.provider = this.networkController.getProviderAndBlockTracker().provider
|
||||
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker
|
||||
|
||||
// token exchange rate tracker
|
||||
this.tokenRatesController = new TokenRatesController({
|
||||
|
@ -125,9 +128,17 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
// start and stop polling for balances based on activeControllerConnections
|
||||
this.on('controllerConnectionChanged', (activeControllerConnections) => {
|
||||
if (activeControllerConnections > 0) {
|
||||
this.accountTracker.start()
|
||||
} else {
|
||||
this.accountTracker.stop()
|
||||
}
|
||||
})
|
||||
|
||||
// key mgmt
|
||||
const additionalKeyrings = [TrezorKeyring]
|
||||
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
|
||||
this.keyringController = new KeyringController({
|
||||
keyringTypes: additionalKeyrings,
|
||||
initState: initState.KeyringController,
|
||||
|
@ -135,19 +146,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
encryptor: opts.encryptor || undefined,
|
||||
})
|
||||
|
||||
// If only one account exists, make sure it is selected.
|
||||
this.keyringController.memStore.subscribe((state) => {
|
||||
const addresses = state.keyrings.reduce((res, keyring) => {
|
||||
return res.concat(keyring.accounts)
|
||||
}, [])
|
||||
if (addresses.length === 1) {
|
||||
const address = addresses[0]
|
||||
this.preferencesController.setSelectedAddress(address)
|
||||
}
|
||||
// ensure preferences + identities controller know about all addresses
|
||||
this.preferencesController.addAddresses(addresses)
|
||||
this.accountTracker.syncWithAddresses(addresses)
|
||||
})
|
||||
this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
|
||||
|
||||
// detect tokens controller
|
||||
this.detectTokensController = new DetectTokensController({
|
||||
|
@ -174,7 +173,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
blockTracker: this.blockTracker,
|
||||
getGasPrice: this.getGasPrice.bind(this),
|
||||
})
|
||||
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
|
||||
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
|
||||
|
||||
this.txController.on(`tx:status-update`, (txId, status) => {
|
||||
if (status === 'confirmed' || status === 'failed') {
|
||||
|
@ -208,7 +207,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
this.networkController.lookupNetwork()
|
||||
this.messageManager = new MessageManager()
|
||||
this.personalMessageManager = new PersonalMessageManager()
|
||||
this.typedMessageManager = new TypedMessageManager()
|
||||
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
|
||||
this.publicConfigStore = this.initPublicConfigStore()
|
||||
|
||||
this.store.updateStructure({
|
||||
|
@ -252,30 +251,23 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
static: {
|
||||
eth_syncing: false,
|
||||
web3_clientVersion: `MetaMask/v${version}`,
|
||||
eth_sendTransaction: (payload, next, end) => {
|
||||
const origin = payload.origin
|
||||
const txParams = payload.params[0]
|
||||
nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end)
|
||||
},
|
||||
},
|
||||
// account mgmt
|
||||
getAccounts: (cb) => {
|
||||
getAccounts: async () => {
|
||||
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
|
||||
const result = []
|
||||
const selectedAddress = this.preferencesController.getSelectedAddress()
|
||||
|
||||
// only show address if account is unlocked
|
||||
if (isUnlocked && selectedAddress) {
|
||||
result.push(selectedAddress)
|
||||
return [selectedAddress]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
cb(null, result)
|
||||
},
|
||||
// tx signing
|
||||
// old style msg signing
|
||||
processMessage: this.newUnsignedMessage.bind(this),
|
||||
// personal_sign msg signing
|
||||
processTransaction: this.newUnapprovedTransaction.bind(this),
|
||||
// msg signing
|
||||
processEthSignMessage: this.newUnsignedMessage.bind(this),
|
||||
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
||||
processTypedMessage: this.newUnsignedTypedMessage.bind(this),
|
||||
}
|
||||
const providerProxy = this.networkController.initializeProvider(providerOpts)
|
||||
return providerProxy
|
||||
|
@ -317,18 +309,15 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @returns {Object} status
|
||||
*/
|
||||
getState () {
|
||||
const wallet = this.configManager.getWallet()
|
||||
const vault = this.keyringController.store.getState().vault
|
||||
const isInitialized = (!!wallet || !!vault)
|
||||
const isInitialized = !!vault
|
||||
|
||||
return {
|
||||
...{ isInitialized },
|
||||
...this.memStore.getFlatState(),
|
||||
...this.configManager.getConfig(),
|
||||
...{
|
||||
lostAccounts: this.configManager.getLostAccounts(),
|
||||
seedWords: this.configManager.getSeedWords(),
|
||||
forgottenPassword: this.configManager.getPasswordForgotten(),
|
||||
// TODO: Remove usages of lost accounts
|
||||
lostAccounts: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -377,9 +366,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
connectHardware: nodeify(this.connectHardware, this),
|
||||
forgetDevice: nodeify(this.forgetDevice, this),
|
||||
checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
|
||||
|
||||
// TREZOR
|
||||
unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
|
||||
unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this),
|
||||
|
||||
// vault management
|
||||
submitPassword: nodeify(this.submitPassword, this),
|
||||
|
@ -392,6 +379,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
|
||||
addToken: nodeify(preferencesController.addToken, preferencesController),
|
||||
removeToken: nodeify(preferencesController.removeToken, preferencesController),
|
||||
removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
|
||||
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
|
||||
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
|
||||
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
|
||||
|
@ -411,6 +399,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
updateTransaction: nodeify(txController.updateTransaction, txController),
|
||||
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
|
||||
retryTransaction: nodeify(this.retryTransaction, this),
|
||||
createCancelTransaction: nodeify(this.createCancelTransaction, this),
|
||||
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
|
||||
isNonceTaken: nodeify(txController.isNonceTaken, txController),
|
||||
estimateGas: nodeify(this.estimateGas, this),
|
||||
|
@ -481,12 +470,32 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
async createNewVaultAndRestore (password, seed) {
|
||||
const releaseLock = await this.createVaultMutex.acquire()
|
||||
try {
|
||||
let accounts, lastBalance
|
||||
|
||||
const keyringController = this.keyringController
|
||||
|
||||
// clear known identities
|
||||
this.preferencesController.setAddresses([])
|
||||
// create new vault
|
||||
const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
|
||||
const vault = await keyringController.createNewVaultAndRestore(password, seed)
|
||||
|
||||
const ethQuery = new EthQuery(this.provider)
|
||||
accounts = await keyringController.getAccounts()
|
||||
lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery)
|
||||
|
||||
const primaryKeyring = keyringController.getKeyringsByType('HD Key Tree')[0]
|
||||
if (!primaryKeyring) {
|
||||
throw new Error('MetamaskController - No HD Key Tree found')
|
||||
}
|
||||
|
||||
// seek out the first zero balance
|
||||
while (lastBalance !== '0x0') {
|
||||
await keyringController.addNewAccount(primaryKeyring)
|
||||
accounts = await keyringController.getAccounts()
|
||||
lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery)
|
||||
}
|
||||
|
||||
// set new identities
|
||||
const accounts = await this.keyringController.getAccounts()
|
||||
this.preferencesController.setAddresses(accounts)
|
||||
this.selectFirstIdentity()
|
||||
releaseLock()
|
||||
|
@ -497,6 +506,30 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an account balance from the AccountTracker or request it directly from the network.
|
||||
* @param {string} address - The account address
|
||||
* @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network
|
||||
*/
|
||||
getBalance (address, ethQuery) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cached = this.accountTracker.store.getState().accounts[address]
|
||||
|
||||
if (cached && cached.balance) {
|
||||
resolve(cached.balance)
|
||||
} else {
|
||||
ethQuery.getBalance(address, (error, balance) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
log.error(error)
|
||||
} else {
|
||||
resolve(balance || '0x0')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Submits the user's password and attempts to unlock the vault.
|
||||
* Also synchronizes the preferencesController, to ensure its schema
|
||||
|
@ -540,25 +573,40 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
// Hardware
|
||||
//
|
||||
|
||||
async getKeyringForDevice (deviceName, hdPath = null) {
|
||||
let keyringName = null
|
||||
switch (deviceName) {
|
||||
case 'trezor':
|
||||
keyringName = TrezorKeyring.type
|
||||
break
|
||||
case 'ledger':
|
||||
keyringName = LedgerBridgeKeyring.type
|
||||
break
|
||||
default:
|
||||
throw new Error('MetamaskController:getKeyringForDevice - Unknown device')
|
||||
}
|
||||
let keyring = await this.keyringController.getKeyringsByType(keyringName)[0]
|
||||
if (!keyring) {
|
||||
keyring = await this.keyringController.addNewKeyring(keyringName)
|
||||
}
|
||||
if (hdPath && keyring.setHdPath) {
|
||||
keyring.setHdPath(hdPath)
|
||||
}
|
||||
|
||||
keyring.network = this.networkController.getProviderConfig().type
|
||||
|
||||
return keyring
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch account list from a trezor device.
|
||||
*
|
||||
* @returns [] accounts
|
||||
*/
|
||||
async connectHardware (deviceName, page) {
|
||||
|
||||
switch (deviceName) {
|
||||
case 'trezor':
|
||||
const keyringController = this.keyringController
|
||||
const oldAccounts = await keyringController.getAccounts()
|
||||
let keyring = await keyringController.getKeyringsByType(
|
||||
'Trezor Hardware'
|
||||
)[0]
|
||||
if (!keyring) {
|
||||
keyring = await this.keyringController.addNewKeyring('Trezor Hardware')
|
||||
}
|
||||
async connectHardware (deviceName, page, hdPath) {
|
||||
const keyring = await this.getKeyringForDevice(deviceName, hdPath)
|
||||
let accounts = []
|
||||
|
||||
switch (page) {
|
||||
case -1:
|
||||
accounts = await keyring.getPreviousPage()
|
||||
|
@ -572,13 +620,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
|
||||
// Merge with existing accounts
|
||||
// and make sure addresses are not repeated
|
||||
const oldAccounts = await this.keyringController.getAccounts()
|
||||
const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
|
||||
this.accountTracker.syncWithAddresses(accountsToTrack)
|
||||
return accounts
|
||||
|
||||
default:
|
||||
throw new Error('MetamaskController:connectHardware - Unknown device')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -586,21 +631,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async checkHardwareStatus (deviceName) {
|
||||
|
||||
switch (deviceName) {
|
||||
case 'trezor':
|
||||
const keyringController = this.keyringController
|
||||
const keyring = await keyringController.getKeyringsByType(
|
||||
'Trezor Hardware'
|
||||
)[0]
|
||||
if (!keyring) {
|
||||
return false
|
||||
}
|
||||
async checkHardwareStatus (deviceName, hdPath) {
|
||||
const keyring = await this.getKeyringForDevice(deviceName, hdPath)
|
||||
return keyring.isUnlocked()
|
||||
default:
|
||||
throw new Error('MetamaskController:checkHardwareStatus - Unknown device')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -610,20 +643,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
*/
|
||||
async forgetDevice (deviceName) {
|
||||
|
||||
switch (deviceName) {
|
||||
case 'trezor':
|
||||
const keyringController = this.keyringController
|
||||
const keyring = await keyringController.getKeyringsByType(
|
||||
'Trezor Hardware'
|
||||
)[0]
|
||||
if (!keyring) {
|
||||
throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found')
|
||||
}
|
||||
const keyring = await this.getKeyringForDevice(deviceName)
|
||||
keyring.forgetDevice()
|
||||
return true
|
||||
default:
|
||||
throw new Error('MetamaskController:forgetDevice - Unknown device')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -631,23 +653,19 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
*
|
||||
* @returns {} keyState
|
||||
*/
|
||||
async unlockTrezorAccount (index) {
|
||||
const keyringController = this.keyringController
|
||||
const keyring = await keyringController.getKeyringsByType(
|
||||
'Trezor Hardware'
|
||||
)[0]
|
||||
if (!keyring) {
|
||||
throw new Error('MetamaskController - No Trezor Hardware Keyring found')
|
||||
}
|
||||
async unlockHardwareWalletAccount (index, deviceName, hdPath) {
|
||||
const keyring = await this.getKeyringForDevice(deviceName, hdPath)
|
||||
|
||||
keyring.setAccountToUnlock(index)
|
||||
const oldAccounts = await keyringController.getAccounts()
|
||||
const keyState = await keyringController.addNewAccount(keyring)
|
||||
const newAccounts = await keyringController.getAccounts()
|
||||
const oldAccounts = await this.keyringController.getAccounts()
|
||||
const keyState = await this.keyringController.addNewAccount(keyring)
|
||||
const newAccounts = await this.keyringController.getAccounts()
|
||||
this.preferencesController.setAddresses(newAccounts)
|
||||
newAccounts.forEach(address => {
|
||||
if (!oldAccounts.includes(address)) {
|
||||
this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`)
|
||||
// Set the account label to Trezor 1 / Ledger 1, etc
|
||||
this.preferencesController.setAccountLabel(address, `${deviceName[0].toUpperCase()}${deviceName.slice(1)} ${parseInt(index, 10) + 1}`)
|
||||
// Select the account
|
||||
this.preferencesController.setSelectedAddress(address)
|
||||
}
|
||||
})
|
||||
|
@ -701,7 +719,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
|
||||
this.verifySeedPhrase()
|
||||
.then((seedWords) => {
|
||||
this.configManager.setSeedWords(seedWords)
|
||||
this.preferencesController.setSeedWords(seedWords)
|
||||
return cb(null, seedWords)
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -750,7 +768,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {function} cb Callback function called with the current address.
|
||||
*/
|
||||
clearSeedWordCache (cb) {
|
||||
this.configManager.setSeedWords(null)
|
||||
this.preferencesController.setSeedWords(null)
|
||||
cb(null, this.preferencesController.getSelectedAddress())
|
||||
}
|
||||
|
||||
|
@ -779,7 +797,8 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
// Remove account from the preferences controller
|
||||
this.preferencesController.removeAddress(address)
|
||||
// Remove account from the account tracker controller
|
||||
this.accountTracker.removeAccount(address)
|
||||
this.accountTracker.removeAccount([address])
|
||||
|
||||
// Remove account from the keyring
|
||||
await this.keyringController.removeAccount(address)
|
||||
return address
|
||||
|
@ -809,6 +828,18 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
// ---------------------------------------------------------------------------
|
||||
// Identity Management (signature operations)
|
||||
|
||||
/**
|
||||
* Called when a Dapp suggests a new tx to be signed.
|
||||
* this wrapper needs to exist so we can provide a reference to
|
||||
* "newUnapprovedTransaction" before "txController" is instantiated
|
||||
*
|
||||
* @param {Object} msgParams - The params passed to eth_sign.
|
||||
* @param {Object} req - (optional) the original request, containing the origin
|
||||
*/
|
||||
async newUnapprovedTransaction (txParams, req) {
|
||||
return await this.txController.newUnapprovedTransaction(txParams, req)
|
||||
}
|
||||
|
||||
// eth_sign methods:
|
||||
|
||||
/**
|
||||
|
@ -820,20 +851,11 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Object} msgParams - The params passed to eth_sign.
|
||||
* @param {Function} cb = The callback function called with the signature.
|
||||
*/
|
||||
newUnsignedMessage (msgParams, cb) {
|
||||
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
|
||||
newUnsignedMessage (msgParams, req) {
|
||||
const promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
this.messageManager.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return cb(null, data.rawSig)
|
||||
case 'rejected':
|
||||
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
|
||||
default:
|
||||
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
|
||||
}
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -887,24 +909,11 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Function} cb - The callback function called with the signature.
|
||||
* Passed back to the requesting Dapp.
|
||||
*/
|
||||
newUnsignedPersonalMessage (msgParams, cb) {
|
||||
if (!msgParams.from) {
|
||||
return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.')))
|
||||
}
|
||||
|
||||
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
|
||||
async newUnsignedPersonalMessage (msgParams, req) {
|
||||
const promise = this.personalMessageManager.addUnapprovedMessageAsync(msgParams, req)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
this.personalMessageManager.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return cb(null, data.rawSig)
|
||||
case 'rejected':
|
||||
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
|
||||
default:
|
||||
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
|
||||
}
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -953,26 +962,11 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Object} msgParams - The params passed to eth_signTypedData.
|
||||
* @param {Function} cb - The callback function, called with the signature.
|
||||
*/
|
||||
newUnsignedTypedMessage (msgParams, cb) {
|
||||
let msgId
|
||||
try {
|
||||
msgId = this.typedMessageManager.addUnapprovedMessage(msgParams)
|
||||
newUnsignedTypedMessage (msgParams, req) {
|
||||
const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
} catch (e) {
|
||||
return cb(e)
|
||||
}
|
||||
|
||||
this.typedMessageManager.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
case 'signed':
|
||||
return cb(null, data.rawSig)
|
||||
case 'rejected':
|
||||
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
|
||||
default:
|
||||
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
|
||||
}
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -982,22 +976,31 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Object} msgParams - The params passed to eth_signTypedData.
|
||||
* @returns {Object} Full state update.
|
||||
*/
|
||||
signTypedMessage (msgParams) {
|
||||
log.info('MetaMaskController - signTypedMessage')
|
||||
async signTypedMessage (msgParams) {
|
||||
log.info('MetaMaskController - eth_signTypedData')
|
||||
const msgId = msgParams.metamaskId
|
||||
// sets the status op the message to 'approved'
|
||||
// and removes the metamaskId for signing
|
||||
return this.typedMessageManager.approveMessage(msgParams)
|
||||
.then((cleanMsgParams) => {
|
||||
// signs the message
|
||||
return this.keyringController.signTypedMessage(cleanMsgParams)
|
||||
})
|
||||
.then((rawSig) => {
|
||||
// tells the listener that the message has been signed
|
||||
// and can be returned to the dapp
|
||||
this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
|
||||
const version = msgParams.version
|
||||
try {
|
||||
const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams)
|
||||
const address = sigUtil.normalize(cleanMsgParams.from)
|
||||
const keyring = await this.keyringController.getKeyringForAccount(address)
|
||||
const wallet = keyring._getWalletForAccount(address)
|
||||
const privKey = ethUtil.toBuffer(wallet.getPrivateKey())
|
||||
let signature
|
||||
switch (version) {
|
||||
case 'V1':
|
||||
signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data })
|
||||
break
|
||||
case 'V3':
|
||||
signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) })
|
||||
break
|
||||
}
|
||||
this.typedMessageManager.setMsgStatusSigned(msgId, signature)
|
||||
return this.getState()
|
||||
})
|
||||
} catch (error) {
|
||||
log.info('MetaMaskController - eth_signTypedData failed.', error)
|
||||
this.typedMessageManager.errorMessage(msgId, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1035,35 +1038,16 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
/**
|
||||
* A legacy method used to record user confirmation that they understand
|
||||
* that some of their accounts have been recovered but should be backed up.
|
||||
* This function no longer does anything and will be removed.
|
||||
*
|
||||
* @deprecated
|
||||
* @param {Function} cb - A callback function called with a full state update.
|
||||
*/
|
||||
markAccountsFound (cb) {
|
||||
this.configManager.setLostAccounts([])
|
||||
this.sendUpdate()
|
||||
// TODO Remove me
|
||||
cb(null, this.getState())
|
||||
}
|
||||
|
||||
/**
|
||||
* A legacy method (probably dead code) that was used when we swapped out our
|
||||
* key management library that we depended on.
|
||||
*
|
||||
* Described in:
|
||||
* https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
|
||||
*
|
||||
* @deprecated
|
||||
* @param {} migratorOutput
|
||||
*/
|
||||
restoreOldLostAccounts (migratorOutput) {
|
||||
const { lostAccounts } = migratorOutput
|
||||
if (lostAccounts) {
|
||||
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
|
||||
return this.importLostAccounts(migratorOutput)
|
||||
}
|
||||
return Promise.resolve(migratorOutput)
|
||||
}
|
||||
|
||||
/**
|
||||
* An account object
|
||||
* @typedef Account
|
||||
|
@ -1108,6 +1092,19 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a user to attempt to cancel a previously submitted transaction by creating a new
|
||||
* transaction.
|
||||
* @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
|
||||
* @param {string=} customGasPrice - the hex value to use for the cancel transaction
|
||||
* @returns {object} MetaMask state
|
||||
*/
|
||||
async createCancelTransaction (originalTxId, customGasPrice, cb) {
|
||||
await this.txController.createCancelTransaction(originalTxId, customGasPrice)
|
||||
const state = await this.getState()
|
||||
return state
|
||||
}
|
||||
|
||||
estimateGas (estimateGasParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
|
||||
|
@ -1129,7 +1126,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Function} cb - A callback function called when complete.
|
||||
*/
|
||||
markPasswordForgotten (cb) {
|
||||
this.configManager.setPasswordForgotten(true)
|
||||
this.preferencesController.setPasswordForgotten(true)
|
||||
this.sendUpdate()
|
||||
cb()
|
||||
}
|
||||
|
@ -1139,7 +1136,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Function} cb - A callback function called when complete.
|
||||
*/
|
||||
unMarkPasswordForgotten (cb) {
|
||||
this.configManager.setPasswordForgotten(false)
|
||||
this.preferencesController.setPasswordForgotten(false)
|
||||
this.sendUpdate()
|
||||
cb()
|
||||
}
|
||||
|
@ -1210,18 +1207,28 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
setupControllerConnection (outStream) {
|
||||
const api = this.getApi()
|
||||
const dnode = Dnode(api)
|
||||
// report new active controller connection
|
||||
this.activeControllerConnections++
|
||||
this.emit('controllerConnectionChanged', this.activeControllerConnections)
|
||||
// connect dnode api to remote connection
|
||||
pump(
|
||||
outStream,
|
||||
dnode,
|
||||
outStream,
|
||||
(err) => {
|
||||
// report new active controller connection
|
||||
this.activeControllerConnections--
|
||||
this.emit('controllerConnectionChanged', this.activeControllerConnections)
|
||||
// report any error
|
||||
if (err) log.error(err)
|
||||
}
|
||||
)
|
||||
dnode.on('remote', (remote) => {
|
||||
// push updates to popup
|
||||
const sendUpdate = remote.sendUpdate.bind(remote)
|
||||
const sendUpdate = (update) => remote.sendUpdate(update)
|
||||
this.on('update', sendUpdate)
|
||||
// remove update listener once the connection ends
|
||||
dnode.on('end', () => this.removeListener('update', sendUpdate))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1237,12 +1244,16 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
// create filter polyfill middleware
|
||||
const filterMiddleware = createFilterMiddleware({
|
||||
provider: this.provider,
|
||||
blockTracker: this.provider._blockTracker,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
|
||||
engine.push(createOriginMiddleware({ origin }))
|
||||
engine.push(createLoggerMiddleware({ origin }))
|
||||
engine.push(filterMiddleware)
|
||||
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
|
||||
engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
|
||||
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
|
||||
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3').bind(this))
|
||||
engine.push(createProviderMiddleware({ provider: this.provider }))
|
||||
|
||||
// setup connection
|
||||
|
@ -1270,15 +1281,45 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {*} outStream - The stream to provide public config over.
|
||||
*/
|
||||
setupPublicConfig (outStream) {
|
||||
const configStream = asStream(this.publicConfigStore)
|
||||
pump(
|
||||
asStream(this.publicConfigStore),
|
||||
configStream,
|
||||
outStream,
|
||||
(err) => {
|
||||
configStream.destroy()
|
||||
if (err) log.error(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a KeyringController update
|
||||
* @param {object} state the KC state
|
||||
* @return {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _onKeyringControllerUpdate (state) {
|
||||
const {isUnlocked, keyrings} = state
|
||||
const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
|
||||
|
||||
if (!addresses.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure preferences + identities controller know about all addresses
|
||||
this.preferencesController.addAddresses(addresses)
|
||||
this.accountTracker.syncWithAddresses(addresses)
|
||||
|
||||
const wasLocked = !isUnlocked
|
||||
if (wasLocked) {
|
||||
const oldSelectedAddress = this.preferencesController.getSelectedAddress()
|
||||
if (!addresses.includes(oldSelectedAddress)) {
|
||||
const address = addresses[0]
|
||||
await this.preferencesController.setSelectedAddress(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method for emitting the full MetaMask state to all registered listeners.
|
||||
* @private
|
||||
|
@ -1425,6 +1466,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
|
||||
/**
|
||||
* A method for recording whether the MetaMask user interface is open or not.
|
||||
* @private
|
||||
|
@ -1445,4 +1487,34 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
set isClientOpenAndUnlocked (active) {
|
||||
this.tokenRatesController.isActive = active
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates RPC engine middleware for processing eth_signTypedData requests
|
||||
*
|
||||
* @param {Object} req - request object
|
||||
* @param {Object} res - response object
|
||||
* @param {Function} - next
|
||||
* @param {Function} - end
|
||||
*/
|
||||
createTypedDataMiddleware (methodName, version) {
|
||||
return async (req, res, next, end) => {
|
||||
const { method, params } = req
|
||||
if (method === methodName) {
|
||||
const promise = this.typedMessageManager.addUnapprovedMessageAsync({
|
||||
data: params.length >= 1 && params[0],
|
||||
from: params.length >= 2 && params[1],
|
||||
}, req, version)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
try {
|
||||
res.result = await promise
|
||||
end()
|
||||
} catch (error) {
|
||||
end(error)
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
const version = 5
|
||||
|
||||
/*
|
||||
|
||||
This is an incomplete migration bc it requires post-decrypted data
|
||||
which we dont have access to at the time of this writing.
|
||||
|
||||
*/
|
||||
|
||||
const ObservableStore = require('obs-store')
|
||||
const ConfigManager = require('../../app/scripts/lib/config-manager')
|
||||
const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
|
||||
const KeyringController = require('eth-keyring-controller')
|
||||
|
||||
const password = 'obviously not correct'
|
||||
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (versionedData) {
|
||||
versionedData.meta.version = version
|
||||
|
||||
const store = new ObservableStore(versionedData.data)
|
||||
const configManager = new ConfigManager({ store })
|
||||
const idStoreMigrator = new IdentityStoreMigrator({ configManager })
|
||||
const keyringController = new KeyringController({
|
||||
configManager: configManager,
|
||||
})
|
||||
|
||||
// attempt to migrate to multiVault
|
||||
return idStoreMigrator.migratedVaultForPassword(password)
|
||||
.then((result) => {
|
||||
// skip if nothing to migrate
|
||||
if (!result) return Promise.resolve(versionedData)
|
||||
delete versionedData.data.wallet
|
||||
// create new keyrings
|
||||
const privKeys = result.lostAccounts.map(acct => acct.privateKey)
|
||||
return Promise.all([
|
||||
keyringController.restoreKeyring(result.serialized),
|
||||
keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }),
|
||||
]).then(() => {
|
||||
return keyringController.persistAllKeyrings(password)
|
||||
}).then(() => {
|
||||
// copy result on to state object
|
||||
versionedData.data = store.get()
|
||||
return Promise.resolve(versionedData)
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
|
@ -7,7 +7,7 @@ const uniqBy = require('lodash.uniqby')
|
|||
|
||||
module.exports = class NoticeController extends EventEmitter {
|
||||
|
||||
constructor (opts) {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.noticePoller = null
|
||||
this.firstVersion = opts.firstVersion
|
||||
|
|
|
@ -2,7 +2,7 @@ const injectCss = require('inject-css')
|
|||
const OldMetaMaskUiCss = require('../../old-ui/css')
|
||||
const NewMetaMaskUiCss = require('../../ui/css')
|
||||
const startPopup = require('./popup-core')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const PortStream = require('extension-port-stream')
|
||||
const { getEnvironmentType } = require('./lib/util')
|
||||
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums')
|
||||
const extension = require('extensionizer')
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
},
|
||||
"sidebar": {},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null,
|
||||
|
|
|
@ -141,6 +141,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"modal": {
|
||||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
|
|
|
@ -162,6 +162,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"modal": {
|
||||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"modal": {
|
||||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null,
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"name": "Send Account 4"
|
||||
}
|
||||
},
|
||||
"assetImages": {},
|
||||
"unapprovedTxs": {},
|
||||
"currentCurrency": "USD",
|
||||
"conversionRate": 1200.88200327,
|
||||
|
@ -141,6 +142,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"modal": {
|
||||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"name": "Address Book Account 1"
|
||||
}
|
||||
],
|
||||
"assetImages": {},
|
||||
"tokens": [],
|
||||
"transactions": {},
|
||||
"selectedAddressTxList": [],
|
||||
|
@ -120,6 +121,7 @@
|
|||
"accountDetail": {
|
||||
"subview": "transactions"
|
||||
},
|
||||
"sidebar": {},
|
||||
"modal": {
|
||||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"name": "Account 4"
|
||||
}
|
||||
},
|
||||
"assetImages": {},
|
||||
"unapprovedTxs": {},
|
||||
"currentCurrency": "USD",
|
||||
"conversionRate": 16.88200327,
|
||||
|
@ -99,6 +100,7 @@
|
|||
"accountExport": "none",
|
||||
"privateKey": ""
|
||||
},
|
||||
"sidebar": {},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null,
|
||||
|
|
|
@ -118,6 +118,7 @@
|
|||
"modalState": {},
|
||||
"previousModalState": {}
|
||||
},
|
||||
"sidebar": {},
|
||||
"transForward": true,
|
||||
"isLoading": false,
|
||||
"warning": null,
|
||||
|
|
|
@ -5,7 +5,6 @@ To add another network to our dropdown menu, make sure the following files are a
|
|||
```
|
||||
app/scripts/config.js
|
||||
app/scripts/lib/buy-eth-url.js
|
||||
app/scripts/lib/config-manager.js
|
||||
ui/app/app.js
|
||||
ui/app/components/buy-button-subview.js
|
||||
ui/app/components/drop-menu-item.js
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
### Developing on Dependencies
|
||||
|
||||
To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies:
|
||||
To enjoy the live-reloading that `gulp dev` offers while working on the dependencies:
|
||||
|
||||
1. Clone the dependency locally.
|
||||
2. `npm install` in its folder.
|
||||
3. Run `npm link` in its folder.
|
||||
4. Run `npm link $DEP_NAME` in this project folder.
|
||||
5. Next time you `npm start` it will watch the dependency for changes as well!
|
||||
|
||||
|
|
|
@ -89,8 +89,6 @@ MetaMask has two kinds of [duplex stream APIs](https://github.com/substack/strea
|
|||
|
||||
If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background).
|
||||
|
||||
To make this as easy as possible, we use one of our favorite internal tools, [web3-provider-engine](https://www.npmjs.com/package/web3-provider-engine) to construct a custom web3 provider object whose source of truth is a stream that we connect to remotely.
|
||||
|
||||
To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available.
|
||||
|
||||
In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic!
|
||||
|
|
|
@ -63,7 +63,9 @@ class CreatePasswordScreen extends Component {
|
|||
return password === confirmPassword
|
||||
}
|
||||
|
||||
createAccount = () => {
|
||||
createAccount = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
}
|
||||
|
@ -127,7 +129,7 @@ class CreatePasswordScreen extends Component {
|
|||
It allows you to hold ether & tokens, and interact with decentralized applications.
|
||||
</div>
|
||||
</div>}
|
||||
<div className="create-password">
|
||||
<form className="create-password">
|
||||
<div className="create-password__title">
|
||||
Create Password
|
||||
</div>
|
||||
|
@ -188,7 +190,7 @@ class CreatePasswordScreen extends Component {
|
|||
</a>
|
||||
{ */ }
|
||||
<Breadcrumbs total={3} currentIndex={0} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -32,6 +32,7 @@ function mapStateToProps (state) {
|
|||
currentCurrency: state.metamask.currentCurrency,
|
||||
currentAccountTab: state.metamask.currentAccountTab,
|
||||
tokens: state.metamask.tokens,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
computedBalances: state.metamask.computedBalances,
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () {
|
|||
var account = props.accounts[selected]
|
||||
const { network, conversionRate, currentCurrency } = props
|
||||
|
||||
if (Object.keys(props.suggestedTokens).length > 0) {
|
||||
this.props.dispatch(actions.showAddSuggestedTokenPage())
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
h('.account-detail-section.full-flex-height', [
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../ui/app/actions')
|
||||
const Tooltip = require('./components/tooltip.js')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const Copyable = require('./components/copyable')
|
||||
const addressSummary = require('./util').addressSummary
|
||||
|
||||
|
||||
module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
identities: state.metamask.identities,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(AddSuggestedTokenScreen, Component)
|
||||
function AddSuggestedTokenScreen () {
|
||||
this.state = {
|
||||
warning: null,
|
||||
}
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.render = function () {
|
||||
const state = this.state
|
||||
const props = this.props
|
||||
const { warning } = state
|
||||
const key = Object.keys(props.suggestedTokens)[0]
|
||||
const { address, symbol, decimals } = props.suggestedTokens[key]
|
||||
|
||||
return (
|
||||
h('.flex-column.flex-grow', [
|
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [
|
||||
h('h2.page-subtitle', 'Add Suggested Token'),
|
||||
]),
|
||||
|
||||
h('.error', {
|
||||
style: {
|
||||
display: warning ? 'block' : 'none',
|
||||
padding: '0 20px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}, warning),
|
||||
|
||||
// conf view
|
||||
h('.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||
h('.flex-space-around', {
|
||||
style: {
|
||||
padding: '20px',
|
||||
},
|
||||
}, [
|
||||
|
||||
h('div', [
|
||||
h(Tooltip, {
|
||||
position: 'top',
|
||||
title: 'The contract of the actual token contract. Click for more info.',
|
||||
}, [
|
||||
h('a', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
|
||||
target: '_blank',
|
||||
}, [
|
||||
h('span', 'Token Contract Address '),
|
||||
h('i.fa.fa-question-circle'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div', {
|
||||
style: { display: 'flex' },
|
||||
}, [
|
||||
h(Copyable, {
|
||||
value: ethUtil.toChecksumAddress(address),
|
||||
}, [
|
||||
h('span#token-address', {
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
display: 'flex',
|
||||
},
|
||||
}, addressSummary(address, 24, 4, false)),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div', [
|
||||
h('span', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
}, 'Token Symbol'),
|
||||
]),
|
||||
|
||||
h('div', { style: {display: 'flex'} }, [
|
||||
h('p#token_symbol', {
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
},
|
||||
}, symbol),
|
||||
]),
|
||||
|
||||
h('div', [
|
||||
h('span', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
}, 'Decimals of Precision'),
|
||||
]),
|
||||
|
||||
h('div', { style: {display: 'flex'} }, [
|
||||
h('p#token_decimals', {
|
||||
type: 'number',
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
},
|
||||
}, decimals),
|
||||
]),
|
||||
|
||||
h('button', {
|
||||
style: {
|
||||
alignSelf: 'center',
|
||||
margin: '8px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
this.props.dispatch(actions.removeSuggestedTokens())
|
||||
},
|
||||
}, 'Cancel'),
|
||||
|
||||
h('button', {
|
||||
style: {
|
||||
alignSelf: 'center',
|
||||
margin: '8px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
const valid = this.validateInputs({ address, symbol, decimals })
|
||||
if (!valid) return
|
||||
|
||||
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
|
||||
.then(() => {
|
||||
this.props.dispatch(actions.removeSuggestedTokens())
|
||||
})
|
||||
},
|
||||
}, 'Add'),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
|
||||
if (typeof global.ethereumProvider === 'undefined') return
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.validateInputs = function (opts) {
|
||||
let msg = ''
|
||||
const identitiesList = Object.keys(this.props.identities)
|
||||
const { address, symbol, decimals } = opts
|
||||
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
|
||||
|
||||
const validAddress = ethUtil.isValidAddress(address)
|
||||
if (!validAddress) {
|
||||
msg += 'Address is invalid.'
|
||||
}
|
||||
|
||||
const validDecimals = decimals >= 0 && decimals <= 36
|
||||
if (!validDecimals) {
|
||||
msg += 'Decimals must be at least 0, and not over 36. '
|
||||
}
|
||||
|
||||
const symbolLen = symbol.trim().length
|
||||
const validSymbol = symbolLen > 0 && symbolLen < 10
|
||||
if (!validSymbol) {
|
||||
msg += 'Symbol must be between 0 and 10 characters.'
|
||||
}
|
||||
|
||||
const ownAddress = identitiesList.includes(standardAddress)
|
||||
if (ownAddress) {
|
||||
msg = 'Personal address detected. Input the token contract address.'
|
||||
}
|
||||
|
||||
const isValid = validAddress && validDecimals && !ownAddress
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({
|
||||
warning: msg,
|
||||
})
|
||||
} else {
|
||||
this.setState({ warning: null })
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
|
@ -196,7 +196,7 @@ AddTokenScreen.prototype.validateInputs = function () {
|
|||
msg += 'Address is invalid.'
|
||||
}
|
||||
|
||||
const validDecimals = decimals >= 0 && decimals < 36
|
||||
const validDecimals = decimals >= 0 && decimals <= 36
|
||||
if (!validDecimals) {
|
||||
msg += 'Decimals must be at least 0, and not over 36. '
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
|
|||
// other views
|
||||
const ConfigScreen = require('./config')
|
||||
const AddTokenScreen = require('./add-token')
|
||||
const AddSuggestedTokenScreen = require('./add-suggested-token')
|
||||
const Import = require('./accounts/import')
|
||||
const InfoScreen = require('./info')
|
||||
const NewUiAnnouncement = require('./new-ui-annoucement')
|
||||
|
@ -74,6 +75,7 @@ function mapStateToProps (state) {
|
|||
lostAccounts: state.metamask.lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
featureFlags,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
|
||||
// state needed to get account dropdown temporarily rendering from app bar
|
||||
identities,
|
||||
|
@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () {
|
|||
log.debug('rendering add-token screen from unlock screen.')
|
||||
return h(AddTokenScreen, {key: 'add-token'})
|
||||
|
||||
case 'add-suggested-token':
|
||||
log.debug('rendering add-suggested-token screen from unlock screen.')
|
||||
return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'})
|
||||
|
||||
case 'config':
|
||||
log.debug('rendering config screen')
|
||||
return h(ConfigScreen, {key: 'config'})
|
||||
|
|
|
@ -350,11 +350,14 @@ module.exports = class AppBar extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderCommonRpc (rpcList, {rpcTarget}) {
|
||||
renderCommonRpc (rpcList, provider) {
|
||||
const {dispatch} = this.props
|
||||
const reversedRpcList = rpcList.slice().reverse()
|
||||
|
||||
return rpcList.map((rpc) => {
|
||||
if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) {
|
||||
return reversedRpcList.map((rpc) => {
|
||||
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
|
||||
|
||||
if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
|
||||
return null
|
||||
} else {
|
||||
return h(DropdownMenuItem, {
|
||||
|
@ -364,7 +367,7 @@ module.exports = class AppBar extends Component {
|
|||
}, [
|
||||
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
|
||||
rpc,
|
||||
rpcTarget === rpc
|
||||
currentRpcTarget
|
||||
? h('.check', '✓')
|
||||
: null,
|
||||
])
|
||||
|
|
|
@ -116,12 +116,25 @@ Notice.prototype.render = function () {
|
|||
)
|
||||
}
|
||||
|
||||
Notice.prototype.setInitialDisclaimerState = function () {
|
||||
if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
|
||||
this.setState({disclaimerDisabled: false})
|
||||
}
|
||||
}
|
||||
|
||||
Notice.prototype.componentDidMount = function () {
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
var node = findDOMNode(this)
|
||||
linker.setupListener(node)
|
||||
if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
|
||||
this.setState({disclaimerDisabled: false})
|
||||
this.setInitialDisclaimerState()
|
||||
}
|
||||
|
||||
Notice.prototype.componentDidUpdate = function (prevProps) {
|
||||
const { notice: { id } = {} } = this.props
|
||||
const { notice: { id: prevNoticeId } = {} } = prevProps
|
||||
|
||||
if (id !== prevNoticeId) {
|
||||
this.setInitialDisclaimerState()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ PendingMsgDetails.prototype.render = function () {
|
|||
var identity = state.identities[address] || { address: address }
|
||||
var account = state.accounts[address] || { address: address }
|
||||
|
||||
var { data } = msgParams
|
||||
var { data, version } = msgParams
|
||||
|
||||
return (
|
||||
h('div', {
|
||||
|
@ -48,6 +48,7 @@ PendingMsgDetails.prototype.render = function () {
|
|||
h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'),
|
||||
h(TypedMessageRenderer, {
|
||||
value: data,
|
||||
version,
|
||||
style: {
|
||||
height: '215px',
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ const Component = require('react').Component
|
|||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const extend = require('xtend')
|
||||
const { ObjectInspector } = require('react-inspector')
|
||||
|
||||
module.exports = TypedMessageRenderer
|
||||
|
||||
|
@ -12,8 +13,16 @@ function TypedMessageRenderer () {
|
|||
|
||||
TypedMessageRenderer.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { value, style } = props
|
||||
const text = renderTypedData(value)
|
||||
const { value, version, style } = props
|
||||
let text
|
||||
switch (version) {
|
||||
case 'V1':
|
||||
text = renderTypedData(value)
|
||||
break
|
||||
case 'V3':
|
||||
text = renderTypedDataV3(value)
|
||||
break
|
||||
}
|
||||
|
||||
const defaultStyle = extend({
|
||||
width: '315px',
|
||||
|
@ -44,3 +53,17 @@ function renderTypedData (values) {
|
|||
])
|
||||
})
|
||||
}
|
||||
|
||||
function renderTypedDataV3 (values) {
|
||||
const { domain, message } = JSON.parse(values)
|
||||
return [
|
||||
domain ? h('div', [
|
||||
h('h1', 'Domain'),
|
||||
h(ObjectInspector, { data: domain, expandLevel: 1, name: 'domain' }),
|
||||
]) : '',
|
||||
message ? h('div', [
|
||||
h('h1', 'Message'),
|
||||
h(ObjectInspector, { data: message, expandLevel: 1, name: 'message' }),
|
||||
]) : '',
|
||||
]
|
||||
}
|
||||
|
|
|
@ -138,7 +138,6 @@ InfoScreen.prototype.render = function () {
|
|||
h('div.fa.fa-envelope', [
|
||||
h('a.info', {
|
||||
target: '_blank',
|
||||
style: { width: '85vw' },
|
||||
href: 'mailto:help@metamask.io?subject=Feedback',
|
||||
}, 'Email us!'),
|
||||
]),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
|
@ -8,6 +8,7 @@
|
|||
"mascara": "gulp dev:mascara & node ./mascara/example/server",
|
||||
"dist": "gulp dist",
|
||||
"doc": "jsdoc -c development/tools/.jsdoc.json",
|
||||
"publish-docs": "gh-pages -d docs/jsdocs",
|
||||
"test": "npm run test:unit && npm run test:integration && npm run lint",
|
||||
"watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui",
|
||||
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
|
||||
|
@ -22,7 +23,7 @@
|
|||
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/metamask.spec --bail --recursive",
|
||||
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
|
||||
"test:screens:run": "node test/screens/new-ui.js",
|
||||
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
|
||||
"test:coverage": "nyc --reporter=text --reporter=html npm run test:unit && npm run test:coveralls-upload",
|
||||
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
||||
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
|
||||
"test:flat:build": "npm run test:flat:build:ui && npm run test:flat:build:tests && npm run test:flat:build:locales",
|
||||
|
@ -36,7 +37,7 @@
|
|||
"test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales",
|
||||
"test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js",
|
||||
"test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js",
|
||||
"ganache:start": "ganache-cli -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'",
|
||||
"ganache:start": "ganache-cli --noVMErrorsOnRPCResponse -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'",
|
||||
"sentry:publish": "node ./development/sentry-publish.js",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
|
@ -57,7 +58,11 @@
|
|||
[
|
||||
"env",
|
||||
{
|
||||
"debug": true
|
||||
"browsers": [
|
||||
">0.25%",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
],
|
||||
"stage-0"
|
||||
|
@ -105,15 +110,19 @@
|
|||
"ensnare": "^1.0.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"eth-bin-to-ops": "^1.0.1",
|
||||
"eth-block-tracker": "^4.0.1",
|
||||
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
|
||||
"eth-ens-namehash": "^2.0.8",
|
||||
"eth-hd-keyring": "^1.2.2",
|
||||
"eth-json-rpc-filters": "^1.2.6",
|
||||
"eth-json-rpc-filters": "^2.1.1",
|
||||
"eth-json-rpc-infura": "^3.0.0",
|
||||
"eth-json-rpc-middleware": "^2.4.0",
|
||||
"eth-keyring-controller": "^3.1.4",
|
||||
"eth-ledger-bridge-keyring": "^0.1.0",
|
||||
"eth-method-registry": "^1.0.0",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-sig-util": "^1.4.2",
|
||||
"eth-sig-util": "^2.0.2",
|
||||
"eth-token-tracker": "^1.1.4",
|
||||
"eth-trezor-keyring": "^0.1.0",
|
||||
"ethereumjs-abi": "^0.6.4",
|
||||
|
@ -127,6 +136,7 @@
|
|||
"ethjs-query": "^0.3.4",
|
||||
"express": "^4.15.5",
|
||||
"extension-link-enabler": "^1.0.0",
|
||||
"extension-port-stream": "^1.0.0",
|
||||
"extensionizer": "^1.0.1",
|
||||
"fast-json-patch": "^2.0.4",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
|
@ -138,28 +148,30 @@
|
|||
"gulp-eslint": "^4.0.0",
|
||||
"gulp-sass": "^4.0.0",
|
||||
"hat": "0.0.3",
|
||||
"human-standard-token-abi": "^1.0.2",
|
||||
"human-standard-token-abi": "^2.0.0",
|
||||
"idb-global": "^2.1.0",
|
||||
"identicon.js": "^2.3.1",
|
||||
"iframe": "^1.0.0",
|
||||
"iframe-stream": "^3.0.0",
|
||||
"inject-css": "^0.1.1",
|
||||
"jazzicon": "^1.2.0",
|
||||
"json-rpc-engine": "^3.6.1",
|
||||
"json-rpc-engine": "^3.7.3",
|
||||
"json-rpc-middleware-stream": "^1.0.1",
|
||||
"jsonschema": "^1.2.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"loglevel": "^1.4.1",
|
||||
"metamascara": "^2.0.0",
|
||||
"metamask-inpage-provider": "^1.0.0",
|
||||
"metamask-logo": "^2.1.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"multihashes": "^0.4.12",
|
||||
"multiplex": "^6.7.0",
|
||||
"number-to-bn": "^1.7.0",
|
||||
"obj-multiplex": "^1.0.0",
|
||||
"obs-store": "^3.0.0",
|
||||
"obs-store": "^3.0.2",
|
||||
"percentile": "^1.2.0",
|
||||
"pify": "^3.0.0",
|
||||
"ping-pong-stream": "^1.0.0",
|
||||
|
@ -178,7 +190,9 @@
|
|||
"react-addons-css-transition-group": "^15.6.0",
|
||||
"react-dom": "^15.6.2",
|
||||
"react-hyperscript": "^3.0.0",
|
||||
"react-inspector": "^2.3.0",
|
||||
"react-markdown": "^3.0.0",
|
||||
"react-media": "^1.8.0",
|
||||
"react-redux": "^5.0.5",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-select": "^1.0.0",
|
||||
|
@ -202,11 +216,11 @@
|
|||
"shallow-copy": "0.0.1",
|
||||
"sw-controller": "^1.0.3",
|
||||
"sw-stream": "^2.0.2",
|
||||
"swappable-obj-proxy": "^1.1.0",
|
||||
"textarea-caret": "^3.0.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"vreme": "^3.0.2",
|
||||
"web3": "^0.20.1",
|
||||
"web3-provider-engine": "^14.0.5",
|
||||
"web3-stream-provider": "^3.0.1",
|
||||
"webrtc-adapter": "^6.3.0",
|
||||
"xtend": "^4.0.1"
|
||||
|
@ -240,8 +254,8 @@
|
|||
"del": "^3.0.0",
|
||||
"dot-only-hunter": "^1.0.3",
|
||||
"envify": "^4.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-15": "^1.0.5",
|
||||
"enzyme": "^3.4.4",
|
||||
"enzyme-adapter-react-15": "^1.0.6",
|
||||
"eslint-plugin-chai": "0.0.1",
|
||||
"eslint-plugin-json": "^1.2.0",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
|
@ -249,6 +263,7 @@
|
|||
"eth-json-rpc-middleware": "^1.6.0",
|
||||
"eth-keyring-controller": "^3.3.1",
|
||||
"file-loader": "^1.1.11",
|
||||
"fs-extra": "^6.0.1",
|
||||
"fs-promise": "^2.0.3",
|
||||
"ganache-cli": "^6.1.0",
|
||||
"ganache-core": "^2.1.5",
|
||||
|
@ -293,6 +308,7 @@
|
|||
"open": "0.0.5",
|
||||
"path": "^0.12.7",
|
||||
"png-file-stream": "^1.0.0",
|
||||
"prepend-file": "^1.3.1",
|
||||
"prompt": "^1.0.0",
|
||||
"proxyquire": "2.0.1",
|
||||
"qs": "^6.2.0",
|
||||
|
|
|
@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
assert.equal(await txValues[0].getText(), '1 ETH')
|
||||
assert.equal(await txValues[0].getText(), '-1 ETH')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -366,7 +366,10 @@ describe('Using MetaMask with an existing account', function () {
|
|||
})
|
||||
|
||||
it('should open the TREZOR Connect popup', async () => {
|
||||
const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`))
|
||||
const trezorButton = await findElements(driver, By.css('.hw-connect__btn'))
|
||||
await trezorButton[1].click()
|
||||
await delay(regularDelayMs)
|
||||
const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
|
||||
await connectButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
const allWindows = await driver.getAllWindowHandles()
|
||||
|
|
|
@ -2,8 +2,8 @@ const fs = require('fs')
|
|||
const mkdirp = require('mkdirp')
|
||||
const pify = require('pify')
|
||||
const assert = require('assert')
|
||||
const {until} = require('selenium-webdriver')
|
||||
const { delay } = require('../func')
|
||||
const { until } = require('selenium-webdriver')
|
||||
|
||||
module.exports = {
|
||||
assertElementNotPresent,
|
||||
|
|
|
@ -225,19 +225,9 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
}
|
||||
|
||||
await clickWordAndWait(words[0])
|
||||
await clickWordAndWait(words[1])
|
||||
await clickWordAndWait(words[2])
|
||||
await clickWordAndWait(words[3])
|
||||
await clickWordAndWait(words[4])
|
||||
await clickWordAndWait(words[5])
|
||||
await clickWordAndWait(words[6])
|
||||
await clickWordAndWait(words[7])
|
||||
await clickWordAndWait(words[8])
|
||||
await clickWordAndWait(words[9])
|
||||
await clickWordAndWait(words[10])
|
||||
await clickWordAndWait(words[11])
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await clickWordAndWait(words[i])
|
||||
}
|
||||
} catch (e) {
|
||||
if (count > 2) {
|
||||
throw e
|
||||
|
@ -414,12 +404,12 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -457,16 +447,11 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
await findElement(driver, By.xpath(`//span[contains(text(), 'Submitted')]`))
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -489,9 +474,9 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
|
||||
const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('displays the contract creation data', async () => {
|
||||
|
@ -513,15 +498,15 @@ describe('MetaMask', function () {
|
|||
it('confirms a deploy contract transaction', async () => {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElement(driver, By.xpath(`//span[contains(text(), 'Submitted')]`))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 3
|
||||
}, 10000)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
|
||||
const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
|
@ -542,9 +527,9 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(extension)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
|
||||
await findElements(driver, By.css('.transaction-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -572,15 +557,17 @@ describe('MetaMask', function () {
|
|||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 4
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
const firstTxAddress = await txAccounts[0].getText()
|
||||
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
|
||||
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
// const firstTxAddress = await txAccounts[0].getText()
|
||||
// assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is received', async () => {
|
||||
|
@ -594,7 +581,7 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
const txListItem = await findElement(driver, By.css('.transaction-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -602,18 +589,20 @@ describe('MetaMask', function () {
|
|||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 5
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
|
||||
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await driver.switchTo().window(extension)
|
||||
})
|
||||
|
||||
it('renders the correct ETH balance', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
|
||||
await delay(regularDelayMs)
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
|
||||
|
@ -626,20 +615,21 @@ describe('MetaMask', function () {
|
|||
|
||||
describe('Add a custom token from a dapp', () => {
|
||||
it('creates a new token', async () => {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
let windowHandles = await driver.getAllWindowHandles()
|
||||
const extension = windowHandles[0]
|
||||
const dapp = windowHandles[1]
|
||||
await delay(regularDelayMs * 2)
|
||||
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
await delay(regularDelayMs * 2)
|
||||
|
||||
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
|
||||
await createToken.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await loadExtension(driver, extensionId)
|
||||
windowHandles = await driver.getAllWindowHandles()
|
||||
const popup = windowHandles[2]
|
||||
await driver.switchTo().window(popup)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
|
@ -657,18 +647,17 @@ describe('MetaMask', function () {
|
|||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await delay(regularDelayMs)
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
|
||||
await addToken.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('picks the newly created Test token', async () => {
|
||||
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
|
||||
const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]"))
|
||||
await addCustomToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -686,7 +675,7 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
|
||||
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
|
||||
|
@ -755,21 +744,25 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
|
||||
}
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
|
||||
assert.equal(await tx.getText(), 'Confirmed')
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 1
|
||||
}, 10000)
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
|
||||
assert.equal(await tx.getText(), 'Sent Tokens')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -792,9 +785,9 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(extension)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
|
||||
await findElements(driver, By.css('.transaction-list__pending-transactions'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -841,25 +834,28 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 2
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
|
||||
|
||||
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
|
||||
await walletBalance.click()
|
||||
|
||||
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
|
||||
await tokenListItems[0].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '43')
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '43 TST')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -883,9 +879,14 @@ describe('MetaMask', function () {
|
|||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
|
||||
driver.wait(async () => {
|
||||
const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
|
||||
return pendingTxes.length === 1
|
||||
}, 10000)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
@ -956,10 +957,15 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 3
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1009,9 +1015,69 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('renders the balance for the chosen token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
|
||||
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Stores custom RPC history', () => {
|
||||
const customRpcUrls = [
|
||||
'https://mainnet.infura.io/1',
|
||||
'https://mainnet.infura.io/2',
|
||||
'https://mainnet.infura.io/3',
|
||||
'https://mainnet.infura.io/4',
|
||||
]
|
||||
|
||||
customRpcUrls.forEach(customRpcUrl => {
|
||||
it('creates custom RPC: ' + customRpcUrl, async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
|
||||
await customRpcButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
|
||||
await customRpcInput.clear()
|
||||
await customRpcInput.sendKeys(customRpcUrl)
|
||||
|
||||
const customRpcSave = await findElement(driver, By.css('.settings-tab__rpc-save-button'))
|
||||
await customRpcSave.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('selects another provider', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`))
|
||||
await customRpcButton.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
|
||||
it('finds 3 recent RPCs in history', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// oldest selected RPC is not found
|
||||
await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`))
|
||||
|
||||
// only recent 3 are found and in correct order (most recent at the top)
|
||||
const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`))
|
||||
|
||||
assert.equal(customRpcs.length, 3)
|
||||
|
||||
for (let i = 0; i < customRpcs.length; i++) {
|
||||
const linkText = await customRpcs[i].getText()
|
||||
const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1]
|
||||
|
||||
assert.notEqual(linkText.indexOf(rpcUrl), -1)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,5 +6,5 @@ set -o pipefail
|
|||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
require('chromedriver')
|
||||
require('geckodriver')
|
||||
const fs = require('fs')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const pify = require('pify')
|
||||
const prependFile = pify(require('prepend-file'))
|
||||
const webdriver = require('selenium-webdriver')
|
||||
const Command = require('selenium-webdriver/lib/command').Command
|
||||
const By = webdriver.By
|
||||
|
||||
module.exports = {
|
||||
delay,
|
||||
createModifiedTestBuild,
|
||||
setupBrowserAndExtension,
|
||||
verboseReportOnFailure,
|
||||
buildChromeWebDriver,
|
||||
buildFirefoxWebdriver,
|
||||
installWebExt,
|
||||
|
@ -20,6 +25,37 @@ function delay (time) {
|
|||
return new Promise(resolve => setTimeout(resolve, time))
|
||||
}
|
||||
|
||||
async function createModifiedTestBuild ({ browser, srcPath }) {
|
||||
// copy build to test-builds directory
|
||||
const extPath = path.resolve(`test-builds/${browser}`)
|
||||
await fs.ensureDir(extPath)
|
||||
await fs.copy(srcPath, extPath)
|
||||
// inject METAMASK_TEST_CONFIG setting default test network
|
||||
const config = { NetworkController: { provider: { type: 'localhost' } } }
|
||||
await prependFile(`${extPath}/background.js`, `window.METAMASK_TEST_CONFIG=${JSON.stringify(config)};\n`)
|
||||
return { extPath }
|
||||
}
|
||||
|
||||
async function setupBrowserAndExtension ({ browser, extPath }) {
|
||||
let driver, extensionId, extensionUri
|
||||
|
||||
if (browser === 'chrome') {
|
||||
driver = buildChromeWebDriver(extPath)
|
||||
extensionId = await getExtensionIdChrome(driver)
|
||||
extensionUri = `chrome-extension://${extensionId}/home.html`
|
||||
} else if (browser === 'firefox') {
|
||||
driver = buildFirefoxWebdriver()
|
||||
await installWebExt(driver, extPath)
|
||||
await delay(700)
|
||||
extensionId = await getExtensionIdFirefox(driver)
|
||||
extensionUri = `moz-extension://${extensionId}/home.html`
|
||||
} else {
|
||||
throw new Error(`Unknown Browser "${browser}"`)
|
||||
}
|
||||
|
||||
return { driver, extensionId, extensionUri }
|
||||
}
|
||||
|
||||
function buildChromeWebDriver (extPath) {
|
||||
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
|
||||
return new webdriver.Builder()
|
||||
|
@ -61,3 +97,13 @@ async function installWebExt (driver, extension) {
|
|||
|
||||
return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
|
||||
}
|
||||
|
||||
async function verboseReportOnFailure ({ browser, driver, title }) {
|
||||
const artifactDir = `./test-artifacts/${browser}/${title}`
|
||||
const filepathBase = `${artifactDir}/test-failure`
|
||||
await fs.ensureDir(artifactDir)
|
||||
const screenshot = await driver.takeScreenshot()
|
||||
await fs.writeFile(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
|
||||
const htmlSource = await driver.getPageSource()
|
||||
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource)
|
||||
}
|
||||
|
|
|
@ -1,49 +1,41 @@
|
|||
const fs = require('fs')
|
||||
const mkdirp = require('mkdirp')
|
||||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
const pify = require('pify')
|
||||
const webdriver = require('selenium-webdriver')
|
||||
const { By, Key, until } = webdriver
|
||||
const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func')
|
||||
const { By, Key, until } = require('selenium-webdriver')
|
||||
const { delay, createModifiedTestBuild, setupBrowserAndExtension, verboseReportOnFailure } = require('./func')
|
||||
|
||||
describe('Metamask popup page', function () {
|
||||
let driver, accountAddress, tokenAddress, extensionId
|
||||
const browser = process.env.SELENIUM_BROWSER
|
||||
let driver, accountAddress, tokenAddress, extensionUri
|
||||
|
||||
this.timeout(0)
|
||||
|
||||
before(async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const extPath = path.resolve('dist/chrome')
|
||||
driver = buildChromeWebDriver(extPath)
|
||||
extensionId = await getExtensionIdChrome(driver)
|
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
const srcPath = path.resolve(`dist/${browser}`)
|
||||
const { extPath } = await createModifiedTestBuild({ browser, srcPath })
|
||||
const installResult = await setupBrowserAndExtension({ browser, extPath })
|
||||
driver = installResult.driver
|
||||
extensionUri = installResult.extensionUri
|
||||
|
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
const extPath = path.resolve('dist/firefox')
|
||||
driver = buildFirefoxWebdriver()
|
||||
await installWebExt(driver, extPath)
|
||||
await delay(700)
|
||||
extensionId = await getExtensionIdFirefox(driver)
|
||||
await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
}
|
||||
await driver.get(extensionUri)
|
||||
await delay(300)
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
// logs command not supported in firefox
|
||||
// https://github.com/SeleniumHQ/selenium/issues/2910
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
if (browser === 'chrome') {
|
||||
// check for console errors
|
||||
const errors = await checkBrowserForConsoleErrors()
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map(err => err.message)
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
this.test.error(new Error(errorMessage))
|
||||
console.error(new Error(errorMessage))
|
||||
|
||||
}
|
||||
}
|
||||
// gather extra data if test failed
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await verboseReportOnFailure(this.currentTest)
|
||||
await verboseReportOnFailure({ browser, driver, title: this.currentTest.title })
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -54,7 +46,6 @@ describe('Metamask popup page', function () {
|
|||
describe('Setup', function () {
|
||||
|
||||
it('switches to Chrome extensions list', async function () {
|
||||
await delay(300)
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(windowHandles[0])
|
||||
})
|
||||
|
@ -98,6 +89,7 @@ describe('Metamask popup page', function () {
|
|||
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
|
||||
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
|
||||
await button.click()
|
||||
await delay(300)
|
||||
})
|
||||
|
||||
it('shows privacy notice', async () => {
|
||||
|
@ -108,7 +100,6 @@ describe('Metamask popup page', function () {
|
|||
})
|
||||
|
||||
it('shows phishing notice', async () => {
|
||||
await delay(300)
|
||||
const noticeHeader = await driver.findElement(By.css('.terms-header')).getText()
|
||||
assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning')
|
||||
const element = await driver.findElement(By.css('.markdown'))
|
||||
|
@ -295,11 +286,7 @@ describe('Metamask popup page', function () {
|
|||
})
|
||||
|
||||
it('navigates back to MetaMask popup in the tab', async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
}
|
||||
await driver.get(extensionUri)
|
||||
await delay(700)
|
||||
})
|
||||
})
|
||||
|
@ -362,21 +349,4 @@ describe('Metamask popup page', function () {
|
|||
return matchedErrorObjects
|
||||
}
|
||||
|
||||
async function verboseReportOnFailure (test) {
|
||||
let artifactDir
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
artifactDir = `./test-artifacts/chrome/${test.title}`
|
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
artifactDir = `./test-artifacts/firefox/${test.title}`
|
||||
}
|
||||
const filepathBase = `${artifactDir}/test-failure`
|
||||
await pify(mkdirp)(artifactDir)
|
||||
// capture screenshot
|
||||
const screenshot = await driver.takeScreenshot()
|
||||
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
|
||||
// capture dom source
|
||||
const htmlSource = await driver.getPageSource()
|
||||
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
const Ganache = require('ganache-core')
|
||||
const nock = require('nock')
|
||||
import Enzyme from 'enzyme'
|
||||
import Adapter from 'enzyme-adapter-react-15'
|
||||
|
||||
nock.disableNetConnect()
|
||||
nock.enableNetConnect('localhost')
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() })
|
||||
// disallow promises from swallowing errors
|
||||
enableFailureOnUnhandledPromiseRejection()
|
||||
|
||||
// ganache server
|
||||
const server = Ganache.server()
|
||||
server.listen(8545, () => {
|
||||
console.log('Ganache Testrpc is running on "http://localhost:8545"')
|
||||
})
|
||||
|
||||
// logging util
|
||||
var log = require('loglevel')
|
||||
log.setDefaultLevel(5)
|
||||
|
@ -14,6 +25,9 @@ global.log = log
|
|||
// polyfills
|
||||
//
|
||||
|
||||
// fetch
|
||||
global.fetch = require('isomorphic-fetch')
|
||||
|
||||
// dom
|
||||
require('jsdom-global')()
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
|
|||
$('button.btn-primary.btn--large')[0].click()
|
||||
|
||||
// Verify added token image
|
||||
let heroBalance = await queryAsync($, '.hero-balance')
|
||||
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
|
||||
assert.ok(heroBalance, 'rendered hero balance')
|
||||
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
|
||||
|
||||
|
@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
|
|||
// $('button.btn-primary--lg')[0].click()
|
||||
|
||||
// Verify added token image
|
||||
heroBalance = await queryAsync($, '.hero-balance')
|
||||
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
|
||||
assert.ok(heroBalance, 'rendered hero balance')
|
||||
assert.ok(heroBalance.find('.identicon')[0], 'token added')
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
|
|||
selectState.val('confirm sig requests')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
|
||||
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
|
||||
|
||||
if (pendingRequestItem[0]) {
|
||||
pendingRequestItem[0].click()
|
||||
|
|
|
@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
|
|||
await timeout(1000)
|
||||
reactTriggerChange(selectState[0])
|
||||
await timeout(1000)
|
||||
const txView = await queryAsync($, '.tx-view')
|
||||
const heroBalance = await findAsync($(txView), '.hero-balance')
|
||||
const fiatAmount = await findAsync($(heroBalance), '.fiat-amount')
|
||||
assert.equal(fiatAmount[0].textContent, '₱102,707.97')
|
||||
const txView = await queryAsync($, '.transaction-view')
|
||||
const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
|
||||
const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
|
||||
assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
|
|||
selectState.val('send new ui')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button')
|
||||
const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
|
||||
assert.ok(sendScreenButton[1], 'send screen button present')
|
||||
sendScreenButton[1].click()
|
||||
|
||||
|
@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) {
|
|||
selectState.val('send edit')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first()
|
||||
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
|
||||
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
|
||||
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
|
||||
|
||||
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
|
||||
|
|
|
@ -29,26 +29,25 @@ async function runTxListItemsTest (assert, done) {
|
|||
assert.ok(metamaskLogo[0], 'metamask logo present')
|
||||
metamaskLogo[0].click()
|
||||
|
||||
const txListItems = await queryAsync($, '.tx-list-item')
|
||||
const txListItems = await queryAsync($, '.transaction-list-item')
|
||||
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
|
||||
|
||||
const unapprovedTx = txListItems[0]
|
||||
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
|
||||
|
||||
const retryTx = txListItems[1]
|
||||
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
|
||||
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
|
||||
const retryTxGrid = await findAsync($(txListItems[1]), '.transaction-list-item__grid')
|
||||
retryTxGrid[0].click()
|
||||
const retryTxDetails = await findAsync($(txListItems[1]), '.transaction-list-item-details')
|
||||
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
|
||||
assert.equal(headerButtons[0].textContent, 'speed up')
|
||||
|
||||
const approvedTx = txListItems[2]
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
|
||||
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
|
||||
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
|
||||
|
||||
const unapprovedMsg = txListItems[3]
|
||||
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
|
||||
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
|
||||
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
|
||||
|
||||
const failedTx = txListItems[4]
|
||||
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
|
||||
const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
|
||||
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
|
||||
|
||||
const shapeShiftTx = txListItems[5]
|
||||
|
@ -56,10 +55,10 @@ async function runTxListItemsTest (assert, done) {
|
|||
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
|
||||
|
||||
const confirmedTokenTx = txListItems[6]
|
||||
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
|
||||
assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address')
|
||||
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
|
||||
assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
|
||||
|
||||
const rejectedTx = txListItems[7]
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
|
||||
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
|
||||
|
||||
module.exports = createTxMeta
|
||||
|
||||
function createTxMeta (partialMeta) {
|
||||
const txMeta = Object.assign({
|
||||
status: 'unapproved',
|
||||
txParams: {},
|
||||
}, partialMeta)
|
||||
// initialize history
|
||||
txMeta.history = []
|
||||
// capture initial snapshot of txMeta for history
|
||||
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||
txMeta.history.push(snapshot)
|
||||
return txMeta
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
const clone = require('clone')
|
||||
const ConfigManager = require('../../app/scripts/lib/config-manager')
|
||||
const firstTimeState = require('../../app/scripts/first-time-state')
|
||||
|
||||
module.exports = function () {
|
||||
const store = new ObservableStore(clone(firstTimeState))
|
||||
return new ConfigManager({ store })
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
// polyfill fetch
|
||||
global.fetch = global.fetch || require('isomorphic-fetch')
|
||||
|
||||
const assert = require('assert')
|
||||
const nock = require('nock')
|
||||
const CurrencyController = require('../../../../app/scripts/controllers/currency')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue