diff --git a/.babelrc b/.babelrc index 307583ffd..628fe652c 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["es2015", "stage-0", "react"], - "plugins": ["transform-runtime", "transform-async-to-generator"] + "presets": [["env", { "debug": true }], "react", "stage-0"], + "plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"] } diff --git a/.circleci/config.yml b/.circleci/config.yml index bc40c9a7c..6d4bf270c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,43 +97,44 @@ workflows: jobs: prep-deps-npm: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + 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 deps via npm - command: npm install + name: Install npm 6 + deps via npm + command: | + sudo npm install -g npm@6.1.0 && npm install --no-save - save_cache: - key: dependency-cache-{{ checksum "package-lock.json" }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} paths: - node_modules - - save_cache: - key: dependency-cache-{{ .Revision }} - paths: - - node_modules - prep-deps-firefox: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout + - restore_cache: + key: v1.0-dependency-cache-firefox- - run: - name: Download Firefox + name: Download Firefox If needed command: ./.circleci/scripts/firefox-download.sh - save_cache: - key: dependency-cache-firefox-{{ .Revision }} + key: v1.0-dependency-cache-firefox- paths: - firefox prep-build: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: build:dist command: npm run dist @@ -148,11 +149,11 @@ jobs: prep-docs: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: build:dist command: npm run doc @@ -163,11 +164,11 @@ jobs: prep-scss: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -182,33 +183,33 @@ jobs: test-lint: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Test command: npm run lint test-deps: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Test command: npx nsp check test-e2e-chrome: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - run: @@ -220,16 +221,16 @@ jobs: test-e2e-firefox: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-firefox-{{ .Revision }} + key: v1.0-dependency-cache-firefox- - run: name: Install firefox command: ./.circleci/scripts/firefox-install.sh - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - run: @@ -241,11 +242,11 @@ jobs: test-e2e-beta-chrome: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - run: @@ -257,16 +258,16 @@ jobs: test-e2e-beta-firefox: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-firefox-{{ .Revision }} + key: v1.0-dependency-cache-firefox- - run: name: Install firefox command: ./.circleci/scripts/firefox-install.sh - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - run: @@ -278,11 +279,11 @@ jobs: job-screens: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - run: @@ -295,11 +296,11 @@ jobs: job-publish-prerelease: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - restore_cache: @@ -322,11 +323,11 @@ jobs: job-publish-release: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - restore_cache: key: build-cache-{{ .Revision }} - restore_cache: @@ -345,11 +346,11 @@ jobs: test-unit: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: test:coverage command: npm run test:coverage @@ -358,16 +359,16 @@ jobs: environment: browsers: '["Firefox"]' docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-firefox-{{ .Revision }} + key: v1.0-dependency-cache-firefox- - run: name: Install firefox command: ./.circleci/scripts/firefox-install.sh - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -382,11 +383,11 @@ jobs: environment: browsers: '["Chrome"]' docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -401,16 +402,16 @@ jobs: environment: browsers: '["Firefox"]' docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-firefox-{{ .Revision }} + key: v1.0-dependency-cache-firefox- - run: name: Install firefox command: ./.circleci/scripts/firefox-install.sh - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -425,11 +426,11 @@ jobs: environment: browsers: '["Chrome"]' docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - checkout - restore_cache: - key: dependency-cache-{{ .Revision }} + key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -442,8 +443,8 @@ jobs: all-tests-pass: docker: - - image: circleci/node:8-browsers + - image: circleci/node:8.11.3-browsers steps: - run: name: All Tests Passed - command: echo 'weew - everything passed!' + command: echo 'weew - everything passed!' \ No newline at end of file diff --git a/.circleci/scripts/firefox-download.sh b/.circleci/scripts/firefox-download.sh index c63e8c3df..64f0c74e3 100755 --- a/.circleci/scripts/firefox-download.sh +++ b/.circleci/scripts/firefox-download.sh @@ -1,6 +1,13 @@ #!/usr/bin/env bash - -echo "Downloading firefox..." -wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 \ -&& tar xjf firefox-58.0.tar.bz2 -echo "firefox download complete" +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 diff --git a/.circleci/scripts/firefox-install.sh b/.circleci/scripts/firefox-install.sh index 589bcbbb5..1c60f4de9 100755 --- a/.circleci/scripts/firefox-install.sh +++ b/.circleci/scripts/firefox-install.sh @@ -2,7 +2,7 @@ echo "Installing firefox..." sudo rm -r /opt/firefox -sudo mv firefox /opt/firefox58 +sudo mv firefox /opt/firefox61 sudo mv /usr/bin/firefox /usr/bin/firefox-old -sudo ln -s /opt/firefox58/firefox /usr/bin/firefox +sudo ln -s /opt/firefox61/firefox /usr/bin/firefox echo "Firefox installed." diff --git a/.eslintignore b/.eslintignore index e4cade21c..6c7f99f40 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,21 @@ +node_modules/** +dist/** +builds/** +test-builds/** +docs/** + +development/bundle.js +development/states.js + app/scripts/lib/extension-instance.js +app/scripts/chromereload.js + +ui/lib/blockies.js + +mascara/src/app/first-time/spinner.js +mascara/test/jquery-3.1.0.min.js + test/integration/bundle.js test/integration/jquery-3.1.0.min.js test/integration/helpers.js test/integration/lib/first-time.js -ui/lib/blockies.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index b49a7a28a..1317864d1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,7 +37,9 @@ "document": false, "navigator": false, "web3": true, - "window": false + "window": false, + "$": false, + "QUnit": false }, "rules": { @@ -159,5 +161,6 @@ "yield-star-spacing": [2, "both"], "yoda": [2, "never"], "prefer-const": 1, + "mocha/no-exclusive-tests": "error" } } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1cdadda65 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +ui/ @danjm @alextsg @whymarrh +app/scripts/controllers/transactions @frankiebee + diff --git a/.gitignore b/.gitignore index 0e91a7d04..e53133361 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ package .idea .vscode +# VIM +*.swp +*.swo + temp .tmp .sass-cache diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nsprc b/.nsprc index cb37fad49..bd3a4f039 100644 --- a/.nsprc +++ b/.nsprc @@ -1,6 +1,7 @@ { "exceptions": [ "https://nodesecurity.io/advisories/566", - "https://nodesecurity.io/advisories/157" + "https://nodesecurity.io/advisories/157", + "https://nodesecurity.io/advisories/577" ] } diff --git a/.nvmrc b/.nvmrc index bb83eeea2..41c421777 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v6.3.1 +v8.11.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 43302548b..7e1e1ff4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,22 @@ ## Current Master -- Attempting to import an empty private key will now show a clear error. -- Fix bug where metamask data would stop being written to disk after prolonged use -- Fix bug where account reset did not work with custom RPC providers. -- Fix for Brave i18n getAcceptLanguages [#4270](https://github.com/MetaMask/metamask-extension/issues/4270) -- Fix bug where nonce mutex was never released -- Add phishing notice +- Add new tokens auto detection +- Remove rejected transactions from transaction history +- Add Trezor Support +- Allow to remove accounts (Imported and Hardware Wallets) +- [#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): network.js: convert rpc protocol to lower case. + +## 4.8.0 Thur Jun 14 2018 + +- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error. +- [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use. +- [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers. +- [#4524](https://github.com/MetaMask/metamask-extension/pull/4524): Fix for Brave i18n getAcceptLanguages. +- [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released. +- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice. +- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan. ## 4.7.4 Tue Jun 05 2018 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8517eed70..63d19c633 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,31 +2,26 @@ If you're submitting code to MetaMask, there are some simple things we'd appreciate you doing to help us stay organized! -## Submitting pull requests +### Finding the right project Before taking the time to code and implement something, feel free to open an issue and discuss it! There may even be an issue already open, and together we may come up with a specific strategy before you take your precious time to write code. -### Tests +There are also plenty of open issues we'd love help with. Search the [`good first issue`](https://github.com/MetaMask/metamask-extension/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) label, or head to Gitcoin and earn ETH for completing projects we've posted bounties on. -For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder. +If you're picking up a bounty or an existing issue, feel free to ask clarifying questions on the issue as you go about your work. -### PR Format +### Submitting a pull request +When you're done with your project / bugfix / feature and ready to submit a PR, there are a couple guidelines we ask you to follow: -If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`. +- [ ] **Test it**: For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder. +- [ ] **Add to the CHANGELOG**: Help us keep track of all the moving pieces by adding an entry to the [`CHANGELOG.md`](https://github.com/MetaMask/metamask-extension/blob/develop/CHANGELOG.md) with a link to your PR. +- [ ] **Meet the spec**: Make sure the PR adds functionality that matches the issue you're closing. This is especially important for bounties: sometimes design or implementation details are included in the conversation, so read carefully! +- [ ] **Close the issue**: If this PR closes an open issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`. If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`. +- [ ] **Keep it simple**: Try not to include multiple features in a single PR, and don't make extraneous changes outside the scope of your contribution. All those touched files make things harder to review ;) +- [ ] **PR against `develop`**: Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production. If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging. +- [ ] **Get reviewed by a core contributor**: Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from a user with a `Member` badge before merging. -If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`. - -Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production. - -If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging. - -## Before Merging - -Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging. - -## Before Closing Issues - -Make sure the relevant code has been reviewed and merged. +And that's it! Thanks for helping out. ### Developing inside a node_modules folder diff --git a/LICENSE b/LICENSE index ddfbecf90..bbed2e24b 100644 --- a/LICENSE +++ b/LICENSE @@ -18,3 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 70faa8856..8a1eda46c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ If you're a user seeking support, [here is our support site](https://metamask.he [Mission Statement](./MISSION.md) -[Internal documentation](./docs/jsdocs) +[Internal documentation](./docs#documentation) ## Developing Compatible Dapps @@ -26,10 +26,9 @@ If you're a web dapp developer, we've got two types of guides for you: ## Building locally - - Install [Node.js](https://nodejs.org/en/) version 6.3.1 or later. + - Install [Node.js](https://nodejs.org/en/) version 8.11.3 and npm version 6.1.0 - Install dependencies: - - For node versions up to and including 9, install local dependencies with `npm install`. - - For node versions 10 and later, install [Yarn](https://yarnpkg.com/lang/en/docs/install/) and use `yarn install`. + - If you are using nvm (recommended) running `nvm use` will automatically choose the right node version for you. - Install gulp globally with `npm install -g gulp-cli`. - Build the project to the `./dist/` folder with `gulp build`. - Optionally, to rebuild on file changes, run `gulp dev`. @@ -81,6 +80,7 @@ To write tests that will be run in the browser using QUnit, add your test files - [How to add new networks to the Provider Menu](./docs/adding-new-networks.md) - [How to manage notices that appear when the app starts up](./docs/notices.md) - [How to port MetaMask to a new platform](./docs/porting_to_new_environment.md) +- [How to use the TREZOR emulator](./docs/trezor-emulator.md) - [How to generate a visualization of this repository's development](./docs/development-visualization.md) [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A diff --git a/USER_AGREEMENT.md b/USER_AGREEMENT.md index 8c4c41e58..c485edd1d 100644 --- a/USER_AGREEMENT.md +++ b/USER_AGREEMENT.md @@ -138,7 +138,7 @@ Notwithstanding the parties' decision to resolve all disputes through arbitratio ### 13.6 30-Day Right to Opt Out ### -You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them. +You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them. ### 13.7 Changes to This Section ### diff --git a/app/404.html b/app/404.html new file mode 100644 index 000000000..8a6df9d7a --- /dev/null +++ b/app/404.html @@ -0,0 +1,52 @@ + + + MetaMask + + + +
+ +

Powered by Portal Network

+
+ + diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2579da87d..8d65bc596 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -11,6 +11,9 @@ "accountName": { "message": "Account Name" }, + "accountSelectionRequired": { + "message": "You need to select an account!" + }, "address": { "message": "Address" }, @@ -40,6 +43,9 @@ "message": "MetaMask", "description": "The name of the application" }, + "approve": { + "message": "Approve" + }, "approved": { "message": "Approved" }, @@ -77,6 +83,9 @@ "borrowDharma": { "message": "Borrow With Dharma (Beta)" }, + "browserNotSupported": { + "message": "Your Browser is not supported..." + }, "builtInCalifornia": { "message": "MetaMask is designed and built in California." }, @@ -89,6 +98,9 @@ "buyCoinbaseExplainer": { "message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin." }, + "bytes": { + "message": "Bytes" + }, "ok": { "message": "Ok" }, @@ -104,6 +116,9 @@ "close": { "message": "Close" }, + "chromeRequiredForTrezor":{ + "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device." + }, "confirm": { "message": "Confirm" }, @@ -119,6 +134,24 @@ "confirmTransaction": { "message": "Confirm Transaction" }, + "connectHardwareWallet": { + "message": "Connect Hardware Wallet" + }, + "connect": { + "message": "Connect" + }, + "connecting": { + "message": "Connecting..." + }, + "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" }, @@ -149,6 +182,9 @@ "copyContractAddress": { "message": "Copy Contract Address" }, + "copyAddress": { + "message": "Copy address to clipboard" + }, "copyToClipboard": { "message": "Copy to clipboard" }, @@ -244,9 +280,15 @@ "done": { "message": "Done" }, + "downloadGoogleChrome": { + "message": "Download Google Chrome" + }, "downloadStateLogs": { "message": "Download State Logs" }, + "dontHaveATrezorWallet": { + "message": "Don't have a TREZOR hardware wallet?" + }, "dropped": { "message": "Dropped" }, @@ -265,6 +307,9 @@ "encryptNewDen": { "message": "Encrypt your new DEN" }, + "ensNameNotFound": { + "message": "ENS name not found" + }, "enterPassword": { "message": "Enter password" }, @@ -274,6 +319,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "parameters": { + "message": "Parameters" + }, "passwordNotLongEnough": { "message": "Password not long enough" }, @@ -306,6 +354,9 @@ "followTwitter": { "message": "Follow us on Twitter" }, + "forgetDevice": { + "message": "Forget this device" + }, "from": { "message": "From" }, @@ -315,6 +366,9 @@ "fromShapeShift": { "message": "From ShapeShift" }, + "functionType": { + "message": "Function Type" + }, "gas": { "message": "Gas", "description": "Short indication of gas cost" @@ -356,10 +410,28 @@ "message": "Get Ether from a faucet for the $1", "description": "Displays network name for Ether faucet" }, + "getHelp": { + "message": "Get Help." + }, "greaterThanMin": { "message": "must be greater than or equal to $1.", "description": "helper for inputting hex as decimal input" }, + "hardware": { + "message": "hardware" + }, + "hardwareWalletConnected": { + "message": "Hardware wallet connected" + }, + "hardwareSupport": { + "message": "Hardware Support" + }, + "hardwareSupportMsg": { + "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + }, + "havingTroubleConnecting": { + "message": "Having trouble connecting?" + }, "here": { "message": "here", "description": "as in -click here- for more information (goes with troubleTokenBalances)" @@ -367,6 +439,9 @@ "hereList": { "message": "Here's a list!!!!" }, + "hexData": { + "message": "Hex Data" + }, "hide": { "message": "Hide" }, @@ -455,7 +530,7 @@ "message": "Max" }, "learnMore": { - "message": "Learn more." + "message": "Learn more" }, "lessThanMax": { "message": "must be less than or equal to $1.", @@ -494,6 +569,9 @@ "mainnet": { "message": "Main Ethereum Network" }, + "menu": { + "message": "Menu" + }, "message": { "message": "Message" }, @@ -560,12 +638,18 @@ "noDeposits": { "message": "No deposits received" }, + "noConversionRateAvailable":{ + "message": "No Conversion Rate Available" + }, "noTransactionHistory": { "message": "No transaction history." }, "noTransactions": { "message": "No Transactions" }, + "notFound": { + "message": "Not Found" + }, "notStarted": { "message": "Not Started" }, @@ -575,10 +659,16 @@ "oldUIMessage": { "message": "You have returned to the old UI. You can switch back to the New UI through the option in the top right dropdown menu." }, + "openInTab": { + "message": "Open in tab" + }, "or": { "message": "or", "description": "choice between creating or importing a new account" }, + "origin": { + "message": "Origin" + }, "password": { "message": "Password" }, @@ -609,6 +699,9 @@ "popularTokens": { "message": "Popular Tokens" }, + "prev": { + "message": "Prev" + }, "privacyMsg": { "message": "Privacy Policy" }, @@ -661,6 +754,9 @@ "restoreVault": { "message": "Restore Vault" }, + "restoreAccountWithSeed": { + "message": "Restore your Account with Seed Phrase" + }, "required": { "message": "Required" }, @@ -670,6 +766,9 @@ "walletSeed": { "message": "Wallet Seed" }, + "restore": { + "message": "Restore" + }, "revealSeedWords": { "message": "Reveal Seed Words" }, @@ -688,6 +787,18 @@ "revert": { "message": "Revert" }, + "remove": { + "message": "remove" + }, + "removeAccount": { + "message": "Remove account" + }, + "removeAccountDescription": { + "message": "This account will be removed from your wallet. Please make sure you have the original seed phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. " + }, + "readyToConnect": { + "message": "Ready to Connect?" + }, "rinkeby": { "message": "Rinkeby Test Network" }, @@ -774,6 +885,9 @@ "sendTokens": { "message": "Send Tokens" }, + "separateEachWord": { + "message": "Separate each word with a single space" + }, "onlySendToEtherAddress": { "message": "Only send ETH to an Ethereum address." }, @@ -781,15 +895,45 @@ "message": "Only send $1 to an Ethereum account address.", "description": "displays token symbol" }, + "orderOneHere": { + "message": "Order one here." + }, "searchTokens": { "message": "Search Tokens" }, + "selectAnAddress": { + "message": "Select an Address" + }, + "selectAnAccount": { + "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." + }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" }, "settings": { "message": "Settings" }, + "step1HardwareWallet": { + "message": "1. Connect Hardware Wallet" + }, + "step1HardwareWalletMsg": { + "message": "Connect your hardware wallet directly to your computer." + }, + "step2HardwareWallet": { + "message": "2. Select an Account" + }, + "step2HardwareWalletMsg": { + "message": "Select the account you want to view. You can only choose one at a time." + }, + "step3HardwareWallet": { + "message": "3. Start using dApps and more!" + }, + "step3HardwareWalletMsg": { + "message": "Use your hardware account like you would with any Ethereum account. Log in to dApps, send Eth, buy and store ERC20 tokens and Non-Fungible tokens like CryptoKitties." + }, "info": { "message": "Info" }, @@ -899,9 +1043,15 @@ "transactionNumber": { "message": "Transaction Number" }, + "transfer": { + "message": "Transfer" + }, "transfers": { "message": "Transfers" }, + "trezorHardwareWallet": { + "message": "TREZOR Hardware Wallet" + }, "troubleTokenBalances": { "message": "We had trouble loading your token balances. You can view them ", "description": "Followed by a link (here) to view token balances" @@ -927,12 +1077,18 @@ "unknown": { "message": "Unknown" }, + "unknownFunction": { + "message": "Unknown Function" + }, "unknownNetwork": { "message": "Unknown Private Network" }, "unknownNetworkId": { "message": "Unknown network ID" }, + "unlock": { + "message": "Unlock" + }, "unlockMessage": { "message": "The decentralized web awaits" }, diff --git a/app/_locales/index.json b/app/_locales/index.json index 7717502b7..f50c09f88 100644 --- a/app/_locales/index.json +++ b/app/_locales/index.json @@ -17,6 +17,6 @@ { "code": "tml", "name": "Tamil" }, { "code": "tr", "name": "Turkish" }, { "code": "vi", "name": "Vietnamese" }, - { "code": "zh_CN", "name": "Mandarin" }, - { "code": "zh_TW", "name": "Taiwanese" } + { "code": "zh_CN", "name": "Chinese (Simplified)" }, + { "code": "zh_TW", "name": "Chinese (Traditional)" } ] diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 75deeaddf..c9d192139 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -122,6 +122,9 @@ "copy": { "message": "コピー" }, + "copyContractAddress": { + "message": "コントラクトアドレスをコピー" + }, "copyToClipboard": { "message": "クリップボードへコピー" }, @@ -395,6 +398,9 @@ "mainnet": { "message": "Ethereumメインネットワーク" }, + "menu": { + "message": "メニュー" + }, "message": { "message": "メッセージ" }, @@ -464,6 +470,9 @@ "oldUIMessage": { "message": "旧UIを表示しています。右上のドロップダウンメニューのオプションより、新UIへ切り替えが可能です。" }, + "openInTab": { + "message": "タブを開く" + }, "or": { "message": "または", "description": "choice between creating or importing a new account" @@ -573,6 +582,15 @@ "searchResults": { "message": "検索結果" }, + "newPassword8Chars": { + "message": "新しいパスワード (8桁以上)" + }, + "select": { + "message": "選択" + }, + "selectCurrency": { + "message": "通貨を選択" + }, "selectService": { "message": "サービスを選択" }, @@ -586,10 +604,14 @@ "message": "ETHの送信" }, "sendTokens": { - "message": "トークンを送る" + "message": "トークンを送信" }, "onlySendToEtherAddress": { - "message": "ETHはイーサリウムアカウントのみに送信できます。" + "message": "ETH はイーサリウムアカウントのみに送信できます。" + }, + "onlySendTokensToAccountAddress": { + "message": "$1 はイーサリアムアカウントのみに送信できます。", + "description": "displays token symbol" }, "searchTokens": { "message": "トークンの検索" @@ -690,10 +712,10 @@ "message": "パスワードの入力" }, "uiWelcome": { - "message": "新UIへようこそ!(ベータ版)" + "message": "新UIへようこそ! (ベータ版)" }, "uiWelcomeMessage": { - "message": "現在Metamaskの新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう!何か問題があればご報告ください。" + "message": "現在、MetaMask の新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう! 何か問題があればご報告ください。" }, "unavailable": { "message": "有効ではありません。" @@ -720,6 +742,9 @@ "viewAccount": { "message": "アカウントを見る" }, + "viewOnEtherscan": { + "message": "Etherscan で見る" + }, "warning": { "message": "警告" }, diff --git a/app/error.html b/app/error.html new file mode 100644 index 000000000..366b3d94a --- /dev/null +++ b/app/error.html @@ -0,0 +1,79 @@ + + + MetaMask Error + + + + +
+ +

not found

+

Powered by Portal Network

+
+ + + diff --git a/app/images/404.png b/app/images/404.png new file mode 100644 index 000000000..b1a767dde Binary files /dev/null and b/app/images/404.png differ diff --git a/app/images/alert-red.svg b/app/images/alert-red.svg new file mode 100644 index 000000000..ac5b30e27 --- /dev/null +++ b/app/images/alert-red.svg @@ -0,0 +1,14 @@ + + + + Artboard Copy + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/app/images/alert.svg b/app/images/alert.svg new file mode 100644 index 000000000..534eda194 --- /dev/null +++ b/app/images/alert.svg @@ -0,0 +1,19 @@ + + + + 7414FFD8-B28A-4593-9D7E-19E73D687B50 + Created with sketchtool. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/cancel.png b/app/images/cancel.png new file mode 100644 index 000000000..4e0eb1143 Binary files /dev/null and b/app/images/cancel.png differ diff --git a/app/images/caret-left.svg b/app/images/caret-left.svg new file mode 100644 index 000000000..0ea266161 --- /dev/null +++ b/app/images/caret-left.svg @@ -0,0 +1,18 @@ + + + + 8439120D-5704-4273-B416-FEE134322584 + Created with sketchtool. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/connect-icon.svg b/app/images/connect-icon.svg new file mode 100644 index 000000000..84540999a --- /dev/null +++ b/app/images/connect-icon.svg @@ -0,0 +1,11 @@ + + + + background + + + + Layer 1 + + + \ No newline at end of file diff --git a/app/images/deadface.png b/app/images/deadface.png new file mode 100644 index 000000000..e12476c03 Binary files /dev/null and b/app/images/deadface.png differ diff --git a/app/images/ethereum-metamask-chrome.png b/app/images/ethereum-metamask-chrome.png new file mode 100644 index 000000000..0b886babb Binary files /dev/null and b/app/images/ethereum-metamask-chrome.png differ diff --git a/app/images/hardware-wallet-step-1.svg b/app/images/hardware-wallet-step-1.svg new file mode 100644 index 000000000..2b6596a43 --- /dev/null +++ b/app/images/hardware-wallet-step-1.svg @@ -0,0 +1,44 @@ + + + + 2981A924-C7CB-4957-87AD-8C680802DAD7 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/hardware-wallet-step-2.svg b/app/images/hardware-wallet-step-2.svg new file mode 100644 index 000000000..9fff05b7e --- /dev/null +++ b/app/images/hardware-wallet-step-2.svg @@ -0,0 +1,81 @@ + + + + 27B850D0-B3BA-4F98-8BB4-B542D8BFDE3B + Created with sketchtool. + + + + + + + + + + + + + + + + + + 3 + + + OXz3…T3A4 + + + 0.020000 ETH + + + + + + + 1 + + + OXa4…s0a2 + + + 0.01500 ETH + + + + + + + 4 + + + OXd2…D0V4 + + + 0.030000 ETH + + + + + + + + + + + 2 + + + OXe7…B0a1 + + + 0.041000 ETH + + + + + + + + + \ No newline at end of file diff --git a/app/images/hardware-wallet-step-3.svg b/app/images/hardware-wallet-step-3.svg new file mode 100644 index 000000000..4a7655b3b --- /dev/null +++ b/app/images/hardware-wallet-step-3.svg @@ -0,0 +1,42 @@ + + + + CEB55C41-7BCE-405E-83CD-834B388B495F + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LOGIN WITH + METAMASK + + + + + + \ No newline at end of file diff --git a/app/images/loginglogo.svg b/app/images/loginglogo.svg new file mode 100644 index 000000000..ca8b0a2ee --- /dev/null +++ b/app/images/loginglogo.svg @@ -0,0 +1,53 @@ + + + + logo2 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/logo.png b/app/images/logo.png new file mode 100644 index 000000000..db6ba7d6c Binary files /dev/null and b/app/images/logo.png differ diff --git a/app/images/pw-128x128.png b/app/images/pw-128x128.png new file mode 100644 index 000000000..a0eb1b730 Binary files /dev/null and b/app/images/pw-128x128.png differ diff --git a/app/images/pw-48x48.png b/app/images/pw-48x48.png new file mode 100644 index 000000000..a96c59e15 Binary files /dev/null and b/app/images/pw-48x48.png differ diff --git a/app/images/pw128x128.png b/app/images/pw128x128.png new file mode 100644 index 000000000..a0eb1b730 Binary files /dev/null and b/app/images/pw128x128.png differ diff --git a/app/loading.html b/app/loading.html new file mode 100644 index 000000000..aef5d9607 --- /dev/null +++ b/app/loading.html @@ -0,0 +1,35 @@ + + + MetaMask Loading + + + + + + diff --git a/app/manifest.json b/app/manifest.json index e3a7fd963..ed328f19f 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.7.4", + "version": "4.8.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -59,10 +59,16 @@ "unlimitedStorage", "clipboardWrite", "http://localhost:8545/", - "https://*.infura.io/" + "https://*.infura.io/", + "activeTab", + "webRequest", + "*://*.eth/", + "*://*.test/", + "notifications" ], "web_accessible_resources": [ - "inpage.js" + "inpage.js", + "phishing.html" ], "externally_connectable": { "matches": [ @@ -72,4 +78,4 @@ "*" ] } -} \ No newline at end of file +} diff --git a/app/phishing.html b/app/phishing.html new file mode 100644 index 000000000..86f2985cc --- /dev/null +++ b/app/phishing.html @@ -0,0 +1,60 @@ + + + + + + Phishing Warning + + + + + + + + +
+ + +

ATTENTION

+

MetaMask believes this domain to have malicious intent and has prevented you from interacting with it.

+

This is because the site tested positive on the Ethereum Phishing Detector.

+

You can turn MetaMask off to interact with this site, but it's advised not to.

+

If you think this domain is incorrectly flagged, please file an issue.

+ +
+ + \ No newline at end of file diff --git a/app/scripts/background.js b/app/scripts/background.js index 2be600c4b..c0b00730d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -26,6 +26,8 @@ const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const EdgeEncryptor = require('./edge-encryptor') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getObjStructure = require('./lib/getObjStructure') +const ipfsContent = require('./lib/ipfsContent.js') + const { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, @@ -42,8 +44,8 @@ const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager // setup sentry error reporting -const release = platform.getVersion() -const raven = setupRaven({ release }) +const releaseVersion = platform.getVersion() +const raven = setupRaven({ releaseVersion }) // browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser // Internet Explorer 6-11 @@ -51,6 +53,7 @@ const isIE = !!document.documentMode // Edge 20+ const isEdge = !isIE && !!window.StyleMedia +let ipfsHandle let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} @@ -66,6 +69,7 @@ initialize().catch(log.error) // setup metamask mesh testing container setupMetamaskMeshMetrics() + /** * An object representing a transaction, in whatever state it is in. * @typedef TransactionMeta @@ -155,6 +159,7 @@ async function initialize () { const initLangCode = await getFirstPreferredLangCode() await setupController(initState, initLangCode) log.debug('MetaMask initialization complete.') + ipfsHandle = ipfsContent(initState.NetworkController.provider) } // @@ -259,6 +264,11 @@ function setupController (initState, initLangCode) { }) global.metamaskController = controller + controller.networkController.on('networkDidChange', () => { + ipfsHandle && ipfsHandle.remove() + ipfsHandle = ipfsContent(controller.networkController.providerStore.getState()) + }) + // report failed transactions to Sentry controller.txController.on(`tx:status-update`, (txId, status) => { if (status !== 'failed') return @@ -379,7 +389,7 @@ function setupController (initState, initLangCode) { } // communication with page or other extension - function connectExternal(remotePort) { + function connectExternal (remotePort) { const originDomain = urlUtil.parse(remotePort.sender.url).hostname const portStream = new PortStream(remotePort) controller.setupUntrustedCommunication(portStream, originDomain) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 75e0a95b3..b7496f318 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -115,8 +115,8 @@ function logStreamDisconnectWarning (remoteLabel, err) { * @returns {boolean} {@code true} if Web3 should be injected */ function shouldInjectWeb3 () { - return doctypeCheck() && suffixCheck() - && documentElementCheck() && !blacklistedDomainCheck() + return doctypeCheck() && suffixCheck() && + documentElementCheck() && !blacklistedDomainCheck() } /** @@ -177,6 +177,9 @@ function blacklistedDomainCheck () { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', 'adyen.com', 'gravityforms.com', + 'harbourair.com', + 'ani.gamer.com.tw', + 'blueskybooking.com', ] var currentUrl = window.location.href var currentRegex @@ -194,6 +197,7 @@ function blacklistedDomainCheck () { * Redirects the current page to a phishing information page */ function redirectToPhishingWarning () { - console.log('MetaMask - redirecting to phishing warning') - window.location.href = 'https://metamask.io/phishing.html' + console.log('MetaMask - routing to Phishing Warning component') + let extensionURL = extension.runtime.getURL('phishing.html') + window.location.href = extensionURL } diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 86619fce1..4c97810a3 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -60,7 +60,7 @@ class BalanceController { * Sets up listeners and subscriptions which should trigger an update of ethBalance. These updates include: * - when a transaction changes state to 'submitted', 'confirmed' or 'failed' * - when the current account changes (i.e. a new account is selected) - * - when there is a block update + * - when there is a block update * * @private * @@ -100,7 +100,7 @@ class BalanceController { /** * Gets the pending transactions (i.e. those with a 'submitted' status). These are accessed from the - * TransactionController passed to this BalanceController during construction. + * TransactionController passed to this BalanceController during construction. * * @private * @returns {Promise} Promises an array of transaction objects. diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index f100c4525..1d2191433 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -87,7 +87,7 @@ class BlacklistController { * * @private * @param {object} config A config object like that found at {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json} - * + * */ _setupPhishingDetector (config) { this._phishingDetector = new PhishingDetector(config) diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 1a6802f9a..e04ce2ef7 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -18,7 +18,7 @@ class ComputedbalancesController { /** * Creates a new controller instance * - * @param {ComputedBalancesOptions} [opts] Controller configuration parameters + * @param {ComputedBalancesOptions} [opts] Controller configuration parameters */ constructor (opts = {}) { const { accountTracker, txController, blockTracker } = opts diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index 480c08b1c..a93aff49b 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -16,9 +16,9 @@ class CurrencyController { * currentCurrency, conversionRate and conversionDate properties * @property {string} currentCurrency A 2-4 character shorthand that describes a specific currency, currently * selected by the user - * @property {number} conversionRate The conversion rate from ETH to the selected currency. + * @property {number} conversionRate The conversion rate from ETH to the selected currency. * @property {string} conversionDate The date at which the conversion rate was set. Expressed in in milliseconds - * since midnight of January 1, 1970 + * since midnight of January 1, 1970 * @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method. * Used to clear an existing interval on subsequent calls of that method. * @@ -59,7 +59,7 @@ class CurrencyController { /** * A getter for the conversionRate property * - * @returns {string} The conversion rate from ETH to the selected currency. + * @returns {string} The conversion rate from ETH to the selected currency. * */ getConversionRate () { @@ -80,7 +80,7 @@ class CurrencyController { * A getter for the conversionDate property * * @returns {string} The date at which the conversion rate was set. Expressed in milliseconds since midnight of - * January 1, 1970 + * January 1, 1970 * */ getConversionDate () { diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js new file mode 100644 index 000000000..195ec918a --- /dev/null +++ b/app/scripts/controllers/detect-tokens.js @@ -0,0 +1,130 @@ +const Web3 = require('web3') +const contracts = require('eth-contract-metadata') +const { warn } = require('loglevel') +const { MAINNET } = require('./network/enums') +// By default, poll every 3 minutes +const DEFAULT_INTERVAL = 180 * 1000 +const ERC20_ABI = [{'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'type': 'function'}] + +/** + * A controller that polls for token exchange + * rates based on a user's current token list + */ +class DetectTokensController { + /** + * Creates a DetectTokensController + * + * @param {Object} [config] - Options to configure controller + */ + constructor ({ interval = DEFAULT_INTERVAL, preferences, network, keyringMemStore } = {}) { + this.preferences = preferences + this.interval = interval + this.network = network + this.keyringMemStore = keyringMemStore + } + + /** + * For each token in eth-contract-metada, find check selectedAddress balance. + * + */ + async detectNewTokens () { + if (!this.isActive) { return } + if (this._network.store.getState().provider.type !== MAINNET) { return } + this.web3.setProvider(this._network._provider) + for (const contractAddress in contracts) { + if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) { + this.detectTokenBalance(contractAddress) + } + } + } + + /** + * Find if selectedAddress has tokens with contract in contractAddress. + * + * @param {string} contractAddress Hex address of the token contract to explore. + * @returns {boolean} If balance is detected, token is added. + * + */ + async detectTokenBalance (contractAddress) { + const ethContract = this.web3.eth.contract(ERC20_ABI).at(contractAddress) + ethContract.balanceOf(this.selectedAddress, (error, result) => { + if (!error) { + if (!result.isZero()) { + this._preferences.addToken(contractAddress, contracts[contractAddress].symbol, contracts[contractAddress].decimals) + } + } else { + warn(`MetaMask - DetectTokensController balance fetch failed for ${contractAddress}.`, error) + } + }) + } + + /** + * Restart token detection polling period and call detectNewTokens + * in case of address change or user session initialization. + * + */ + restartTokenDetection () { + if (!(this.isActive && this.selectedAddress)) { return } + this.detectNewTokens() + this.interval = DEFAULT_INTERVAL + } + + /** + * @type {Number} + */ + set interval (interval) { + this._handle && clearInterval(this._handle) + if (!interval) { return } + this._handle = setInterval(() => { this.detectNewTokens() }, interval) + } + + /** + * In setter when selectedAddress is changed, detectNewTokens and restart polling + * @type {Object} + */ + set preferences (preferences) { + if (!preferences) { return } + this._preferences = preferences + preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) }) + preferences.store.subscribe(({ selectedAddress }) => { + if (this.selectedAddress !== selectedAddress) { + this.selectedAddress = selectedAddress + this.restartTokenDetection() + } + }) + } + + /** + * @type {Object} + */ + set network (network) { + if (!network) { return } + this._network = network + this.web3 = new Web3(network._provider) + } + + /** + * In setter when isUnlocked is updated to true, detectNewTokens and restart polling + * @type {Object} + */ + set keyringMemStore (keyringMemStore) { + if (!keyringMemStore) { return } + this._keyringMemStore = keyringMemStore + this._keyringMemStore.subscribe(({ isUnlocked }) => { + if (this.isUnlocked !== isUnlocked) { + this.isUnlocked = isUnlocked + if (isUnlocked) { this.restartTokenDetection() } + } + }) + } + + /** + * Internal isActive state + * @type {Object} + */ + get isActive () { + return this.isOpen && this.isUnlocked + } +} + +module.exports = DetectTokensController diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 9da7f309c..3190eb37c 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -4,6 +4,7 @@ const KOVAN = 'kovan' const MAINNET = 'mainnet' const LOCALHOST = 'localhost' +const MAINNET_CODE = 1 const ROPSTEN_CODE = 3 const RINKEYBY_CODE = 4 const KOVAN_CODE = 42 @@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby' const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' - module.exports = { ROPSTEN, RINKEBY, KOVAN, MAINNET, LOCALHOST, + MAINNET_CODE, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 5e0c63e7d..b6f7705b5 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -9,6 +9,7 @@ const extend = require('xtend') const EthQuery = require('eth-query') const createEventEmitterProxy = require('../../lib/events-proxy.js') const log = require('loglevel') +const urlUtil = require('url') const { ROPSTEN, RINKEBY, @@ -132,7 +133,7 @@ module.exports = class NetworkController extends EventEmitter { } else if (type === LOCALHOST) { this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL }) // url-based rpc endpoints - } else if (type === 'rpc'){ + } else if (type === 'rpc') { this._configureStandardProvider({ rpcUrl: rpcTarget }) } else { throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`) @@ -155,6 +156,8 @@ module.exports = class NetworkController extends EventEmitter { } _configureStandardProvider ({ rpcUrl }) { + // urlUtil handles malformed urls + rpcUrl = urlUtil.parse(rpcUrl).format() const providerParams = extend(this._baseProviderParams, { rpcUrl, engineParams: { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index f1bd66889..8a4a63bb6 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -126,6 +126,30 @@ class PreferencesController { this.store.updateState({ identities }) } + /** + * Removes an address from state + * + * @param {string} address A hex address + * @returns {string} the address that was removed + */ + removeAddress (address) { + const identities = this.store.getState().identities + if (!identities[address]) { + throw new Error(`${address} can't be deleted cause it was not found`) + } + delete identities[address] + this.store.updateState({ identities }) + + // If the selected account is no longer valid, + // select an arbitrary other account: + if (address === this.getSelectedAddress()) { + const selected = Object.keys(identities)[0] + this.setSelectedAddress(selected) + } + return address + } + + /** * Adds addresses to the identities object without removing identities * @@ -152,9 +176,9 @@ class PreferencesController { * @returns {Promise} selectedAddress the selected address. */ syncAddresses (addresses) { - let { identities, lostIdentities } = this.store.getState() + const { identities, lostIdentities } = this.store.getState() - let newlyLost = {} + const newlyLost = {} Object.keys(identities).forEach((identity) => { if (!addresses.includes(identity)) { newlyLost[identity] = identities[identity] @@ -169,7 +193,7 @@ class PreferencesController { if (this.diagnostics) this.diagnostics.reportOrphans(newlyLost) // store lost accounts - for (let key in newlyLost) { + for (const key in newlyLost) { lostIdentities[key] = newlyLost[key] } } diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 033ef1d7e..926268691 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -117,7 +117,7 @@ class RecentBlocksController { * * @returns {Promise} Promises undefined */ - async backfill() { + async backfill () { this.blockTracker.once('block', async (block) => { const currentBlockNumber = Number.parseInt(block.number, 16) const blocksToFetch = Math.min(currentBlockNumber, this.historyLength) diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index 35ca08d6c..06f336eaa 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -129,19 +129,6 @@ class NonceTracker { return Number.isInteger(highest) ? highest + 1 : 0 } - _reduceTxListToUniqueNonces (txList) { - const reducedTxList = txList.reduce((reducedList, txMeta, index) => { - if (!index) return [txMeta] - const nonceMatches = txList.filter((txData) => { - return txMeta.txParams.nonce === txData.txParams.nonce - }) - if (nonceMatches.length > 1) return reducedList - reducedList.push(txMeta) - return reducedList - }, []) - return reducedTxList - } - _getHighestNonce (txList) { const nonces = txList.map((txMeta) => { const nonce = txMeta.txParams.nonce diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 36b5cdbc9..5cd0f5407 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -30,14 +30,10 @@ class TxGasUtil { try { estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) } catch (err) { - const simulationFailed = ( - err.message.includes('Transaction execution error.') || - err.message.includes('gas required exceeds allowance or always failing transaction') - ) - if (simulationFailed) { - txMeta.simulationFails = true - return txMeta + txMeta.simulationFails = { + reason: err.message, } + return txMeta } this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) return txMeta @@ -126,4 +122,4 @@ class TxGasUtil { } } -module.exports = TxGasUtil \ No newline at end of file +module.exports = TxGasUtil diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 0aae4774b..28a18ca2e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter { */ setTxStatusRejected (txId) { this._setTxStatus(txId, 'rejected') + this._removeTx(txId) } /** @@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter { _saveTxList (transactions) { this.store.updateState({ transactions }) } + + _removeTx (txId) { + const transactionList = this.getFullTxList() + this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId)) + } } module.exports = TransactionStateManager diff --git a/app/scripts/lib/cleanErrorStack.js b/app/scripts/lib/cleanErrorStack.js index fe1bfb0ce..8adf55db7 100644 --- a/app/scripts/lib/cleanErrorStack.js +++ b/app/scripts/lib/cleanErrorStack.js @@ -3,7 +3,7 @@ * @param {Error} err - error * @returns {Error} Error with clean stack trace. */ -function cleanErrorStack(err){ +function cleanErrorStack (err) { var name = err.name name = (name === undefined) ? 'Error' : String(name) diff --git a/app/scripts/lib/contracts/registrar.js b/app/scripts/lib/contracts/registrar.js new file mode 100644 index 000000000..99ca24458 --- /dev/null +++ b/app/scripts/lib/contracts/registrar.js @@ -0,0 +1 @@ +module.exports = [{'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'resolver', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'label', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setSubnodeOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'ttl', 'type': 'uint64'}], 'name': 'setTTL', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'ttl', 'outputs': [{'name': '', 'type': 'uint64'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'resolver', 'type': 'address'}], 'name': 'setResolver', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'label', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'NewOwner', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'resolver', 'type': 'address'}], 'name': 'NewResolver', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'ttl', 'type': 'uint64'}], 'name': 'NewTTL', 'type': 'event'}] diff --git a/app/scripts/lib/contracts/resolver.js b/app/scripts/lib/contracts/resolver.js new file mode 100644 index 000000000..1bf3f90ce --- /dev/null +++ b/app/scripts/lib/contracts/resolver.js @@ -0,0 +1,2 @@ +module.exports = +[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}] diff --git a/app/scripts/lib/createErrorMiddleware.js b/app/scripts/lib/createErrorMiddleware.js index c70beddfd..7f6a4bd73 100644 --- a/app/scripts/lib/createErrorMiddleware.js +++ b/app/scripts/lib/createErrorMiddleware.js @@ -64,4 +64,4 @@ function createErrorMiddleware ({ override = true } = {}) { } } -module.exports = createErrorMiddleware \ No newline at end of file +module.exports = createErrorMiddleware diff --git a/app/scripts/lib/createStreamSink.js b/app/scripts/lib/createStreamSink.js index cf9416fea..b93dbc089 100644 --- a/app/scripts/lib/createStreamSink.js +++ b/app/scripts/lib/createStreamSink.js @@ -4,7 +4,7 @@ const promiseToCallback = require('promise-to-callback') module.exports = createStreamSink -function createStreamSink(asyncWriteFn, _opts) { +function createStreamSink (asyncWriteFn, _opts) { return new AsyncWritableStream(asyncWriteFn, _opts) } diff --git a/app/scripts/lib/diagnostics-reporter.js b/app/scripts/lib/diagnostics-reporter.js index aa4ca6e26..569eb3268 100644 --- a/app/scripts/lib/diagnostics-reporter.js +++ b/app/scripts/lib/diagnostics-reporter.js @@ -5,7 +5,7 @@ class DiagnosticsReporter { this.version = version } - async reportOrphans(orphans) { + async reportOrphans (orphans) { try { return await this.submit({ accounts: Object.keys(orphans), @@ -19,7 +19,7 @@ class DiagnosticsReporter { } } - async reportMultipleKeyrings(rawKeyrings) { + async reportMultipleKeyrings (rawKeyrings) { try { const keyrings = await Promise.all(rawKeyrings.map(async (keyring, index) => { return { @@ -55,7 +55,7 @@ class DiagnosticsReporter { } -function postData(data) { +function postData (data) { const uri = 'https://diagnostics.metamask.io/v1/orphanedAccounts' return fetch(uri, { body: JSON.stringify(data), // must match 'Content-Type' header diff --git a/app/scripts/lib/extractEthjsErrorMessage.js b/app/scripts/lib/extractEthjsErrorMessage.js index 0f100756f..4891075c3 100644 --- a/app/scripts/lib/extractEthjsErrorMessage.js +++ b/app/scripts/lib/extractEthjsErrorMessage.js @@ -10,13 +10,13 @@ module.exports = extractEthjsErrorMessage * * @param {string} errorMessage The error message to parse * @returns {string} Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError - * + * * @example * // returns 'Transaction Failed: replacement transaction underpriced' * extractEthjsErrorMessage(`Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced`) * */ -function extractEthjsErrorMessage(errorMessage) { +function extractEthjsErrorMessage (errorMessage) { const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug) if (isEthjsRpcError) { const payloadAndError = errorMessage.slice(ethJsRpcSlug.length) diff --git a/app/scripts/lib/get-first-preferred-lang-code.js b/app/scripts/lib/get-first-preferred-lang-code.js index 41a886d74..170d508c1 100644 --- a/app/scripts/lib/get-first-preferred-lang-code.js +++ b/app/scripts/lib/get-first-preferred-lang-code.js @@ -28,7 +28,7 @@ async function getFirstPreferredLangCode () { // safeguard for Brave Browser until they implement chrome.i18n.getAcceptLanguages // https://github.com/MetaMask/metamask-extension/issues/4270 - if (!userPreferredLocaleCodes){ + if (!userPreferredLocaleCodes) { userPreferredLocaleCodes = [] } diff --git a/app/scripts/lib/getObjStructure.js b/app/scripts/lib/getObjStructure.js index 52250d3fb..9c92879fb 100644 --- a/app/scripts/lib/getObjStructure.js +++ b/app/scripts/lib/getObjStructure.js @@ -18,12 +18,12 @@ module.exports = getObjStructure * Creates an object that represents the structure of the given object. It replaces all values with the result of their * type. * - * @param {object} obj The object for which a 'structure' will be returned. Usually a plain object and not a class. + * @param {object} obj The object for which a 'structure' will be returned. Usually a plain object and not a class. * @returns {object} The "mapped" version of a deep clone of the passed object, with each non-object property value * replaced with the javascript type of that value. * */ -function getObjStructure(obj) { +function getObjStructure (obj) { const structure = clone(obj) return deepMap(structure, (value) => { return value === null ? 'null' : typeof value @@ -38,7 +38,7 @@ function getObjStructure(obj) { * @param {Function} visit The modifier to apply to each non-object property value * @returns {object} The modified object */ -function deepMap(target = {}, visit) { +function deepMap (target = {}, visit) { Object.entries(target).forEach(([key, value]) => { if (typeof value === 'object' && value !== null) { target[key] = deepMap(value, visit) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 4e65f0a23..6ef511453 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -54,6 +54,11 @@ function MetamaskInpageProvider (connectionStream) { // 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) } diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js new file mode 100644 index 000000000..5222151ea --- /dev/null +++ b/app/scripts/lib/ipfsContent.js @@ -0,0 +1,44 @@ +const extension = require('extensionizer') +const resolver = require('./resolver.js') + +module.exports = function (provider) { + function ipfsContent (details) { + const name = details.url.substring(7, details.url.length - 1) + let clearTime = null + extension.tabs.getSelected(null, tab => { + extension.tabs.update(tab.id, { url: 'loading.html' }) + + clearTime = setTimeout(() => { + return extension.tabs.update(tab.id, { url: '404.html' }) + }, 60000) + + resolver.resolve(name, provider).then(ipfsHash => { + clearTimeout(clearTime) + let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => { + if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' }) + extension.tabs.update(tab.id, { url: url }) + }) + .catch(err => { + url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + extension.tabs.update(tab.id, {url: url}) + return err + }) + }) + .catch(err => { + clearTimeout(clearTime) + const url = err === 'unsupport' ? 'unsupport' : 'error' + extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`}) + }) + }) + return { cancel: true } + } + + extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + + return { + remove () { + extension.webRequest.onBeforeRequest.removeListener(ipfsContent) + }, + } +} diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js index 139ff86bd..fbcba09cd 100644 --- a/app/scripts/lib/local-store.js +++ b/app/scripts/lib/local-store.js @@ -8,7 +8,7 @@ module.exports = class ExtensionStore { /** * @constructor */ - constructor() { + constructor () { this.isSupported = !!(extension.storage.local) if (!this.isSupported) { log.error('Storage local API not available.') @@ -19,7 +19,7 @@ module.exports = class ExtensionStore { * Returns all of the keys currently saved * @return {Promise<*>} */ - async get() { + async get () { if (!this.isSupported) return undefined const result = await this._get() // extension.storage.local always returns an obj @@ -36,7 +36,7 @@ module.exports = class ExtensionStore { * @param {object} state - The state to set * @return {Promise} */ - async set(state) { + async set (state) { return this._set(state) } @@ -45,7 +45,7 @@ module.exports = class ExtensionStore { * @private * @return {object} the key-value map from local storage */ - _get() { + _get () { const local = extension.storage.local return new Promise((resolve, reject) => { local.get(null, (/** @type {any} */ result) => { @@ -65,7 +65,7 @@ module.exports = class ExtensionStore { * @return {Promise} * @private */ - _set(obj) { + _set (obj) { const local = extension.storage.local return new Promise((resolve, reject) => { local.set(obj, () => { @@ -85,6 +85,6 @@ module.exports = class ExtensionStore { * @param {object} obj - The object to check * @returns {boolean} */ -function isEmpty(obj) { +function isEmpty (obj) { return Object.keys(obj).length === 0 } diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 5dfb42078..969a9459a 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -26,13 +26,15 @@ class NotificationManager { // bring focus to existing chrome popup extension.windows.update(popup.id, { focused: true }) } else { + const cb = (currentPopup) => { this._popupId = currentPopup.id } // create new notification popup - extension.windows.create({ + const creation = extension.windows.create({ url: 'notification.html', type: 'popup', width, height, - }) + }, cb) + creation && creation.then && creation.then(cb) } }) } @@ -84,7 +86,7 @@ class NotificationManager { } /** - * Given an array of windows, returns the first that has a 'popup' type, or null if no such window exists. + * Given an array of windows, returns the 'popup' that has been opened by MetaMask, or null if no such window exists. * * @private * @param {array} windows An array of objects containing data about the open MetaMask extension windows. @@ -93,7 +95,7 @@ class NotificationManager { _getPopupIn (windows) { return windows ? windows.find((win) => { // Returns notification popup - return (win && win.type === 'popup') + return (win && win.type === 'popup' && win.id === this._popupId) }) : null } diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index 5c4224fd9..fd65d94f3 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -58,7 +58,7 @@ 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 diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js index e09f4f1f8..df5661e59 100644 --- a/app/scripts/lib/reportFailedTxToSentry.js +++ b/app/scripts/lib/reportFailedTxToSentry.js @@ -7,7 +7,7 @@ module.exports = reportFailedTxToSentry // for sending to sentry // -function reportFailedTxToSentry({ raven, txMeta }) { +function reportFailedTxToSentry ({ raven, txMeta }) { const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message) raven.captureMessage(errorMessage, { // "extra" key is required by Sentry diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js new file mode 100644 index 000000000..ff0fed161 --- /dev/null +++ b/app/scripts/lib/resolver.js @@ -0,0 +1,71 @@ +const namehash = require('eth-ens-namehash') +const multihash = require('multihashes') +const HttpProvider = require('ethjs-provider-http') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') +const registrarAbi = require('./contracts/registrar') +const resolverAbi = require('./contracts/resolver') + +function ens (name, provider) { + const eth = new Eth(new HttpProvider(getProvider(provider.type))) + const hash = namehash.hash(name) + const contract = new EthContract(eth) + const Registrar = contract(registrarAbi).at(getRegistrar(provider.type)) + return new Promise((resolve, reject) => { + if (provider.type === 'mainnet' || provider.type === 'ropsten') { + Registrar.resolver(hash).then((address) => { + if (address === '0x0000000000000000000000000000000000000000') { + reject(null) + } else { + const Resolver = contract(resolverAbi).at(address['0']) + return Resolver.content(hash) + } + }).then((contentHash) => { + if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null) + if (contentHash.ret !== '0x') { + const hex = contentHash['0'].substring(2) + const buf = multihash.fromHexString(hex) + resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256'))) + } else { + reject(null) + } + }) + } else { + return reject('unsupport') + } + }) +} + +function getProvider (type) { + switch (type) { + case 'mainnet': + return 'https://mainnet.infura.io/' + case 'ropsten': + return 'https://ropsten.infura.io/' + default: + return 'http://localhost:8545/' + } +} + +function getRegistrar (type) { + switch (type) { + case 'mainnet': + return '0x314159265dd8dbb310642f98f50c066173c1259b' + case 'ropsten': + return '0x112234455c3a32fd11230c42e7bccd4a84e02010' + default: + return '0x0000000000000000000000000000000000000000' + } +} + +module.exports.resolve = function (name, provider) { + const path = name.split('.') + const topLevelDomain = path[path.length - 1] + if (topLevelDomain === 'eth' || topLevelDomain === 'test') { + return ens(name, provider) + } else { + return new Promise((resolve, reject) => { + reject(null) + }) + } +} diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js index 02690a948..fd3b93fc4 100644 --- a/app/scripts/lib/setupMetamaskMeshMetrics.js +++ b/app/scripts/lib/setupMetamaskMeshMetrics.js @@ -4,7 +4,7 @@ module.exports = setupMetamaskMeshMetrics /** * Injects an iframe into the current document for testing */ -function setupMetamaskMeshMetrics() { +function setupMetamaskMeshMetrics () { const testingContainer = document.createElement('iframe') testingContainer.src = 'https://metamask.github.io/mesh-testing/' console.log('Injecting MetaMask Mesh testing client') diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 77aefb00a..e657e278f 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -7,9 +7,11 @@ const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' module.exports = setupRaven // Setup raven / sentry remote error reporting -function setupRaven(opts) { - const { release } = opts +function setupRaven (opts) { + const { releaseVersion } = opts let ravenTarget + // detect brave + const isBrave = Boolean(window.chrome.ipcRenderer) if (METAMASK_DEBUG) { console.log('Setting up Sentry Remote Error Reporting: DEV') @@ -20,9 +22,11 @@ function setupRaven(opts) { } const client = Raven.config(ravenTarget, { - release, - transport: function(opts) { + releaseVersion, + transport: function (opts) { + opts.data.extra.isBrave = isBrave const report = opts.data + try { // handle error-like non-error exceptions rewriteErrorLikeExceptions(report) @@ -42,7 +46,7 @@ function setupRaven(opts) { return Raven } -function rewriteErrorLikeExceptions(report) { +function rewriteErrorLikeExceptions (report) { // handle errors that lost their error-ness in serialization (e.g. dnode) rewriteErrorMessages(report, (errorMessage) => { if (!errorMessage.includes('Non-Error exception captured with keys:')) return errorMessage @@ -51,7 +55,7 @@ function rewriteErrorLikeExceptions(report) { }) } -function simplifyErrorMessages(report) { +function simplifyErrorMessages (report) { rewriteErrorMessages(report, (errorMessage) => { // simplify ethjs error messages errorMessage = extractEthjsErrorMessage(errorMessage) @@ -64,7 +68,7 @@ function simplifyErrorMessages(report) { }) } -function rewriteErrorMessages(report, rewriteFn) { +function rewriteErrorMessages (report, rewriteFn) { // rewrite top level message if (report.message) report.message = rewriteFn(report.message) // rewrite each exception message @@ -75,7 +79,7 @@ function rewriteErrorMessages(report, rewriteFn) { } } -function rewriteReportUrls(report) { +function rewriteReportUrls (report) { // update request url report.request.url = toMetamaskUrl(report.request.url) // update exception stack trace @@ -88,7 +92,7 @@ function rewriteReportUrls(report) { } } -function toMetamaskUrl(origUrl) { +function toMetamaskUrl (origUrl) { const filePath = origUrl.split(location.origin)[1] if (!filePath) return origUrl const metamaskUrl = `metamask${filePath}` diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 431d1e59c..51e9036cc 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -28,7 +28,7 @@ function getStack () { * */ const getEnvironmentType = (url = window.location.href) => { - if (url.match(/popup.html(?:\?.+)*$/)) { + if (url.match(/popup.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_POPUP } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { return ENVIRONMENT_TYPE_FULLSCREEN diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d5627a0d1..e843ec660 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -35,6 +35,7 @@ const TypedMessageManager = require('./lib/typed-message-manager') 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') @@ -47,6 +48,7 @@ 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') module.exports = class MetamaskController extends EventEmitter { @@ -125,7 +127,9 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt + const additionalKeyrings = [TrezorKeyring] this.keyringController = new KeyringController({ + keyringTypes: additionalKeyrings, initState: initState.KeyringController, getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, @@ -145,6 +149,13 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker.syncWithAddresses(addresses) }) + // detect tokens controller + this.detectTokensController = new DetectTokensController({ + preferences: this.preferencesController, + network: this.networkController, + keyringMemStore: this.keyringController.memStore, + }) + // address book controller this.addressBookController = new AddressBookController({ initState: initState.AddressBookController, @@ -165,6 +176,13 @@ module.exports = class MetamaskController extends EventEmitter { }) this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) + this.txController.on(`tx:status-update`, (txId, status) => { + if (status === 'confirmed' || status === 'failed') { + const txMeta = this.txController.txStateManager.getTx(txId) + this.platform.showTransactionNotification(txMeta) + } + }) + // computed balances (accounting for pending transactions) this.balancesController = new BalancesController({ accountTracker: this.accountTracker, @@ -339,6 +357,7 @@ module.exports = class MetamaskController extends EventEmitter { markAccountsFound: this.markAccountsFound.bind(this), markPasswordForgotten: this.markPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), + getGasPrice: (cb) => cb(null, this.getGasPrice()), // coinbase buyEth: this.buyEth.bind(this), @@ -351,8 +370,17 @@ module.exports = class MetamaskController extends EventEmitter { verifySeedPhrase: nodeify(this.verifySeedPhrase, this), clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), + removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), + // hardware wallets + connectHardware: nodeify(this.connectHardware, this), + forgetDevice: nodeify(this.forgetDevice, this), + checkHardwareStatus: nodeify(this.checkHardwareStatus, this), + + // TREZOR + unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + // vault management submitPassword: nodeify(this.submitPassword, this), @@ -406,7 +434,6 @@ module.exports = class MetamaskController extends EventEmitter { } - //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= @@ -509,6 +536,127 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController.setSelectedAddress(address) } + // + // Hardware + // + + /** + * 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') + } + let accounts = [] + + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() + } + + // Merge with existing accounts + // and make sure addresses are not repeated + 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') + } + } + + /** + * Check if the device is unlocked + * + * @returns {Promise} + */ + async checkHardwareStatus (deviceName) { + + switch (deviceName) { + case 'trezor': + const keyringController = this.keyringController + const keyring = await keyringController.getKeyringsByType( + 'Trezor Hardware' + )[0] + if (!keyring) { + return false + } + return keyring.isUnlocked() + default: + throw new Error('MetamaskController:checkHardwareStatus - Unknown device') + } + } + + /** + * Clear + * + * @returns {Promise} + */ + 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') + } + keyring.forgetDevice() + return true + default: + throw new Error('MetamaskController:forgetDevice - Unknown device') + } + } + + /** + * Imports an account from a trezor device. + * + * @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') + } + + keyring.setAccountToUnlock(index) + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(keyring) + const newAccounts = await keyringController.getAccounts() + this.preferencesController.setAddresses(newAccounts) + newAccounts.forEach(address => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setSelectedAddress(address) + } + }) + + const { identities } = this.preferencesController.store.getState() + return { ...keyState, identities } + } + + // // Account Management // @@ -621,6 +769,23 @@ module.exports = class MetamaskController extends EventEmitter { return selectedAddress } + /** + * Removes an account from state / storage. + * + * @param {string[]} address A hex address + * + */ + async removeAccount (address) { + // Remove account from the preferences controller + this.preferencesController.removeAddress(address) + // Remove account from the account tracker controller + this.accountTracker.removeAccount(address) + // Remove account from the keyring + await this.keyringController.removeAccount(address) + return address + } + + /** * Imports an account with the specified import strategy. * These are defined in app/scripts/account-import-strategies @@ -963,7 +1128,7 @@ module.exports = class MetamaskController extends EventEmitter { * Allows a user to begin the seed phrase recovery process. * @param {Function} cb - A callback function called when complete. */ - markPasswordForgotten(cb) { + markPasswordForgotten (cb) { this.configManager.setPasswordForgotten(true) this.sendUpdate() cb() @@ -973,7 +1138,7 @@ module.exports = class MetamaskController extends EventEmitter { * Allows a user to end the seed phrase recovery process. * @param {Function} cb - A callback function called when complete. */ - unMarkPasswordForgotten(cb) { + unMarkPasswordForgotten (cb) { this.configManager.setPasswordForgotten(false) this.sendUpdate() cb() @@ -1269,10 +1434,12 @@ module.exports = class MetamaskController extends EventEmitter { set isClientOpen (open) { this._isClientOpen = open this.isClientOpenAndUnlocked = this.getState().isUnlocked && open + this.detectTokensController.isOpen = open } /** - * A method for activating the retrieval of price data, which should only be fetched when the UI is visible. + * A method for activating the retrieval of price data and auto detect tokens, + * which should only be fetched when the UI is visible. * @private * @param {boolean} active - True if price data should be getting fetched. */ diff --git a/app/scripts/migrations/013.js b/app/scripts/migrations/013.js index 15a9b28d4..fb7131f8e 100644 --- a/app/scripts/migrations/013.js +++ b/app/scripts/migrations/013.js @@ -28,7 +28,7 @@ module.exports = { function transformState (state) { const newState = state const { config } = newState - if ( config && config.provider ) { + if (config && config.provider) { if (config.provider.type === 'testnet') { newState.config.provider.type = 'ropsten' } diff --git a/app/scripts/migrations/023.js b/app/scripts/migrations/023.js index 151496b06..18493a789 100644 --- a/app/scripts/migrations/023.js +++ b/app/scripts/migrations/023.js @@ -35,10 +35,10 @@ function transformState (state) { if (transactions.length <= 40) return newState - let reverseTxList = transactions.reverse() + const reverseTxList = transactions.reverse() let stripping = true while (reverseTxList.length > 40 && stripping) { - let txIndex = reverseTxList.findIndex((txMeta) => { + const txIndex = reverseTxList.findIndex((txMeta) => { return (txMeta.status === 'failed' || txMeta.status === 'rejected' || txMeta.status === 'confirmed' || diff --git a/app/scripts/migrations/027.js b/app/scripts/migrations/027.js new file mode 100644 index 000000000..d6ebef580 --- /dev/null +++ b/app/scripts/migrations/027.js @@ -0,0 +1,35 @@ +// next version number +const version = 27 + +/* + +normalizes txParams on unconfirmed txs + +*/ +const clone = require('clone') + +module.exports = { + version, + + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + return versionedData + }, +} + +function transformState (state) { + const newState = state + + if (newState.TransactionController) { + if (newState.TransactionController.transactions) { + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected') + } + } + + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 04d90bfff..bd0005221 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -37,4 +37,5 @@ module.exports = [ require('./024'), require('./025'), require('./026'), + require('./027'), ] diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index f5cc255d1..0803164e8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,4 +1,5 @@ const extension = require('extensionizer') +const explorerLink = require('etherscan-link').createExplorerLink class ExtensionPlatform { @@ -13,12 +14,21 @@ class ExtensionPlatform { extension.tabs.create({ url }) } + closeCurrentWindow () { + return extension.windows.getCurrent((windowDetails) => { + return extension.windows.remove(windowDetails.id) + }) + } + getVersion () { return extension.runtime.getManifest().version } - openExtensionInBrowser () { - const extensionURL = extension.runtime.getURL('home.html') + openExtensionInBrowser (route = null) { + let extensionURL = extension.runtime.getURL('home.html') + if (route) { + extensionURL += `#${route}` + } this.openWindow({ url: extensionURL }) } @@ -31,6 +41,59 @@ class ExtensionPlatform { cb(e) } } + + showTransactionNotification (txMeta) { + + const status = txMeta.status + if (status === 'confirmed') { + this._showConfirmedTransaction(txMeta) + } else if (status === 'failed') { + this._showFailedTransaction(txMeta) + } + } + + _showConfirmedTransaction (txMeta) { + + this._subscribeToNotificationClicked() + + const url = explorerLink(txMeta.hash, parseInt(txMeta.metamaskNetworkId)) + const nonce = parseInt(txMeta.txParams.nonce, 16) + + const title = 'Confirmed transaction' + const message = `Transaction ${nonce} confirmed! View on EtherScan` + this._showNotification(title, message, url) + } + + _showFailedTransaction (txMeta) { + + const nonce = parseInt(txMeta.txParams.nonce, 16) + const title = 'Failed transaction' + const message = `Transaction ${nonce} failed! ${txMeta.err.message}` + this._showNotification(title, message) + } + + _showNotification (title, message, url) { + extension.notifications.create( + url, + { + 'type': 'basic', + 'title': title, + 'iconUrl': extension.extension.getURL('../../images/icon-64.png'), + 'message': message, + }) + } + + _subscribeToNotificationClicked () { + if (!extension.notifications.onClicked.hasListener(this._viewOnEtherScan)) { + extension.notifications.onClicked.addListener(this._viewOnEtherScan) + } + } + + _viewOnEtherScan (txId) { + if (txId.startsWith('http://')) { + global.metamaskController.platform.openWindow({ url: txId }) + } + } } module.exports = ExtensionPlatform diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 6325b8a8d..db885ec93 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -12,7 +12,7 @@ module.exports = initializePopup /** * Asynchronously initializes the MetaMask popup UI * - * @param {{ container: Element, connectionStream: * }} config Popup configuration object + * @param {{ container: Element, connectionStream: * }} config Popup configuration object * @param {Function} cb Called when initialization is complete */ function initializePopup ({ container, connectionStream }, cb) { diff --git a/app/scripts/ui.js b/app/scripts/ui.js index bdab29c1e..da100f928 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -14,7 +14,7 @@ const log = require('loglevel') start().catch(log.error) -async function start() { +async function start () { // create platform global global.platform = new ExtensionPlatform() @@ -64,7 +64,6 @@ async function start() { css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss() deleteInjectedCss = injectCss(css) } - if (state.appState.shouldClose) notificationManager.closePopup() }) }) diff --git a/app/unsupport.html b/app/unsupport.html new file mode 100644 index 000000000..6f514eb17 --- /dev/null +++ b/app/unsupport.html @@ -0,0 +1,59 @@ + + + + + MetaMask + + + +
+ +

ENS resolver only support on Ethereum mainnet

+
+ + \ No newline at end of file diff --git a/development/announcer.js b/development/announcer.js index e97ea65b6..ea1bfdd36 100644 --- a/development/announcer.js +++ b/development/announcer.js @@ -7,6 +7,6 @@ var changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toSt var log = changelog.split(version)[1].split('##')[0].trim() -let msg = `*MetaMask ${version}* now published! It should auto-update soon!\n${log}` +const msg = `*MetaMask ${version}* now published! It should auto-update soon!\n${log}` console.log(msg) diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js index ffbe49d4d..aee68854b 100644 --- a/development/backGroundConnectionModifiers.js +++ b/development/backGroundConnectionModifiers.js @@ -1,5 +1,5 @@ module.exports = { - "confirm sig requests": { + 'confirm sig requests': { signMessage: (msgData, cb) => { const stateUpdate = { unapprovedMsgs: {}, @@ -10,7 +10,7 @@ module.exports = { signPersonalMessage: (msgData, cb) => { const stateUpdate = { unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgsCount: 0, + unapprovedPersonalMsgCount: 0, } return cb(null, stateUpdate) }, diff --git a/development/beefy.js b/development/beefy.js index 9eff94320..c1c436cb6 100644 --- a/development/beefy.js +++ b/development/beefy.js @@ -1,17 +1,14 @@ const beefy = require('beefy') const http = require('http') -const fs = require('fs') -const path = require('path') - const port = 8124 const handler = beefy({ - entries: {'mocker.js': 'bundle.js'} - , cwd: __dirname - , live: true - , open: true - , quiet: false - , bundlerFlags: ['-t', 'brfs'] + entries: {'mocker.js': 'bundle.js'}, + cwd: __dirname, + live: true, + open: true, + quiet: false, + bundlerFlags: ['-t', 'brfs'], }) diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 88614ca5c..96b5572fe 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -4,7 +4,7 @@ const VERSION = require('../dist/chrome/manifest.json').version start().catch(console.error) -async function start() { +async function start () { const GITHUB_COMMENT_TOKEN = process.env.GITHUB_COMMENT_TOKEN const CIRCLE_PULL_REQUEST = process.env.CIRCLE_PULL_REQUEST @@ -20,7 +20,7 @@ async function start() { } const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop() - const SHORT_SHA1 = CIRCLE_SHA1.slice(0,7) + const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7) const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0` const MASCARA = `${BUILD_LINK_BASE}/builds/mascara/home.html` diff --git a/development/mock-dev.js b/development/mock-dev.js index f332633d5..15f6ad4bf 100644 --- a/development/mock-dev.js +++ b/development/mock-dev.js @@ -12,7 +12,6 @@ * To use, run `npm run mock`. */ -const extend = require('xtend') const render = require('react-dom').render const h = require('react-hyperscript') const Root = require('../ui/app/root') @@ -24,7 +23,6 @@ const Selector = require('./selector') const MetamaskController = require('../app/scripts/metamask-controller') const firstTimeState = require('../app/scripts/first-time-state') const ExtensionPlatform = require('../app/scripts/platforms/extension') -const extension = require('./mockExtension') const noop = function () {} const log = require('loglevel') @@ -81,14 +79,14 @@ const controller = new MetamaskController({ initState: firstTimeState, }) global.metamaskController = controller -global.platform = new ExtensionPlatform +global.platform = new ExtensionPlatform() // // User Interface // actions._setBackgroundConnection(controller.getApi()) -actions.update = function(stateName) { +actions.update = function (stateName) { selectedView = stateName updateQueryParams(stateName) const newState = states[selectedView] @@ -98,7 +96,7 @@ actions.update = function(stateName) { } } -function modifyBackgroundConnection(backgroundConnectionModifier) { +function modifyBackgroundConnection (backgroundConnectionModifier) { const modifiedBackgroundConnection = Object.assign({}, controller.getApi(), backgroundConnectionModifier) actions._setBackgroundConnection(modifiedBackgroundConnection) } @@ -112,7 +110,7 @@ var store = configureStore(firstState) // start app startApp() -function startApp(){ +function startApp () { const body = document.body const container = document.createElement('div') container.id = 'test-container' diff --git a/development/mockExtension.js b/development/mockExtension.js index ac03d965c..634d4263a 100644 --- a/development/mockExtension.js +++ b/development/mockExtension.js @@ -39,6 +39,6 @@ extension.runtime.reload = noop extension.tabs.create = noop extension.runtime.getManifest = function () { return { - version: 'development' + version: 'development', } -} \ No newline at end of file +} diff --git a/development/run-version-bump.js b/development/run-version-bump.js index 98757f58e..32fed1865 100644 --- a/development/run-version-bump.js +++ b/development/run-version-bump.js @@ -11,7 +11,7 @@ const bumpType = normalizeType(process.argv[2]) start().catch(console.error) -async function start() { +async function start () { const changeBuffer = await readFile(changelogPath) const changelog = changeBuffer.toString() diff --git a/development/selector.js b/development/selector.js index fd387df15..2673d0fe3 100644 --- a/development/selector.js +++ b/development/selector.js @@ -11,7 +11,7 @@ function NewComponent () { NewComponent.prototype.render = function () { const props = this.props - let { + const { states, selectedKey, actions, @@ -28,7 +28,7 @@ NewComponent.prototype.render = function () { margin: '20px 20px 0px', }, value: selected, - onChange:(event) => { + onChange: (event) => { const selectedKey = event.target.value const backgroundConnectionModifier = backGroundConnectionModifiers[selectedKey] modifyBackgroundConnection(backgroundConnectionModifier || {}) diff --git a/development/sentry-publish.js b/development/sentry-publish.js index ab3acabbd..7a6d55115 100644 --- a/development/sentry-publish.js +++ b/development/sentry-publish.js @@ -5,7 +5,7 @@ const VERSION = require('../dist/chrome/manifest.json').version start().catch(console.error) -async function start(){ +async function start () { const authWorked = await checkIfAuthWorks() if (!authWorked) { console.log(`Sentry auth failed...`) @@ -31,21 +31,21 @@ async function start(){ console.log('all done!') } -async function checkIfAuthWorks() { +async function checkIfAuthWorks () { const itWorked = await doesNotFail(async () => { await exec(`sentry-cli releases --org 'metamask' --project 'metamask' list`) }) return itWorked } -async function checkIfVersionExists() { +async function checkIfVersionExists () { const versionAlreadyExists = await doesNotFail(async () => { await exec(`sentry-cli releases --org 'metamask' --project 'metamask' info ${VERSION}`) }) return versionAlreadyExists } -async function doesNotFail(asyncFn) { +async function doesNotFail (asyncFn) { try { await asyncFn() return true diff --git a/development/sourcemap-validator.js b/development/sourcemap-validator.js index edc97667a..143888128 100644 --- a/development/sourcemap-validator.js +++ b/development/sourcemap-validator.js @@ -1,6 +1,6 @@ const fs = require('fs') const { SourceMapConsumer } = require('source-map') - +const path = require('path') // // Utility to help check if sourcemaps are working // @@ -11,9 +11,11 @@ const { SourceMapConsumer } = require('source-map') start() -async function start() { - const rawBuild = fs.readFileSync(__dirname + '/../dist/chrome/inpage.js', 'utf8') - const rawSourceMap = fs.readFileSync(__dirname + '/../dist/sourcemaps/inpage.js.map', 'utf8') + +async function start () { + const rawBuild = fs.readFileSync(path.join(__dirname, '/../dist/chrome/', 'inpage.js') + , 'utf8') + const rawSourceMap = fs.readFileSync(path.join(__dirname, '/../dist/sourcemaps/', 'inpage.js.map'), 'utf8') const consumer = await new SourceMapConsumer(rawSourceMap) console.log('hasContentsOfAllSources:', consumer.hasContentsOfAllSources(), '\n') @@ -34,7 +36,7 @@ async function start() { if (result.source === 'node_modules/web3/dist/web3.min.js') return // minified mess const sourceContent = consumer.sourceContentFor(result.source) const sourceLines = sourceContent.split('\n') - const line = sourceLines[result.line-1] + const line = sourceLines[result.line - 1] console.log(`\n========================== ${result.source} ====================================\n`) console.log(line) console.log(`\n==============================================================================\n`) @@ -42,8 +44,9 @@ async function start() { }) } -function indicesOf(substring, string) { - var a=[],i=-1; - while((i=string.indexOf(substring,i+1)) >= 0) a.push(i); - return a; +function indicesOf (substring, string) { + var a = [] + var i = -1 + while ((i = string.indexOf(substring, i + 1)) >= 0) a.push(i) + return a } diff --git a/development/states/conf-tx.json b/development/states/conf-tx.json index 57f667cb9..d47b26fd4 100644 --- a/development/states/conf-tx.json +++ b/development/states/conf-tx.json @@ -56,7 +56,7 @@ "read": true, "date": "Thu Feb 09 2017", "title": "Terms of Use", - "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n", + "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n", "id": 0 }, "network": "3", diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json index 71ccbd96c..2c2e17704 100644 --- a/development/states/confirm-new-ui.json +++ b/development/states/confirm-new-ui.json @@ -156,5 +156,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index 3c9caafb0..829f513a8 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -73,7 +73,7 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658" }, "status": "unapproved", - "time": 1537889069339, + "time": 1537889070000, "type": "eth_sign" } }, @@ -86,11 +86,11 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" }, "status": "unapproved", - "time": 1517889069339, + "time": 1537889065000, "type": "personal_sign" } }, - "unapprovedPersonalMsgCount": 0, + "unapprovedPersonalMsgCount": 1 , "unapprovedTypedMessages": { "8997167822566869": { "id": 8997167822566869, @@ -102,7 +102,7 @@ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" }, "status": "unapproved", - "time": 1617889069339, + "time": 1537889060000, "type": "eth_signTypedData" } }, @@ -172,5 +172,32 @@ "scrollToBottom": false, "forgottenPassword": null }, - "identities": {} + "identities": {}, + "confirmTransaction": { + "txData": { + "id": 8927167822566864, + "msgParams": { + "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0", + "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658" + }, + "status": "unapproved", + "time": 1537889069339, + "type": "eth_sign" + }, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 8c8b18a91..6848c0840 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -130,5 +130,23 @@ "scrollToBottom": false, "forgottenPassword": null }, - "identities": {} + "identities": {}, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/first-time.json b/development/states/first-time.json index f10eefd35..f44148973 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -16,7 +16,7 @@ "read": false, "date": "Thu Feb 09 2017", "title": "Terms of Use", - "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n", + "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n", "id": 0 }, "network": "3", @@ -58,5 +58,23 @@ } }, "identities": {}, - "computedBalances": {} + "computedBalances": {}, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false + } } diff --git a/development/states/send-edit.json b/development/states/send-edit.json index b05acbf3b..8e5c25a82 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -156,5 +156,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index b457749ee..ad2ff3d6e 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -135,5 +135,23 @@ "fromDropdownOpen": false, "toDropdownOpen": false, "errors": {} + }, + "confirmTransaction": { + "txData": {}, + "tokenData": {}, + "methodData": {}, + "tokenProps": { + "tokenDecimals": "", + "tokenSymbol": "" + }, + "fiatTransactionAmount": "", + "fiatTransactionFee": "", + "fiatTransactionTotal": "", + "ethTransactionAmount": "", + "ethTransactionFee": "", + "ethTransactionTotal": "", + "hexGasTotal": "", + "nonce": "", + "fetchingMethodData": false } } diff --git a/development/ui-dev.js b/development/ui-dev.js index cac433909..bae0ce50e 100644 --- a/development/ui-dev.js +++ b/development/ui-dev.js @@ -29,9 +29,8 @@ log.setDefaultLevel(1) // Query String const qs = require('qs') -let queryString = qs.parse(window.location.href.split('#')[1]) +const queryString = qs.parse(window.location.href.split('#')[1]) let selectedView = queryString.view || 'first time' -const firstState = states[selectedView] updateQueryParams(selectedView) // CSS @@ -39,15 +38,15 @@ const MetaMaskUiCss = require('../ui/css') const injectCss = require('inject-css') -function updateQueryParams(newView) { +function updateQueryParams (newView) { queryString.view = newView const params = qs.stringify(queryString) window.location.href = window.location.href.split('#')[0] + `#${params}` } const actions = { - _setBackgroundConnection(){}, - update: function(stateName) { + _setBackgroundConnection () {}, + update: function (stateName) { selectedView = stateName updateQueryParams(stateName) const newState = states[selectedView] @@ -67,7 +66,7 @@ var store = configureStore(states[selectedView]) // start app startApp() -function startApp(){ +function startApp () { const body = document.body const container = document.createElement('div') container.id = 'test-container' diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js index 8dc0a30f1..0eef2b35d 100644 --- a/development/verify-locale-strings.js +++ b/development/verify-locale-strings.js @@ -1,4 +1,4 @@ -//////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////// // // Locale verification script // @@ -8,7 +8,7 @@ // // will check the given locale against the strings in english // -//////////////////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////////////////// const fs = require('fs') const path = require('path') @@ -20,7 +20,7 @@ const specifiedLocale = process.argv[2] if (specifiedLocale) { console.log(`Verifying selected locale "${specifiedLocale}":\n\n`) const locale = localeIndex.find(localeMeta => localeMeta.code === specifiedLocale) - verifyLocale({ localeMeta }) + verifyLocale({ locale }) } else { console.log('Verifying all locales:\n\n') localeIndex.forEach(localeMeta => { @@ -30,16 +30,16 @@ if (specifiedLocale) { } - -function verifyLocale({ localeMeta }) { +function verifyLocale ({ localeMeta }) { const localeCode = localeMeta.code const localeName = localeMeta.name + let targetLocale, englishLocale try { const localeFilePath = path.join(process.cwd(), 'app', '_locales', localeCode, 'messages.json') - targetLocale = JSON.parse(fs.readFileSync(localeFilePath, 'utf8')); + targetLocale = JSON.parse(fs.readFileSync(localeFilePath, 'utf8')) } catch (e) { - if (e.code == 'ENOENT') { + if (e.code === 'ENOENT') { console.log('Locale file not found') } else { console.log(`Error opening your locale ("${localeCode}") file: `, e) @@ -49,9 +49,9 @@ function verifyLocale({ localeMeta }) { try { const englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json') - englishLocale = JSON.parse(fs.readFileSync(englishFilePath, 'utf8')); + englishLocale = JSON.parse(fs.readFileSync(englishFilePath, 'utf8')) } catch (e) { - if(e.code == 'ENOENT') { + if (e.code === 'ENOENT') { console.log('English File not found') } else { console.log('Error opening english locale file: ', e) @@ -71,7 +71,7 @@ function verifyLocale({ localeMeta }) { if (extraItems.length) { console.log('\nMissing from english locale:') - extraItems.forEach(function(key) { + extraItems.forEach(function (key) { console.log(` - [ ] ${key}`) }) } else { @@ -80,7 +80,7 @@ function verifyLocale({ localeMeta }) { if (missingItems.length) { console.log(`\nMissing:`) - missingItems.forEach(function(key) { + missingItems.forEach(function (key) { console.log(` - [ ] ${key}`) }) } else { @@ -92,6 +92,6 @@ function verifyLocale({ localeMeta }) { } } -function compareLocalesForMissingItems({ base, subject }) { +function compareLocalesForMissingItems ({ base, subject }) { return Object.keys(base).filter((key) => !subject[key]) } diff --git a/development/version-bump.js b/development/version-bump.js index bedf87c01..fa20e9f75 100644 --- a/development/version-bump.js +++ b/development/version-bump.js @@ -1,6 +1,6 @@ const clone = require('clone') -async function versionBump(bumpType, changelog, oldManifest) { +async function versionBump (bumpType, changelog, oldManifest) { const manifest = clone(oldManifest) const newVersion = newVersionFrom(manifest, bumpType) @@ -19,13 +19,13 @@ async function versionBump(bumpType, changelog, oldManifest) { return { version: newVersion, manifest: manifest, - changelog: logLines.join('\n') + changelog: logLines.join('\n'), } } function newVersionFrom (manifest, bumpType) { const string = manifest.version - let segments = string.split('.').map((str) => parseInt(str)) + const segments = string.split('.').map((str) => parseInt(str)) switch (bumpType) { case 'major': @@ -45,8 +45,4 @@ function newVersionFrom (manifest, bumpType) { return segments.map(String).join('.') } -function bumpManifest (manifest, bumpType) { - -} - module.exports = versionBump diff --git a/docs/trezor-emulator.md b/docs/trezor-emulator.md new file mode 100644 index 000000000..8f66ba213 --- /dev/null +++ b/docs/trezor-emulator.md @@ -0,0 +1,25 @@ +# Using the TREZOR simulator + +You can install the TREZOR emulator and use it with Metamask. +Here is how: + +## 1 - Install the TREZOR Bridge + +Download the corresponding bridge for your platform from [this url](https://wallet.trezor.io/data/bridge/latest/index.html) + +## 2 - Download and build the simulator + +Follow this instructions: https://github.com/trezor/trezor-core/blob/master/docs/build.md + +## 3 - Restart the bridge with emulator support (Mac OSx instructions) + +` + # stop any existing instance of trezord + killall trezord + + # start the bridge for the simulator + /Applications/Utilities/TREZOR\ Bridge/trezord -e 21324 >> /dev/null 2>&1 & + + # launch the emulator + ~/trezor-core/emu.sh +` diff --git a/gentests.js b/gentests.js index 08232e10f..9c591e98c 100644 --- a/gentests.js +++ b/gentests.js @@ -1,6 +1,6 @@ const fs = require('fs') -const async = require('async') const path = require('path') +const async = require('async') const promisify = require('pify') // start(/\.selectors.js/, generateSelectorTest).catch(console.error) @@ -8,7 +8,6 @@ const promisify = require('pify') startContainer(/\.container.js/, generateContainerTest).catch(console.error) async function getAllFileNames (dirName) { - const rootPath = path.join(__dirname, dirName) const allNames = (await promisify(fs.readdir)(dirName)) const fileNames = allNames.filter(name => name.match(/^.+\./)) const dirNames = allNames.filter(name => name.match(/^[^.]+$/)) @@ -16,7 +15,7 @@ async function getAllFileNames (dirName) { const fullPathDirNames = dirNames.map(d => `${dirName}/${d}`) const subNameArrays = await promisify(async.map)(fullPathDirNames, getAllFileNames) let subNames = [] - subNameArrays.forEach(subNameArray => subNames = [...subNames, ...subNameArray]) + subNameArrays.forEach(subNameArray => { subNames = [...subNames, ...subNameArray] }) return [ ...fileNames.map(name => dirName + '/' + name), @@ -24,14 +23,15 @@ async function getAllFileNames (dirName) { ] } +/* async function start (fileRegEx, testGenerator) { const fileNames = await getAllFileNames('./ui/app') const sFiles = fileNames.filter(name => name.match(fileRegEx)) - + let sFileMethodNames let testFilePath async.each(sFiles, async (sFile, cb) => { - let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) + const [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) sFileMethodNames = Object.keys(require(__dirname + '/' + sFile)) testFilePath = sPath.replace('.', '-').replace('.', '.test.') @@ -44,92 +44,96 @@ async function start (fileRegEx, testGenerator) { }, (err) => { console.log(err) }) - + } +*/ async function startContainer (fileRegEx, testGenerator) { const fileNames = await getAllFileNames('./ui/app') const sFiles = fileNames.filter(name => name.match(fileRegEx)) - - let sFileMethodNames + async.each(sFiles, async (sFile, cb) => { - console.log(`sFile`, sFile); - let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) - - let testFilePath = sPath.replace('.', '-').replace('.', '.test.') + console.log(`sFile`, sFile) + const [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) + + const testFilePath = sPath.replace('.', '-').replace('.', '.test.') await promisify(fs.readFile)( - __dirname + '/' + sFile, + path.join(__dirname, sFile), 'utf8', async (err, result) => { - console.log(`result`, result.length); - const returnObjectStrings = result - .match(/return\s(\{[\s\S]+?})\n}/g) - .map(str => { - return str - .slice(0, str.length - 1) - .slice(7) - .replace(/\n/g, '') - .replace(/\s\s+/g, ' ') - - }) - const mapStateToPropsAssertionObject = returnObjectStrings[0] - .replace(/\w+:\s\w+\([\w,\s]+\),/g, str => { - const strKey = str.match(/^\w+/)[0] - return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n' - }) - .replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`) - .replace(/\w+:.+,/g, s => ` ${s}`) - .replace(/}/g, s => ` ${s}`) - let mapDispatchToPropsMethodNames - if (returnObjectStrings[1]) { - mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0]) - } - const proxyquireObject = ('{\n ' + result - .match(/import\s{[\s\S]+?}\sfrom\s.+/g) - .map(s => s.replace(/\n/g, '')) - .map((s, i) => { - const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g) - return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1 - ? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n ' - : proxyKeys[0] + ': () => {},') + ' }' - }) - .join(',\n ') + '\n}') - .replace('{ connect: () => {}, },', `{ - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - },`) - // console.log(`proxyquireObject`, proxyquireObject); - // console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject); - // console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames); + if (err) { + console.log('Error: ', err) + } else { + console.log(`result`, result.length) + const returnObjectStrings = result + .match(/return\s(\{[\s\S]+?})\n}/g) + .map(str => { + return str + .slice(0, str.length - 1) + .slice(7) + .replace(/\n/g, '') + .replace(/\s\s+/g, ' ') - const containerTest = generateContainerTest(sPath, { - mapStateToPropsAssertionObject, - mapDispatchToPropsMethodNames, - proxyquireObject, - }) - // console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest); - console.log('----') - console.log(`sRootPath`, sRootPath); - console.log(`testFilePath`, testFilePath); - await promisify(fs.writeFile)( - `${__dirname}/${sRootPath}tests/${testFilePath}`, - containerTest, - 'utf8' - ) + }) + const mapStateToPropsAssertionObject = returnObjectStrings[0] + .replace(/\w+:\s\w+\([\w,\s]+\),/g, str => { + const strKey = str.match(/^\w+/)[0] + return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n' + }) + .replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`) + .replace(/\w+:.+,/g, s => ` ${s}`) + .replace(/}/g, s => ` ${s}`) + let mapDispatchToPropsMethodNames + if (returnObjectStrings[1]) { + mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0]) + } + const proxyquireObject = ('{\n ' + result + .match(/import\s{[\s\S]+?}\sfrom\s.+/g) + .map(s => s.replace(/\n/g, '')) + .map((s, i) => { + const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g) + return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1 + ? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n ' + : proxyKeys[0] + ': () => {},') + ' }' + }) + .join(',\n ') + '\n}') + .replace('{ connect: () => {}, },', `{ + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + },`) + // console.log(`proxyquireObject`, proxyquireObject); + // console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject); + // console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames); + + const containerTest = generateContainerTest(sPath, { + mapStateToPropsAssertionObject, + mapDispatchToPropsMethodNames, + proxyquireObject, + }) + // console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest); + console.log('----') + console.log(`sRootPath`, sRootPath) + console.log(`testFilePath`, testFilePath) + await promisify(fs.writeFile)( + `${__dirname}/${sRootPath}tests/${testFilePath}`, + containerTest, + 'utf8' + ) + } } ) }, (err) => { console.log('123', err) }) - -} +} +/* function generateMethodList (methodArray) { - return methodArray.map(n => ' ' + n).join(',\n') + ',' + return methodArray.map(n => ' ' + n).join(',\n') + ',' } function generateMethodDescribeBlock (methodName, index) { @@ -143,7 +147,7 @@ function generateMethodDescribeBlock (methodName, index) { })` return describeBlock } - +*/ function generateDispatchMethodDescribeBlock (methodName, index) { const describeBlock = `${index ? ' ' : ''}describe('${methodName}()', () => { @@ -154,12 +158,13 @@ function generateDispatchMethodDescribeBlock (methodName, index) { })` return describeBlock } - +/* function generateMethodDescribeBlocks (methodArray) { return methodArray .map((methodName, index) => generateMethodDescribeBlock(methodName, index)) .join('\n\n') } +*/ function generateDispatchMethodDescribeBlocks (methodArray) { return methodArray @@ -167,6 +172,7 @@ function generateDispatchMethodDescribeBlocks (methodArray) { .join('\n\n') } +/* function generateSelectorTest (name, methodArray) { return `import assert from 'assert' import { @@ -192,6 +198,7 @@ describe('${name.match(/^[^.]+/)} utils', () => { })` } +*/ function generateContainerTest (sPath, { mapStateToPropsAssertionObject, @@ -231,4 +238,4 @@ describe('${sPath.match(/^[^.]+/)} container', () => { }) })` -} \ No newline at end of file +} diff --git a/gulpfile.js b/gulpfile.js index 4dca7089f..480f544d8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -13,27 +13,21 @@ const zip = require('gulp-zip') const assign = require('lodash.assign') const livereload = require('gulp-livereload') const del = require('del') -const eslint = require('gulp-eslint') const fs = require('fs') const path = require('path') const manifest = require('./app/manifest.json') -const replace = require('gulp-replace') const mkdirp = require('mkdirp') -const asyncEach = require('async/each') -const exec = require('child_process').exec const sass = require('gulp-sass') const autoprefixer = require('gulp-autoprefixer') const gulpStylelint = require('gulp-stylelint') const stylefmt = require('gulp-stylefmt') const uglify = require('gulp-uglify-es').default -const babel = require('gulp-babel') -const debug = require('gulp-debug') const pify = require('pify') const gulpMultiProcess = require('gulp-multi-process') const endOfStream = pify(require('end-of-stream')) function gulpParallel (...args) { - return function spawnGulpChildProcess(cb) { + return function spawnGulpChildProcess (cb) { return gulpMultiProcess(args, cb, true) } } @@ -48,12 +42,12 @@ const commonPlatforms = [ // browser webapp 'mascara', // browser extensions - ...browserPlatforms + ...browserPlatforms, ] // browser reload -gulp.task('dev:reload', function() { +gulp.task('dev:reload', function () { livereload.listen({ port: 35729, }) @@ -108,7 +102,7 @@ createCopyTasks('html:mascara', { destinations: [`./dist/mascara/`], }) -function createCopyTasks(label, opts) { +function createCopyTasks (label, opts) { if (!opts.devOnly) { const copyTaskName = `copy:${label}` copyTask(copyTaskName, opts) @@ -119,7 +113,7 @@ function createCopyTasks(label, opts) { copyDevTaskNames.push(copyDevTaskName) } -function copyTask(taskName, opts){ +function copyTask (taskName, opts) { const source = opts.source const destination = opts.destination const destinations = opts.destinations || [destination] @@ -137,12 +131,12 @@ function copyTask(taskName, opts){ return performCopy() }) - function performCopy() { + function performCopy () { // stream from source let stream = gulp.src(source + pattern, { base: source }) // copy to destinations - destinations.forEach(function(destination) { + destinations.forEach(function (destination) { stream = stream.pipe(gulp.dest(destination)) }) @@ -152,40 +146,40 @@ function copyTask(taskName, opts){ // manifest tinkering -gulp.task('manifest:chrome', function() { +gulp.task('manifest:chrome', function () { return gulp.src('./dist/chrome/manifest.json') - .pipe(jsoneditor(function(json) { + .pipe(jsoneditor(function (json) { delete json.applications return json })) .pipe(gulp.dest('./dist/chrome', { overwrite: true })) }) -gulp.task('manifest:opera', function() { +gulp.task('manifest:opera', function () { return gulp.src('./dist/opera/manifest.json') - .pipe(jsoneditor(function(json) { + .pipe(jsoneditor(function (json) { json.permissions = [ - "storage", - "tabs", - "clipboardWrite", - "clipboardRead", - "http://localhost:8545/" + 'storage', + 'tabs', + 'clipboardWrite', + 'clipboardRead', + 'http://localhost:8545/', ] return json })) .pipe(gulp.dest('./dist/opera', { overwrite: true })) }) -gulp.task('manifest:production', function() { +gulp.task('manifest:production', function () { return gulp.src([ './dist/firefox/manifest.json', './dist/chrome/manifest.json', './dist/edge/manifest.json', './dist/opera/manifest.json', - ],{base: './dist/'}) + ], {base: './dist/'}) // Exclude chromereload script in production: - .pipe(jsoneditor(function(json) { + .pipe(jsoneditor(function (json) { json.background.scripts = json.background.scripts.filter((script) => { return !script.includes('chromereload') }) @@ -212,29 +206,6 @@ gulp.task('dev:copy', ) ) -// lint js - -const lintTargets = ['app/**/*.json', 'app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'old-ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js'] - -gulp.task('lint', function () { - // Ignoring node_modules, dist/firefox, and docs folders: - return gulp.src(lintTargets) - .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) - // eslint.format() outputs the lint results to the console. - // Alternatively use eslint.formatEach() (see Docs). - .pipe(eslint.format()) - // To have the process exit with an error code (1) on - // lint error, return the stream and pipe to failAfterError last. - .pipe(eslint.failAfterError()) -}); - -gulp.task('lint:fix', function () { - return gulp.src(lintTargets) - .pipe(eslint(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true}))) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()) -}); - // scss compilation and autoprefixing tasks gulp.task('build:scss', createScssBuildTask({ @@ -250,7 +221,7 @@ gulp.task('dev:scss', createScssBuildTask({ pattern: 'ui/app/**/*.scss', })) -function createScssBuildTask({ src, dest, devMode, pattern }) { +function createScssBuildTask ({ src, dest, devMode, pattern }) { return function () { if (devMode) { watch(pattern, async (event) => { @@ -262,7 +233,7 @@ function createScssBuildTask({ src, dest, devMode, pattern }) { return buildScss() } - function buildScss() { + function buildScss () { return gulp.src(src) .pipe(sourcemaps.init()) .pipe(sass().on('error', sass.logError)) @@ -272,22 +243,22 @@ function createScssBuildTask({ src, dest, devMode, pattern }) { } } -gulp.task('lint-scss', function() { +gulp.task('lint-scss', function () { return gulp .src('ui/app/css/itcss/**/*.scss') .pipe(gulpStylelint({ reporters: [ - { formatter: 'string', console: true } + { formatter: 'string', console: true }, ], fix: true, - })); -}); + })) +}) gulp.task('fmt-scss', function () { return gulp.src('ui/app/css/itcss/**/*.scss') .pipe(stylefmt()) - .pipe(gulp.dest('ui/app/css/itcss')); -}); + .pipe(gulp.dest('ui/app/css/itcss')) +}) // build js @@ -300,11 +271,11 @@ const buildJsFiles = [ // bundle tasks createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:extension:js', devMode: true }) -createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' }) +createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' }) createTasksForBuildJsMascara({ taskPrefix: 'build:mascara:js' }) createTasksForBuildJsMascara({ taskPrefix: 'dev:mascara:js', devMode: true }) -function createTasksForBuildJsExtension({ buildJsFiles, taskPrefix, devMode, bundleTaskOpts = {} }) { +function createTasksForBuildJsExtension ({ buildJsFiles, taskPrefix, devMode, bundleTaskOpts = {} }) { // inpage must be built before all other scripts: const rootDir = './app/scripts' const nonInpageFiles = buildJsFiles.filter(file => file !== 'inpage') @@ -322,7 +293,7 @@ function createTasksForBuildJsExtension({ buildJsFiles, taskPrefix, devMode, bun createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1, buildPhase2 }) } -function createTasksForBuildJsMascara({ taskPrefix, devMode, bundleTaskOpts = {} }) { +function createTasksForBuildJsMascara ({ taskPrefix, devMode, bundleTaskOpts = {} }) { // inpage must be built before all other scripts: const rootDir = './mascara/src/' const buildPhase1 = ['ui', 'proxy', 'background', 'metamascara'] @@ -338,7 +309,7 @@ function createTasksForBuildJsMascara({ taskPrefix, devMode, bundleTaskOpts = {} createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 }) } -function createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) { +function createTasksForBuildJs ({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) { // bundle task for each file const jsFiles = [].concat(buildPhase1, buildPhase2) jsFiles.forEach((jsFile) => { @@ -367,7 +338,7 @@ gulp.task('disc', gulp.parallel(buildJsFiles.map(jsFile => `disc:${jsFile}`))) // clean dist -gulp.task('clean', function clean() { +gulp.task('clean', function clean () { return del(['./dist/*']) }) @@ -460,7 +431,7 @@ gulp.task('dist', // task generators -function zipTask(target) { +function zipTask (target) { return () => { return gulp.src(`dist/${target}/**`) .pipe(zip(`metamask-${target}-${manifest.version}.zip`)) @@ -468,7 +439,7 @@ function zipTask(target) { } } -function generateBundler(opts, performBundle) { +function generateBundler (opts, performBundle) { const browserifyOpts = assign({}, watchify.args, { entries: [opts.filepath], plugin: 'browserify-derequire', @@ -497,7 +468,7 @@ function generateBundler(opts, performBundle) { return bundler } -function discTask(opts) { +function discTask (opts) { opts = Object.assign({ buildWithFullPaths: true, }, opts) @@ -508,7 +479,7 @@ function discTask(opts) { return performBundle - function performBundle(){ + function performBundle () { // start "disc" build const discDir = path.join(__dirname, 'disc') mkdirp.sync(discDir) @@ -523,14 +494,14 @@ function discTask(opts) { } -function bundleTask(opts) { +function bundleTask (opts) { const bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', gutil.log) return performBundle - function performBundle(){ + function performBundle () { let buildStream = bundler.bundle() // handle errors @@ -562,7 +533,7 @@ function bundleTask(opts) { buildStream = buildStream .pipe(uglify({ mangle: { - reserved: [ 'MetamaskInpageProvider' ] + reserved: [ 'MetamaskInpageProvider' ], }, })) } diff --git a/mascara/example/app.js b/mascara/example/app.js index 598e2c84c..7b6421fdc 100644 --- a/mascara/example/app.js +++ b/mascara/example/app.js @@ -3,7 +3,7 @@ const EthQuery = require('ethjs-query') window.addEventListener('load', loadProvider) window.addEventListener('message', console.warn) -async function loadProvider() { +async function loadProvider () { const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' }) const ethQuery = new EthQuery(ethereumProvider) const accounts = await ethQuery.accounts() @@ -13,7 +13,7 @@ async function loadProvider() { } -function logToDom(message, context){ +function logToDom (message, context) { document.getElementById(context).innerText = message console.log(message) } @@ -35,4 +35,4 @@ function setupButtons (ethQuery) { }) logToDom(txHash, 'cb-value') }) -} \ No newline at end of file +} diff --git a/mascara/example/server.js b/mascara/example/server.js index d39c19600..bdb1efa16 100644 --- a/mascara/example/server.js +++ b/mascara/example/server.js @@ -1,8 +1,8 @@ const express = require('express') +const path = require('path') const createMetamascaraServer = require('../server/') const createBundle = require('../server/util').createBundle const serveBundle = require('../server/util').serveBundle - // // Iframe Server // @@ -23,7 +23,7 @@ const dappServer = express() // serve dapp bundle serveBundle(dappServer, '/app.js', createBundle(require.resolve('./app.js'))) -dappServer.use(express.static(__dirname + '/app/')) +dappServer.use(express.static(path.join(__dirname, '/app/'))) // start the server const dappPort = '9002' diff --git a/mascara/src/app/first-time/breadcrumbs.js b/mascara/src/app/first-time/breadcrumbs.js index b81a9fb9b..d86e10d48 100644 --- a/mascara/src/app/first-time/breadcrumbs.js +++ b/mascara/src/app/first-time/breadcrumbs.js @@ -8,7 +8,7 @@ export default class Breadcrumbs extends Component { currentIndex: PropTypes.number, }; - render() { + render () { const {total, currentIndex} = this.props return (
@@ -20,7 +20,7 @@ export default class Breadcrumbs extends Component { /> ))}
- ); + ) } } diff --git a/mascara/src/app/first-time/buy-ether-screen.js b/mascara/src/app/first-time/buy-ether-screen.js index c5a560638..e270392e1 100644 --- a/mascara/src/app/first-time/buy-ether-screen.js +++ b/mascara/src/app/first-time/buy-ether-screen.js @@ -54,7 +54,7 @@ class BuyEtherScreen extends Component { return (
showAccountDetail(address)} > Do it later @@ -64,17 +64,17 @@ class BuyEtherScreen extends Component { renderCoinbaseLogo () { return ( - - - - - - - - - - - + + + + + + + + + + + @@ -85,13 +85,13 @@ class BuyEtherScreen extends Component { const {goToCoinbase, address} = this.props return ( -
+
{this.renderCoinbaseLogo()}
-
Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.
- What is Ethereum? -
+
Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.
+ What is Ethereum? +
+ + +
+
+
+
Send eth
+
+ +
+
+
+
Send tokens
+
+
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index 823c72a3a..32f57b157 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -12,7 +12,6 @@ const { } = require('../func') const { checkBrowserForConsoleErrors, - loadExtension, verboseReportOnFailure, findElement, findElements, @@ -22,13 +21,12 @@ const { describe('Using MetaMask with an existing account', function () { let extensionId let driver - let tokenAddress const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC' + const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6' const regularDelayMs = 1000 const largeDelayMs = regularDelayMs * 2 - const waitingNewPageDelayMs = regularDelayMs * 10 this.timeout(0) this.bail(true) @@ -97,15 +95,16 @@ describe('Using MetaMask with an existing account', function () { await delay(regularDelayMs) // Close all other tabs - let [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() - newUi = newUi || infoPage + const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() + + const newUiOrInfoPage = newUi || infoPage await driver.switchTo().window(oldUi) await driver.close() - if (infoPage !== newUi) { + if (infoPage !== newUiOrInfoPage) { await driver.switchTo().window(infoPage) await driver.close() } - await driver.switchTo().window(newUi) + await driver.switchTo().window(newUiOrInfoPage) await delay(regularDelayMs) const continueBtn = await findElement(driver, By.css('.welcome-screen__button')) @@ -166,8 +165,7 @@ describe('Using MetaMask with an existing account', function () { describe('Show account information', () => { it('shows the correct account address', async () => { - const detailsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Details')]`)) - detailsButton.click() + await driver.findElement(By.css('.wallet-view__details-button')).click() await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await delay(regularDelayMs) @@ -263,8 +261,10 @@ describe('Using MetaMask with an existing account', function () { await configureGas.click() await delay(regularDelayMs) + const gasModal = await driver.findElement(By.css('span .modal')) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() + await driver.wait(until.stalenessOf(gasModal)) await delay(regularDelayMs) // Continue to next screen @@ -289,142 +289,83 @@ describe('Using MetaMask with an existing account', function () { }) }) - describe('Send ETH from Faucet', () => { - it('starts a send transaction inside Faucet', async () => { - await driver.executeScript('window.open("https://faucet.metamask.io")') - await delay(waitingNewPageDelayMs) - - const [extension, faucet] = await driver.getAllWindowHandles() - await driver.switchTo().window(faucet) + describe('Imports an account with private key', () => { + it('choose Create Account from the account menu', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() await delay(regularDelayMs) - const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000) - await send1eth.click() + const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`)) + await importAccount.click() await delay(regularDelayMs) + }) - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) + it('enter private key', async () => { + const privateKeyInput = await findElement(driver, By.css('#private-key-box')) + await privateKeyInput.sendKeys(testPrivateKey2) await delay(regularDelayMs) + const importButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`)) + await importButtons[0].click() + await delay(regularDelayMs) + }) - const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000) - await confirmButton.click() + it('should show the correct account name', async () => { + const [accountName] = await findElements(driver, By.css('.account-name')) + assert.equal(await accountName.getText(), 'Account 3') await delay(regularDelayMs) + }) - await driver.switchTo().window(faucet) - await delay(regularDelayMs) - await driver.close() - await delay(regularDelayMs) - await driver.switchTo().window(extension) - await delay(regularDelayMs) - await loadExtension(driver, extensionId) + it('should show the imported label', async () => { + const [importedLabel] = await findElements(driver, By.css('.wallet-view__keyring-label')) + assert.equal(await importedLabel.getText(), 'IMPORTED') await delay(regularDelayMs) }) }) - describe('Add existing token using search', () => { - it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) - await addToken.click() + describe('Connects to a Hardware wallet', () => { + it('choose Connect Hardware Wallet from the account menu', async () => { + await driver.findElement(By.css('.account-menu__icon')).click() + await delay(regularDelayMs) + + const [connectAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Connect Hardware Wallet')]`)) + await connectAccount.click() await delay(regularDelayMs) }) - it('picks an existing token', async () => { - const tokenSearch = await findElement(driver, By.css('#search-tokens')) - await tokenSearch.sendKeys('BAT') + it('should open the TREZOR Connect popup', async () => { + const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`)) + await connectButtons[0].click() await delay(regularDelayMs) - - const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]")) - await token.click() - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) - await nextScreen.click() - await delay(regularDelayMs) - - const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) - await addTokens.click() - await delay(largeDelayMs) + const allWindows = await driver.getAllWindowHandles() + switch (process.env.SELENIUM_BROWSER) { + case 'chrome': + assert.equal(allWindows.length, 2) + break + default: + assert.equal(allWindows.length, 1) + } }) - it('renders the balance for the new token', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '0BAT') - await delay(regularDelayMs) - }) - }) + it('should show the "Browser not supported" screen for non Chrome browsers', async () => { + if (process.env.SELENIUM_BROWSER !== 'chrome') { + const title = await findElements(driver, By.xpath(`//h3[contains(text(), 'Your Browser is not supported...')]`)) + assert.equal(title.length, 1) - describe('Add a custom token from TokenFactory', () => { - it('creates a new token', async () => { - await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') - await delay(waitingNewPageDelayMs) + const downloadChromeButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Download Google Chrome')]`)) + assert.equal(downloadChromeButtons.length, 1) - const [extension, tokenFactory] = await driver.getAllWindowHandles() - await driver.switchTo().window(tokenFactory) - const [ - totalSupply, - tokenName, - tokenDecimal, - tokenSymbol, - ] = await findElements(driver, By.css('.form-control')) + await downloadChromeButtons[0].click() + await delay(regularDelayMs) - await totalSupply.sendKeys('100') - await tokenName.sendKeys('Test') - await tokenDecimal.sendKeys('0') - await tokenSymbol.sendKeys('TST') + const [newUITab, downloadChromeTab] = await driver.getAllWindowHandles() - const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`)) - await createToken.click() - await delay(regularDelayMs) - - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) - await delay(regularDelayMs) - - const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) - await confirmButton.click() - await delay(regularDelayMs) - - await driver.switchTo().window(tokenFactory) - await delay(regularDelayMs) - const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) - tokenAddress = await tokenContactAddress.getText() - await driver.close() - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) - await delay(regularDelayMs) - }) - - it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) - await addToken.click() - await delay(regularDelayMs) - }) - - it('picks the new Test token', async () => { - const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]")) - await addCustomToken.click() - await delay(regularDelayMs) - - const newTokenAddress = await findElement(driver, By.css('#custom-address')) - await newTokenAddress.sendKeys(tokenAddress) - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) - await nextScreen.click() - await delay(regularDelayMs) - - const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) - await addTokens.click() - await delay(regularDelayMs) - }) - - it('renders the balance for the new token', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) - await driver.wait(until.elementTextIs(balance, '100TST')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '100TST') - await delay(regularDelayMs) + await driver.switchTo().window(downloadChromeTab) + await delay(regularDelayMs) + const tabUrl = await driver.getCurrentUrl() + assert.equal(tabUrl, 'https://www.google.com/chrome/') + await driver.close() + await delay(regularDelayMs) + await driver.switchTo().window(newUITab) + } }) }) }) diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js index 31c41d8b7..828f87db7 100644 --- a/test/e2e/beta/helpers.js +++ b/test/e2e/beta/helpers.js @@ -1,14 +1,21 @@ 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') module.exports = { + assertElementNotPresent, checkBrowserForConsoleErrors, - loadExtension, - verboseReportOnFailure, + closeAllWindowHandlesExcept, findElement, findElements, + loadExtension, + openNewPage, + switchToWindowWithTitle, + verboseReportOnFailure, + waitUntilXWindowHandles, } async function loadExtension (driver, extensionId) { @@ -64,3 +71,63 @@ async function findElement (driver, by, timeout = 10000) { async function findElements (driver, by, timeout = 10000) { return driver.wait(until.elementsLocated(by), timeout) } + +async function openNewPage (driver, url) { + await driver.executeScript('window.open()') + await delay(1000) + + const handles = await driver.getAllWindowHandles() + const lastHandle = handles[handles.length - 1] + await driver.switchTo().window(lastHandle) + + await driver.get(url) + await delay(1000) +} + +async function waitUntilXWindowHandles (driver, x) { + const windowHandles = await driver.getAllWindowHandles() + if (windowHandles.length === x) return + await delay(1000) + return await waitUntilXWindowHandles(driver, x) +} + +async function switchToWindowWithTitle (driver, title, windowHandles) { + if (!windowHandles) { + windowHandles = await driver.getAllWindowHandles() + } else if (windowHandles.length === 0) { + throw new Error('No window with title: ' + title) + } + const firstHandle = windowHandles[0] + await driver.switchTo().window(firstHandle) + const handleTitle = await driver.getTitle() + + if (handleTitle === title) { + return firstHandle + } else { + return await switchToWindowWithTitle(driver, title, windowHandles.slice(1)) + } +} + +async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) { + exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions + windowHandles = windowHandles || await driver.getAllWindowHandles() + const lastWindowHandle = windowHandles.pop() + if (!exceptions.includes(lastWindowHandle)) { + await driver.switchTo().window(lastWindowHandle) + await delay(1000) + await driver.close() + await delay(1000) + } + return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles) +} + +async function assertElementNotPresent (webdriver, driver, by) { + try { + const dataTab = await findElement(driver, by, 4000) + if (dataTab) { + assert(false, 'Data tab should not be present') + } + } catch (err) { + assert(err instanceof webdriver.error.NoSuchElementError) + } +} diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index d911b91ed..ca1977c5a 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -11,11 +11,16 @@ const { getExtensionIdFirefox, } = require('../func') const { + assertElementNotPresent, + checkBrowserForConsoleErrors, + closeAllWindowHandlesExcept, findElement, findElements, - checkBrowserForConsoleErrors, loadExtension, + openNewPage, + switchToWindowWithTitle, verboseReportOnFailure, + waitUntilXWindowHandles, } = require('./helpers') describe('MetaMask', function () { @@ -24,10 +29,9 @@ describe('MetaMask', function () { let tokenAddress const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' - const tinyDelayMs = 1000 + const tinyDelayMs = 200 const regularDelayMs = tinyDelayMs * 2 const largeDelayMs = regularDelayMs * 2 - const waitingNewPageDelayMs = regularDelayMs * 30 this.timeout(0) this.bail(true) @@ -62,7 +66,7 @@ describe('MetaMask', function () { } } if (this.currentTest.state === 'failed') { - await verboseReportOnFailure(this.currentTest) + await verboseReportOnFailure(driver, this.currentTest) } }) @@ -71,37 +75,58 @@ describe('MetaMask', function () { }) describe('New UI setup', async function () { + let networkSelector it('switches to first tab', async function () { const [firstTab] = await driver.getAllWindowHandles() await driver.switchTo().window(firstTab) await delay(regularDelayMs) + try { + networkSelector = await findElement(driver, By.css('#network_component')) + } catch (e) { + await loadExtension(driver, extensionId) + await delay(largeDelayMs * 2) + networkSelector = await findElement(driver, By.css('#network_component')) + } + await delay(regularDelayMs) }) - it('use the local network', async function () { - const networkSelector = await findElement(driver, By.css('#network_component')) + it('uses the local network', async function () { await networkSelector.click() await delay(regularDelayMs) - const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`)) + const networks = await findElements(driver, By.css('.dropdown-menu-item')) + const localhost = networks[4] + await driver.wait(until.elementTextMatches(localhost, /Localhost/)) await localhost.click() await delay(regularDelayMs) }) it('selects the new UI option', async () => { + try { + const overlay = await findElement(driver, By.css('.full-flex-height')) + await driver.wait(until.stalenessOf(overlay)) + } catch (e) {} + const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]")) await button.click() await delay(regularDelayMs) // Close all other tabs - let [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() - newUi = newUi || infoPage + const [oldUi, tab1, tab2] = await driver.getAllWindowHandles() await driver.switchTo().window(oldUi) await driver.close() - if (infoPage !== newUi) { - await driver.switchTo().window(infoPage) + + await driver.switchTo().window(tab1) + const tab1Url = await driver.getCurrentUrl() + if (tab1Url.match(/metamask.io/)) { + await driver.switchTo().window(tab1) await driver.close() + await driver.switchTo().window(tab2) + } else if (tab2) { + await driver.switchTo().window(tab2) + await driver.close() + await driver.switchTo().window(tab1) } - await driver.switchTo().window(newUi) await delay(regularDelayMs) const continueBtn = await findElement(driver, By.css('.welcome-screen__button')) @@ -136,6 +161,7 @@ describe('MetaMask', function () { await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos) await delay(regularDelayMs) const acceptTos = await findElement(driver, By.css('.tou button')) + driver.wait(until.elementIsEnabled(acceptTos)) await acceptTos.click() await delay(regularDelayMs) }) @@ -160,8 +186,10 @@ describe('MetaMask', function () { let seedPhrase it('reveals the seed phrase', async () => { - const revealSeedPhrase = await findElement(driver, By.css('.backup-phrase__secret-blocker')) - await revealSeedPhrase.click() + const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button') + await driver.wait(until.elementLocated(byRevealButton, 10000)) + const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) + await revealSeedPhraseButton.click() await delay(regularDelayMs) seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText() @@ -173,56 +201,88 @@ describe('MetaMask', function () { await delay(regularDelayMs) }) + async function retypeSeedPhrase (words, wasReloaded) { + try { + if (wasReloaded) { + const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button') + await driver.wait(until.elementLocated(byRevealButton, 10000)) + const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) + await revealSeedPhraseButton.click() + await delay(regularDelayMs) + + const nextScreen = await findElement(driver, By.css('.backup-phrase button')) + await nextScreen.click() + await delay(regularDelayMs) + } + + const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000) + + await word0.click() + await delay(tinyDelayMs) + + const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000) + + await word1.click() + await delay(tinyDelayMs) + + const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000) + + await word2.click() + await delay(tinyDelayMs) + + const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000) + + await word3.click() + await delay(tinyDelayMs) + + const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000) + + await word4.click() + await delay(tinyDelayMs) + + const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000) + + await word5.click() + await delay(tinyDelayMs) + + const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000) + + await word6.click() + await delay(tinyDelayMs) + + const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`), 10000) + + await word7.click() + await delay(tinyDelayMs) + + const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`), 10000) + + await word8.click() + await delay(tinyDelayMs) + + const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`), 10000) + + await word9.click() + await delay(tinyDelayMs) + + const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`), 10000) + + await word10.click() + await delay(tinyDelayMs) + + const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`), 10000) + await word11.click() + await delay(tinyDelayMs) + } catch (e) { + await loadExtension(driver, extensionId) + await retypeSeedPhrase(words, true) + } + } + it('can retype the seed phrase', async () => { const words = seedPhrase.split(' ') - const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`)) - await word0.click() - await delay(tinyDelayMs) - - const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`)) - await word1.click() - await delay(tinyDelayMs) - - const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`)) - await word2.click() - await delay(tinyDelayMs) - - const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`)) - await word3.click() - await delay(tinyDelayMs) - - const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`)) - await word4.click() - await delay(tinyDelayMs) - - const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`)) - await word5.click() - await delay(tinyDelayMs) - - const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`)) - await word6.click() - await delay(tinyDelayMs) - - const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`)) - await word7.click() - await delay(tinyDelayMs) - - const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`)) - await word8.click() - await delay(tinyDelayMs) - - const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`)) - await word9.click() - await delay(tinyDelayMs) - - const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`)) - await word10.click() - await delay(tinyDelayMs) - - const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`)) - await word11.click() - await delay(tinyDelayMs) + await retypeSeedPhrase(words) const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirm.click() @@ -230,7 +290,8 @@ describe('MetaMask', function () { }) it('clicks through the deposit modal', async () => { - const buyModal = await driver.findElement(By.css('span .modal')) + const byBuyModal = By.css('span .modal') + const buyModal = await driver.wait(until.elementLocated(byBuyModal)) const closeModal = await findElement(driver, By.css('.page-container__header-close')) await closeModal.click() await driver.wait(until.stalenessOf(buyModal)) @@ -244,8 +305,12 @@ describe('MetaMask', function () { await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await delay(regularDelayMs) + const accountModal = await driver.findElement(By.css('span .modal')) + await driver.executeScript("document.querySelector('.account-modal-close').click()") - await delay(regularDelayMs * 4) + + await driver.wait(until.stalenessOf(accountModal)) + await delay(regularDelayMs) }) }) @@ -263,7 +328,7 @@ describe('MetaMask', function () { it('accepts the account password after lock', async () => { await driver.findElement(By.id('password')).sendKeys('correct horse battery staple') await driver.findElement(By.id('password')).sendKeys(Key.ENTER) - await delay(regularDelayMs * 4) + await delay(largeDelayMs * 4) }) }) @@ -284,10 +349,10 @@ describe('MetaMask', function () { const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`)) await create.click() - await delay(regularDelayMs) + await delay(largeDelayMs) }) - it('should correct account name', async () => { + it('should display correct account name', async () => { const accountName = await findElement(driver, By.css('.account-name')) assert.equal(await accountName.getText(), '2nd account') await delay(regularDelayMs) @@ -315,16 +380,18 @@ describe('MetaMask', function () { await seedTextArea.sendKeys(testSeedPhrase) await delay(regularDelayMs) - await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple') - await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple') - await driver.findElement(By.css('button:nth-child(2)')).click() + const passwordInputs = await driver.findElements(By.css('input')) + await delay(regularDelayMs) + + passwordInputs[0].sendKeys('correct horse battery staple') + passwordInputs[1].sendKeys('correct horse battery staple') + await driver.findElement(By.css('.first-time-flow__button')).click() await delay(regularDelayMs) }) it('balance renders', async () => { const balance = await findElement(driver, By.css('.balance-display .token-amount')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '100.000 ETH') + await driver.wait(until.elementTextMatches(balance, /100.+ETH/)) await delay(regularDelayMs) }) }) @@ -340,13 +407,19 @@ describe('MetaMask', function () { await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') + const inputValue = await inputAmount.getAttribute('value') + assert.equal(inputValue, '1') + // Set the gas limit const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button')) await configureGas.click() await delay(regularDelayMs) + const gasModal = await driver.findElement(By.css('span .modal')) + const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() + await driver.wait(until.stalenessOf(gasModal)) await delay(regularDelayMs) // Continue to next screen @@ -356,6 +429,334 @@ describe('MetaMask', function () { }) it('confirms the transaction', async function () { + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(largeDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + const transactions = await findElements(driver, By.css('.tx-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) + } + }) + }) + + describe('Send ETH from dapp', () => { + it('starts a send transaction inside the dapp', async () => { + await openNewPage(driver, 'http://127.0.0.1:8080/') + await delay(regularDelayMs) + + await waitUntilXWindowHandles(driver, 2) + let windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = windowHandles[1] + + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000) + await send3eth.click() + await delay(regularDelayMs) + + windowHandles = await driver.getAllWindowHandles() + await driver.switchTo().window(windowHandles[2]) + await delay(regularDelayMs) + + assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`)) + + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000) + await confirmButton.click() + await delay(regularDelayMs) + + await waitUntilXWindowHandles(driver, 2) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + }) + + 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) + + const txValues = await findElement(driver, By.css('.tx-list-value')) + await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000) + }) + }) + + describe('Deploy contract and call contract methods', () => { + let extension + let dapp + it('creates a deploy contract transaction', async () => { + const windowHandles = await driver.getAllWindowHandles() + extension = windowHandles[0] + dapp = windowHandles[1] + await delay(tinyDelayMs) + + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const deployContractButton = await findElement(driver, By.css('#deployButton')) + await deployContractButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`)) + await txListItem.click() + await delay(regularDelayMs) + }) + + it('displays the contract creation data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await delay(regularDelayMs) + + await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`)) + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/)) + + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await delay(regularDelayMs) + }) + + it('confirms a deploy contract transaction', async () => { + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + 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') + await delay(regularDelayMs) + }) + + it('calls and confirms a contract method where ETH is sent', async () => { + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const depositButton = await findElement(driver, By.css('#depositButton')) + await depositButton.click() + await delay(regularDelayMs) + + 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 txListValue.click() + await delay(regularDelayMs) + + // Set the gas limit + const configureGas = await findElement(driver, By.css('.confirm-detail-row__header-text--edit')) + await configureGas.click() + await delay(regularDelayMs) + + const gasModal = await driver.findElement(By.css('span .modal')) + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) + + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) + await gasPriceInput.clear() + await gasPriceInput.sendKeys('10') + await gasLimitInput.clear() + await gasLimitInput.sendKeys('60001') + + const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) + await save.click() + await delay(regularDelayMs) + + await driver.wait(until.stalenessOf(gasModal)) + + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + 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, /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}$/)) + }) + + it('calls and confirms a contract method where ETH is received', async () => { + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const withdrawButton = await findElement(driver, By.css('#withdrawButton')) + await withdrawButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + const txListItem = await findElement(driver, By.css('.tx-list-item')) + await txListItem.click() + await delay(regularDelayMs) + + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + 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, /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')) + await delay(regularDelayMs) + if (process.env.SELENIUM_BROWSER !== 'firefox') { + await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000) + const tokenAmount = await balance.getText() + assert.ok(/^92.*ETH.*$/.test(tokenAmount)) + await delay(regularDelayMs) + } + }) + }) + + describe('Add a custom token from a dapp', () => { + it('creates a new token', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = windowHandles[1] + await delay(regularDelayMs * 2) + + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`)) + await createToken.click() + await delay(regularDelayMs) + + await driver.switchTo().window(extension) + await loadExtension(driver, extensionId) + await delay(regularDelayMs) + + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) + + const tokenContractAddress = await driver.findElement(By.css('#tokenAddress')) + await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/)) + tokenAddress = await tokenContractAddress.getText() + + await delay(regularDelayMs) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + }) + + it('clicks on the Add Token button', async () => { + const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) + 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')]")) + await addCustomToken.click() + await delay(regularDelayMs) + + const newTokenAddress = await findElement(driver, By.css('#custom-address')) + await newTokenAddress.sendKeys(tokenAddress) + await delay(regularDelayMs) + + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + + const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) + await addTokens.click() + await delay(regularDelayMs) + }) + + it('renders the balance for the new token', async () => { + const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) + await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/)) + const tokenAmount = await balance.getText() + assert.ok(/^100\s*TST\s*$/.test(tokenAmount)) + await delay(regularDelayMs) + }) + }) + + describe('Send token from inside MetaMask', () => { + let gasModal + it('starts to send a transaction', async function () { + const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`)) + await sendButton.click() + await delay(regularDelayMs) + + const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) + const inputAmount = await findElement(driver, By.css('.currency-display__input')) + await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') + await inputAmount.sendKeys('50') + + // Set the gas limit + const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button')) + await configureGas.click() + await delay(regularDelayMs) + + gasModal = await driver.findElement(By.css('span .modal')) + }) + + it('opens customizes gas modal', async () => { + await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title'))) + const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) + await save.click() + await delay(regularDelayMs) + }) + + it('transitions to the confirm screen', async () => { + await driver.wait(until.stalenessOf(gasModal)) + + // Continue to next screen + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) + await nextScreen.click() + await delay(regularDelayMs) + }) + + it('displays the token transfer data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await delay(regularDelayMs) + + const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionTypeText = await functionType.getText() + assert.equal(functionTypeText, 'Transfer') + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/)) + + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await delay(regularDelayMs) + }) + + it('submits the transaction', async function () { const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() await delay(regularDelayMs) @@ -367,40 +768,227 @@ describe('MetaMask', function () { const txValues = await findElements(driver, By.css('.tx-list-value')) assert.equal(txValues.length, 1) - assert.equal(await txValues[0].getText(), '1 ETH') + + // 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) + } + + 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') }) }) - describe('Send ETH from Faucet', () => { - it('starts a send transaction inside Faucet', async () => { - await driver.executeScript('window.open("https://faucet.metamask.io")') - await delay(waitingNewPageDelayMs) - - const [extension, faucet] = await driver.getAllWindowHandles() - await driver.switchTo().window(faucet) + describe('Send a custom token from dapp', () => { + let gasModal + it('sends an already created token', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await delay(regularDelayMs) - const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000) - await send1eth.click() - await delay(regularDelayMs) + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`)) + await transferTokens.click() + + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) + 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 txListValue.click() await delay(regularDelayMs) - const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000) + // Set the gas limit + const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')), 10000) + await configureGas.click() + await delay(regularDelayMs) + + gasModal = await driver.findElement(By.css('span .modal')) + }) + + it('customizes gas', async () => { + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) + + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) + await gasPriceInput.clear() + await delay(tinyDelayMs) + await gasPriceInput.sendKeys('10') + await delay(tinyDelayMs) + await gasLimitInput.clear() + await delay(tinyDelayMs) + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await gasLimitInput.sendKeys('60000') + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) + + // Needed for different behaviour of input in different versions of firefox + const gasLimitInputValue = await gasLimitInput.getAttribute('value') + if (gasLimitInputValue === '600001') { + await gasLimitInput.sendKeys(Key.BACK_SPACE) + } + + const save = await findElement(driver, By.css('.customize-gas__save')) + await save.click() + await driver.wait(until.stalenessOf(gasModal)) + + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) + assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + }) + + it('submits the transaction', async function () { + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() await delay(regularDelayMs) + }) - await driver.switchTo().window(faucet) - await delay(regularDelayMs) - await driver.close() + 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) + + 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 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() + + // 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') + } + }) + }) + + describe('Approves a custom token from dapp', () => { + let gasModal + it('approves an already created token', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await delay(regularDelayMs) + + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) + + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) + await transferTokens.click() + + await closeAllWindowHandlesExcept(driver, extension) await driver.switchTo().window(extension) await delay(regularDelayMs) - await loadExtension(driver, extensionId) + + 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/)) + await txListItem.click() await delay(regularDelayMs) }) + + it('displays the token approval data', async () => { + const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) + dataTab.click() + await delay(regularDelayMs) + + const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionTypeText = await functionType.getText() + assert.equal(functionTypeText, 'Approve') + + const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataText = await confirmDataDiv.getText() + assert.equal(confirmDataText.match(/0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/)) + + const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) + detailsTab.click() + await delay(regularDelayMs) + + const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) + const approvalWarningText = await approvalWarning.getText() + assert(approvalWarningText.match(/By approving this/)) + await delay(regularDelayMs) + }) + + it('opens the gas edit modal', async () => { + const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit'))) + await configureGas.click() + await delay(regularDelayMs) + + gasModal = await driver.findElement(By.css('span .modal')) + }) + + it('customizes gas', async () => { + await driver.wait(until.elementLocated(By.css('.customize-gas__title'))) + + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input')) + await gasPriceInput.clear() + await delay(tinyDelayMs) + await gasPriceInput.sendKeys('10') + await delay(tinyDelayMs) + await gasLimitInput.clear() + await delay(tinyDelayMs) + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await gasLimitInput.sendKeys('60000') + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) + + // Needed for different behaviour of input in different versions of firefox + const gasLimitInputValue = await gasLimitInput.getAttribute('value') + if (gasLimitInputValue === '600001') { + await gasLimitInput.sendKeys(Key.BACK_SPACE) + } + + const save = await findElement(driver, By.css('.customize-gas__save')) + await save.click() + await driver.wait(until.stalenessOf(gasModal)) + + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) + assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + }) + + it('submits the transaction', async function () { + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + 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/)) + }) + }) + + describe('Hide token', () => { + it('hides the token when clicked', async () => { + const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis')) + await hideTokenEllipsis.click() + + const byTokenMenuDropdownOption = By.css('.menu__item--clickable') + const tokenMenuDropdownOption = await driver.wait(until.elementLocated(byTokenMenuDropdownOption)) + + await tokenMenuDropdownOption.click() + + const confirmHideModal = await findElement(driver, By.css('span .modal')) + + const byHideTokenConfirmationButton = By.css('.hide-token-confirmation__button') + const hideTokenConfirmationButton = await driver.wait(until.elementLocated(byHideTokenConfirmationButton)) + await hideTokenConfirmationButton.click() + + await driver.wait(until.stalenessOf(confirmHideModal)) + }) }) describe('Add existing token using search', () => { @@ -430,83 +1018,7 @@ 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')) - await driver.wait(until.elementTextIs(balance, '0BAT')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '0BAT') - await delay(regularDelayMs) - }) - }) - - describe('Add a custom token from TokenFactory', () => { - it('creates a new token', async () => { - await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') - await delay(waitingNewPageDelayMs) - - const [extension, tokenFactory] = await driver.getAllWindowHandles() - await driver.switchTo().window(tokenFactory) - const [ - totalSupply, - tokenName, - tokenDecimal, - tokenSymbol, - ] = await findElements(driver, By.css('.form-control')) - - await totalSupply.sendKeys('100') - await tokenName.sendKeys('Test') - await tokenDecimal.sendKeys('0') - await tokenSymbol.sendKeys('TST') - - const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`)) - await createToken.click() - await delay(regularDelayMs) - - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) - await delay(regularDelayMs) - - const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) - await confirmButton.click() - await delay(regularDelayMs) - - await driver.switchTo().window(tokenFactory) - await delay(regularDelayMs) - const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) - tokenAddress = await tokenContactAddress.getText() - await driver.close() - await driver.switchTo().window(extension) - await loadExtension(driver, extensionId) - await delay(regularDelayMs) - }) - - it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) - 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')]")) - await addCustomToken.click() - await delay(regularDelayMs) - - const newTokenAddress = await findElement(driver, By.css('#custom-address')) - await newTokenAddress.sendKeys(tokenAddress) - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) - await nextScreen.click() - await delay(regularDelayMs) - - const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) - await addTokens.click() - await delay(regularDelayMs) - }) - - it('renders the balance for the new token', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) - await driver.wait(until.elementTextIs(balance, '100TST')) - const tokenAmount = await balance.getText() - assert.equal(tokenAmount, '100TST') + await driver.wait(until.elementTextMatches(balance, /0\sBAT/)) await delay(regularDelayMs) }) }) diff --git a/test/e2e/beta/run-all.sh b/test/e2e/beta/run-all.sh index 5916d5614..493e1360a 100755 --- a/test/e2e/beta/run-all.sh +++ b/test/e2e/beta/run-all.sh @@ -6,5 +6,5 @@ set -o pipefail export PATH="$PATH:./node_modules/.bin" -shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec' -shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec' +shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec' +shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec' diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js index b0a8fe411..b6efae5b3 100644 --- a/test/e2e/metamask.spec.js +++ b/test/e2e/metamask.spec.js @@ -4,7 +4,7 @@ const path = require('path') const assert = require('assert') const pify = require('pify') const webdriver = require('selenium-webdriver') -const { By, Key } = webdriver +const { By, Key, until } = webdriver const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func') describe('Metamask popup page', function () { @@ -229,8 +229,12 @@ describe('Metamask popup page', function () { it('confirms transaction', async function () { await delay(300) - await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click() - await delay(500) + const bySubmitButton = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input') + const submitButton = await driver.wait(until.elementLocated(bySubmitButton)) + + submitButton.click() + + await delay(1500) }) it('finds the transaction in the transactions list', async function () { @@ -269,7 +273,8 @@ describe('Metamask popup page', function () { it('confirms transaction in MetaMask popup', async function () { const windowHandles = await driver.getAllWindowHandles() await driver.switchTo().window(windowHandles[windowHandles.length - 1]) - const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')) + const byMetamaskSubmit = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input') + const metamaskSubmit = await driver.wait(until.elementLocated(byMetamaskSubmit)) await metamaskSubmit.click() await delay(1000) }) @@ -330,7 +335,7 @@ describe('Metamask popup page', function () { await driver.executeScript('window.metamask.setProviderType(arguments[0])', type) } - async function checkBrowserForConsoleErrors() { + async function checkBrowserForConsoleErrors () { const ignoredLogTypes = ['WARNING'] const ignoredErrorMessages = [ // React throws error warnings on "dataset", but still sets the data-* properties correctly diff --git a/test/flat.conf.js b/test/flat.conf.js index cd2dbdcdc..1c9ec3dcd 100644 --- a/test/flat.conf.js +++ b/test/flat.conf.js @@ -1,6 +1,6 @@ const getBaseConfig = require('./base.conf.js') -module.exports = function(config) { +module.exports = function (config) { const settings = getBaseConfig(config) settings.files.push('development/bundle.js') settings.files.push('test/integration/bundle.js') diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js index 5a08c90cd..6de7574c4 100644 --- a/test/integration/lib/add-token.js +++ b/test/integration/lib/add-token.js @@ -75,7 +75,7 @@ async function runAddTokenFlowTest (assert, done) { tokenWrapper[0].click() // Click Next button - let nextButton = await queryAsync($, 'button.btn-primary.btn--large') + const nextButton = await queryAsync($, 'button.btn-primary.btn--large') assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') nextButton[0].click() diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index d5ed7c77c..dcc25c493 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -2,9 +2,7 @@ const reactTriggerChange = require('react-trigger-change') const { timeout, queryAsync, - findAsync, } = require('../../lib/util') -const PASSWORD = 'password123' QUnit.module('confirm sig requests') @@ -16,23 +14,23 @@ QUnit.test('successful confirmation of sig requests', (assert) => { }) }) -async function runConfirmSigRequestsTest(assert, done) { - let selectState = await queryAsync($, 'select') +async function runConfirmSigRequestsTest (assert, done) { + const selectState = await queryAsync($, 'select') selectState.val('confirm sig requests') reactTriggerChange(selectState[0]) - // await timeout(1000000) - const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable') if (pendingRequestItem[0]) { pendingRequestItem[0].click() } + await timeout(1000) + let confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') - let confirmSigMessage = await queryAsync($, '.request-signature__notice') + const confirmSigMessage = await queryAsync($, '.request-signature__notice') assert.ok(confirmSigMessage[0].textContent.match(/^Signing\sthis\smessage/)) let confirmSigRowValue = await queryAsync($, '.request-signature__row-value') @@ -40,16 +38,16 @@ async function runConfirmSigRequestsTest(assert, done) { let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - + await timeout(1000) confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') confirmSigRowValue = await queryAsync($, '.request-signature__row-value') - assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/)) + assert.ok(confirmSigRowValue[0].textContent.match(/^#\sTerms\sof\sUse/)) confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - + await timeout(1000) confirmSigHeadline = await queryAsync($, '.request-signature__headline') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') @@ -60,6 +58,5 @@ async function runConfirmSigRequestsTest(assert, done) { confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large') confirmSigSignButton[0].click() - const txView = await queryAsync($, '.tx-view') - assert.ok(txView[0], 'Should return to the account details screen after confirming') + await timeout(2000) } diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js index 7705c9720..d42b7495d 100644 --- a/test/integration/lib/currency-localization.js +++ b/test/integration/lib/currency-localization.js @@ -15,10 +15,11 @@ QUnit.test('renders localized currency', (assert) => { }) }) -async function runCurrencyLocalizationTest(assert, done) { +async function runCurrencyLocalizationTest (assert, done) { console.log('*** start runCurrencyLocalizationTest') const selectState = await queryAsync($, 'select') selectState.val('currency localization') + await timeout(1000) reactTriggerChange(selectState[0]) await timeout(1000) const txView = await queryAsync($, '.tx-view') diff --git a/test/integration/lib/mascara-first-time.js b/test/integration/lib/mascara-first-time.js index f43a30c74..8bbdb4410 100644 --- a/test/integration/lib/mascara-first-time.js +++ b/test/integration/lib/mascara-first-time.js @@ -42,7 +42,7 @@ async function runFirstTimeUsageTest (assert, done) { assert.equal(created.textContent, 'Your unique account image', 'unique image screen') // Agree button - let button = (await findAsync(app, 'button'))[0] + const button = (await findAsync(app, 'button'))[0] assert.ok(button, 'button present') button.click() diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 72e4a8cb1..406863ca6 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -5,8 +5,6 @@ const { findAsync, } = require('../../lib/util') -const PASSWORD = 'password123' - QUnit.module('new ui send flow') QUnit.test('successful send flow', (assert) => { @@ -54,7 +52,7 @@ async function customizeGas (assert, price, limit, ethFee, usdFee) { ) } -async function runSendFlowTest(assert, done) { +async function runSendFlowTest (assert, done) { console.log('*** start runSendFlowTest') const selectState = await queryAsync($, 'select') selectState.val('send new ui') @@ -87,7 +85,7 @@ async function runSendFlowTest(assert, done) { sendFromFieldItemAddress = await queryAsync($, '.account-list-item__account-name') assert.equal(sendFromFieldItemAddress[0].textContent, 'Send Account 2', 'send from field dropdown changes account name') - let sendToFieldInput = await queryAsync($, '.send-v2__to-autocomplete__input') + const sendToFieldInput = await queryAsync($, '.send-v2__to-autocomplete__input') sendToFieldInput[0].focus() const sendToDropdownList = await queryAsync($, '.send-v2__from-dropdown__list') @@ -114,19 +112,8 @@ async function runSendFlowTest(assert, done) { errorMessage = $('.send-v2__error') assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected') - const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') - assert.equal( - sendGasField.find('.currency-display__input-wrapper > input').val(), - '0.000021', - 'send gas field should show estimated gas total' - ) - assert.equal( - sendGasField.find('.currency-display__converted-value')[0].textContent, - '$0.03 USD', - 'send gas field should show estimated gas total converted to USD' - ) - await customizeGas(assert, 0, 21000, '0', '$0.00 USD') + await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD') await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD') const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') @@ -138,18 +125,18 @@ async function runSendFlowTest(assert, done) { reactTriggerChange(selectState[0]) const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first() - assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name') + 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() assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') - const confirmScreenRows = await queryAsync($, '.confirm-screen-rows') - const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0] - assert.equal(confirmScreenGas.textContent, '$3.60 USD', 'confirm screen should show correct gas') - const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2] - assert.equal(confirmScreenTotal.textContent, '$2,405.36 USD', 'confirm screen should show correct total') + const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat') + const confirmScreenGas = confirmScreenRowFiats[0] + assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas') + const confirmScreenTotal = confirmScreenRowFiats[1] + assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total') - const confirmScreenBackButton = await queryAsync($, '.page-container__back-button') + const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button') confirmScreenBackButton[0].click() const sendFromFieldItemInEdit = await queryAsync($, '.account-list-item') diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js index 4856b3852..b7aca44d5 100644 --- a/test/integration/lib/tx-list-items.js +++ b/test/integration/lib/tx-list-items.js @@ -1,6 +1,5 @@ const reactTriggerChange = require('../../lib/react-trigger-change') const { - timeout, queryAsync, findAsync, } = require('../../lib/util') @@ -15,7 +14,7 @@ QUnit.test('renders list items successfully', (assert) => { }) }) -async function runTxListItemsTest(assert, done) { +async function runTxListItemsTest (assert, done) { console.log('*** start runTxListItemsTest') const selectState = await queryAsync($, 'select') selectState.val('tx list items') @@ -32,8 +31,8 @@ async function runTxListItemsTest(assert, done) { 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-link') - assert.equal(retryTxLink[0].textContent, 'Increase the gas price on your transaction', 'retryTx has expected link') + 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 approvedTx = txListItems[2] const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status') diff --git a/test/lib/migrations/002.json b/test/lib/migrations/002.json index 9ad3d4cfe..15820ded5 100644 --- a/test/lib/migrations/002.json +++ b/test/lib/migrations/002.json @@ -1 +1 @@ -{"meta":{"version":20},"data":{"config":{},"NetworkController":{"provider":{"type":"mainnet","rpcTarget":"https://mainnet.infura.io/metamask"},"network":"1"},"firstTimeInfo":{"version":"3.12.1","date":1517351427287},"NoticeController":{"noticesList":[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Mon May 08 2017","title":"Privacy Notice","body":"MetaMask is beta software. \n\nWhen you log in to MetaMask, your current account is visible to every new site you visit.\n\nFor your privacy, for now, please sign out of MetaMask when you're done using a site.\n\nAlso, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.\n\n","id":2}]},"BlacklistController":{"phishing":{"version":2,"tolerance":2,"fuzzylist":["metamask.io","myetherwallet.com","cryptokitties.co"],"whitelist":["metahash.io","metahash.net","metahash.org","cryptotitties.com","cryptocities.net","cryptoshitties.co","cryptotitties.fun","cryptokitties.forsale","cryptokitties.care","metamate.cc","metamesh.tech","ico.nexus.social","metamesh.org","metatask.io","metmask.com","metarasa.com","metapack.com","metacase.com","metafas.nl","metamako.com","metamast.com","metamax.ru","metadesk.io","metadisk.com","metallsk.ru","metamag.fr","metamaks.ru","metamap.ru","metamaps.cc","metamats.com","metamax.by","metamax.com","metamax.io","metamuse.net","metarank.com","metaxas.com","megamas2.ru","metamask.io","myetherwallet.com","myethlerwallet.com","ethereum.org","myetheroll.com","myetherapi.com","ledgerwallet.com","databrokerdao.com","etherscan.io","etherid.org","ether.cards","etheroll.com","ethnews.com","ethex.market","ethereumdev.io","ethereumdev.kr","dether.io","ethermine.org","slaask.com","etherbtc.io","ethereal.capital","etherisc.com","m.famalk.net","etherecho.com","ethereum.os.tc","theethereum.wiki","metajack.im","etherhub.io","ethereum.network","ethereum.link","ethereum.com","prethereum.org","ethereumj.io","etheraus.com","ethereum.dev","1ethereum.ru","ethereum.nz","nethereum.com","metabank.com","metamas.com","aventus.io","metabase.com","etherdelta.com","metabase.one","cryptokitties.co"],"blacklist":["myetherwallet.uk.com","kodakone.cc","nyeihitervvallet.com","xn--myeterwalet-cm8eoi.com","nucleus.foundation","beetoken-ico.com","data-token.com","tron-labs.com","ocoin.tech","aionfoundation.com","ico-telegram.org","nyeihitervvallat.com","telegramcoin.us","daddi.cloud","daditoken.com","blockarray.org","dadi-cloud.net","wanchainfunding.org","ico-telegram.io","iconfoundation.site","iost.co","beetoken-ico.eu","cindicator.network","wanchainetwork.org","wamchain.org","wanchainltd.org","wanchainalliance.org","nucleus-vision.net","ledgerwallet.by","nucleuss.vision","myenhterswailct.com","cobin-hood.com","wanchainfoundation.org","xn--polniex-ex4c.com","xn--polniex-s1a.com","xn--polonex-ieb.com","xn--polonex-sza.com","xn--polonex-zw4c.com","xn--polonix-ws4c.com","xn--polonix-y8a.com","xn--pooniex-ojb.com","gramico.info","dimnsions.network","www-gemini.com","login-kucoin.net","venchain.foundation","grampreico.com","tgram.cc","ton-gramico.com","wwwpaywithink.com","coniomi.com","paywithnk.com","paywithlnk.com","iluminatto.com.br","pundix.eu","xn--bttrx-esay.com","xn--bttrex-w8a.com","xn--bnance-bwa.com","xn--shpeshift-11a.com","xn--shapeshif-ts6d.com","xn--shapshift-yf7d.com","wwwbluzelle.com","bluzelie.com","nucleus-vision.org","omisegonetwork.site","etlherzero.com","etlherdelta.com","xn--condesk-0ya.com","xn--condesk-sfb.com","xn--coindsk-vs4c.com","iexecplatform.com","tongramico.com","nucleus-vision.eu","intchain.network","wanchain.cloud","bluzelle-ico.com","ethzero-wallet.com","xn--metherwalle-jb9et7d.com","xn--coinesk-jo3c.com","venchainfoundation.com","myenhtersvvailot.com","ether-zero.net","ins.foundation","nastoken.org","telcointoken.com","ether0.org","eterzero.org","bluzelle-ico.eu","bleuzelle.com","appcoinstoken.org","xn--quanstamp-8s6d.com","myehntersvvailct.com","myeherwalllet.com","ico-bluzelle.com","bluzelle.im","bluzelle.one","bluzele.sale","bluzele.co","sether.ws","xn--myetherwalet-6gf.com","xn--rnyethewaliet-om1g.com","rnyethervailet.com","mvetherwaliet.com","rnyetherwailet.com","myethervaliet.com","rnyethervaliet.com","mvetherwalilet.com","xn--myethewalie-3ic0947g.com","xn--mthrwallet-z6ac3y.com","xn--myeherwalie-vici.com","xn--myethervvalie-8vc.com","xn--mythrwallt-06acf.com","xn--mtherwallet-y9a6y.com","myetherwallet.applytoken.tk","ethereum-zero.com","quanstamptoken.tk","bluzelle.network","ether-wallet.org","tron-wallet.info","appcoinsproject.com","vechain.foundation","tronlab.site","tronlabs.network","bluzelle.cc","ethblender.com","ethpaperwallet.net","waltontoken.org","icoselfkey.org","etherzeroclaim.com","etherzero.promo","bluzelle.pro","token-selfkey.org","xn--etherdlta-0f7d.com","sether.in","xn--ttrex-ysa9423c.com","bluzelle.eu","bluzelle.site","gifto.tech","xn--os-g7s.com","selfkey.co","xn--myeherwalet-ns8exy.com","xn--coinelegraph-wk5f.com","dai-stablecoin.com","eos-token.org","venchain.org","gatcoins.io","deepbrainchain.co","myetherwalililet.info","myehvterwallet.com","myehterumswallet.com","nucleusico.com","tronlab.tech","0x-project.com","gift-token-events.mywebcommunity.org","funfairtoken.org","breadtokenapp.com","cloudpetstore.com","myethwalilet.com","selfkeys.org","wallet-ethereum.com","xn--methrwallt-26ar0z.com","xn--mytherwllet-r8a0c.com","bluzelle.promo","tokensale.bluzelle.promo","cedarlake.org","marketingleads4u.com","cashaa.co","xn--inance-hrb.com","wanchain.tech","zenprolocol.com","ethscan.io","etherscan.in","props-project.com","zilliaq.com","reqestnetwork.com","etherdelta.pw","ethereum-giveaway.org","mysimpletoken.org","binancc.com","blnance.org","elherdelta.io","xn--hapeshit-ez9c2y.com","tenxwallet.co","singularitynet.info","mytlherwaliet.info","iconmainnet.ml","tokenselfkey.org","xn--myetewallet-cm8e5y.com","envione.org","myetherwalletet.com","claimbcd.com","ripiocreditnetwork.in","xn--yeterwallet-ml8euo.com","ethclassicwallet.info","myltherwallet.ru.com","etherdella.com","xn--yeterwallet-bm8ewn.com","singularty.net","cloudkitties.co","iconfoundation.io","kittystat.com","gatscoin.io","singularitynet.in","sale.canay.io","canay.io","wabicoin.co","envion.top","sirinslabs.com","tronlab.co","paxful.com.ng","changellyli.com","ethereum-code.com","xn--plonex-6va6c.com","envion.co","envion.cc","envion.site","ethereumchain.info","xn--envon-1sa.org","xn--btstamp-rfb.net","envlon.org","envion-ico.org","spectivvr.org","sirinlbs.com","ethereumdoubler.life","xn--myetherwllet-fnb.com","sirin-labs.com","sirin-labs.org","envion.one","envion.live","propsproject.org","propsprojects.com","decentralland.org","xn--metherwalet-ns8ep4b.com","redpulsetoken.co","propsproject.tech","xn--myeterwalet-nl8emj.com","powrerledger.com","cryptokitties.com","sirinlabs.pro","sirinlabs.co","sirnlabs.com","superbitcoin-blockchain.info","hellobloom.me","mobus.network","powrrledger.com","xn--myeherwalet-ms8eyy.com","qlink-ico.com","gatcoin.in","tokensale.gamefllp.com","gamefllp.com","xn--myeherwalle-vici.com","xn--myetherwalet-39b.com","xn--polonex-ffb.com","xn--birex-leba.com","raiden-network.org","sirintabs.com","xn--metherwallt-79a30a.com","xn--myethrwllet-2kb3p.com","myethlerwallet.eu","xn--btrex-b4a.com","powerrledger.com","xn--cointeegraph-wz4f.com","myerherwalet.com","qauntstanp.com","myetherermwallet.com","xn--myethewalet-ns8eqq.com","xn--nvion-hza.org","nnyetherwallelt.ru.com","ico-wacoin.com","xn--myeterwalet-nl8enj.com","bitcoinsilver.io","t0zero.com","tokensale.gizer.in","gizer.in","wabitoken.com","gladius.ws","xn--metherwallt-8bb4w.com","quanttstamp.com","gladius.im","ethereumstorage.net","powerledgerr.com","xn--myeherwallet-4j5f.com","quamtstamp.com","quntstamp.com","xn--changely-j59c.com","shapeshlft.com","coinbasenews.co.uk","xn--metherwallet-hmb.com","envoin.org","powerledger.com","bitstannp.net","xn--myetherallet-4k5fwn.com","xn--coinbas-pya.com","requestt.network","oracls.network","sirinlabs.website","powrledger.io","slackconfirm.com","shape-shift.io","oracles-network.org","xn--myeherwalle-zb9eia.com","blockstack.one","urtust.io","bittrex.one","t0-ico.com","xn--cinbase-90a.com","xn--metherwalet-ns8ez1g.com","tzero-ico.com","tzero.su","tzero.website","blockstack.network","ico-tzero.com","spectre.site","tzero.pw","spectre-ai.net","xn--waxtokn-y8a.com","dmarket.pro","bittrex.com11648724328774.cf","bittrex.com1987465798.ga","autcus.org","t-zero.org","xn--zero-zxb.com","myetherwalletfork.com","blokclbain.info","datum.sale","spectre-ai.org","powerledgr.com","simpletoken.live","sale.simpletoken.live","qauntstamp.com","raiden-network.com","metalpayme.com","quantstamp-ico.com","myetherwailetclient.com","biockchain.biz","wallets-blockchain.com","golemairdrop.com","omisegoairdrop.net","blodkchainwallet.info","walton-chain.org","elite888-ico.com","bitflyerjp.com","chainlinksmartcontract.com","stormtoken.eu","omise-go.tech","saltending.com","stormltoken.com","xn--quanttamp-42b.com","stormtoken.co","storntoken.com","stromtoken.com","storm-token.com","stormtokens.io","ether-delta.com","ethconnect.live","ethconnect.trade","xn--bttrex-3va.net","quantstamp.com.co","wancha.in","augur-network.com","quantstamp.com.ua","myetherwalletmew.com","myetherumwalletts.com","xn--quanstamp-tmd.com","quantsstamps.com","changellyl.net","xn--myetherwalet-1fb.com","myethereumwallets.com","xn--myetherwalet-e9b.com","quantslamp.com","metelpay.com","xn--eterdelta-m75d.com","linksmartcontract.com","myetherwalletaccess.com","myetherwalletcheck.com","myetherwalletcheck.info","myetherwalletconf.com","myetherwalleteal.com","myetherwalletec.com","myetherwalletgeth.com","myetherwalletmetamask.com","myetherwalletmm.com","myetherwalletmy.com","myetherwalletnh.com","myetherwalletnod.com","myetherwalletrr.com","myetherwalletrty.com","myetherwalletsec.com","myetherwalletsecure.com","myetherwalletutc.com","myetherwalletver.info","myetherwalletview.com","myetherwalletview.info","myetherwalletvrf.com","myetherwalletmist.com","myetherwalletext.com","myetherwalletjson.com","mettalpay.com","bricklblock.io","bittrexy.com","utrust.so","myethierwallet.org","metallpay.com","kraken-wallet.com","dmarkt.io","etherdeltla.com","unlversa.io","universa.sale","mercuryprotocol.live","ripiocredlt.network","myetlherwa11et.com","dentacoin.in","rdrtg.com","myetherwallet.com.rdrgh.com","rdrgh.com","ripiocreditnetwork.co","riaden.network","hydrominer.biz","rdrblock.com","reqest.network","senstoken.com","myetherwallat.services","ripiocredit.net","xn--metherwallet-c06f.com","ico.ripiocredits.com","ripiocredits.com","raidens.network","artoken.co","myetherwalletlgn.com","etherblog.click","stormtoken.site","httpmyetherwallet.com","myetherwalletverify.com","byzantiumfork.com","myetherwallet.com.byzantiumfork.com","www-myethervvallet.com","ether24.info","block-v.io","bittrex.cash","shapishift.io","ripiocerdit.network","rnyetherwa11et.com","claimether.com","enigmatokensale.com","ethereum-org.com","mvetnerwallet.com","myctherwallet.com","myetherwaltet.com","myetherwatlet.com","privatix.me","myetherwalletcnf.com","myetherwalletver.com","privatix.top","privatix.pro","privatex.io","stormtoken.cc","raiden.online","stormstoken.com","myetereumwallet.com","stormtokens.net","myetherwalletconf.info","storrntoken.com","worldofbattles.io","ico.worldofbattles.io","privatix.live","riden.network","raidan.network","ralden.network","mymyetherwallet.com","myetherwallets.net","myetherwalletverify.info","stormxtoken.com","myethereum-wallet.com","myetherwallet-forkprep.pagedemo.co","myetnerwailet.com","www-mvetherwallet.com","etheirdelta.com","myetherwalletiu.com","myetherwaiiett.com","xn--mytherwalet-cbb87i.com","xn--myethrwallet-ivb.co","xn--myeterwallet-f1b.com","myehterwaliet.com","omegaone.co","myetherwaiietw.com","slack.com.ru","polkodot.network","request-network.net","requestnetwork.live","binancie.com","first-eth.info","myewerthwalliet.com","enjincoin.pw","xn--bitrex-k17b.com","alrswap.io","www-request.network","myetnenwallet.com","www-enigma.co","cryptoinsidenews.com","air-swap.tech","launch.airswap.cc","airswap.cc","airswaptoken.com","launch.airswap.in","airswap.in","security-steemit.com.mx","blockchalnwallet.com","blodkchainwallet.com","blodkchaln.com","myethereumwaiiet.com","myethereumwaliet.com","myethereumwalilet.com","myetherswailet.com","myetherswaliet.com","myetherswalilet.com","myetherwalilett.com","myetherwalletl.com","myetherwalletww.com","myethereunwallet.com","myethereumwallct.com","myetherwaiieti.com","myetherwaiiete.com","upfirng.com","paypie.net","paypie.tech","soam.co","myetherwaiict.com","numerai-token.com","www-bankera.com","vvanchain.org","omisegoairdrop.com","xn--enjncoin-41a.io","suncontract.su","myetherwaiietr.com","shapeshiff.io","warchain.org","myethwallett.com","myethervvaliet.com","wanchains.org","etherparty.in","enjincoin.me","etiam.io","invest.smartlands.tech","smartlands.tech","enijncoin.io","wanchain.network","nimiq.su","enjincoin.sale","tenxwallet.io","golem-network.net","myyethwallet.ml","mywetherwailiet.com","omg-omise.com","district0x.tech","centra-token.com","etherdetla.com","etnerparty.io","etherdelta.su","myetherwallett.neocities.org","myetherwallet-secure.com","myethereumwalletntw.info","real-markets.io","wallet-ethereum.org","request-network.com","shapeshifth.io","shiapeshift.in","coin.red-puise.com","ibittreix.com","coinkbase.com","cindicator.pro","myetherwallet.com.ailogin.me","eventchain.co","kinkik.in","myetherumwalletview.com","protostokenhub.com","coinrbase.com","myetherwalletlogin.com","omisegotoken.com","myethereumwalletntw.com","reall.markets","cobinhood.org","cobinhood.io","happy-coin.org","bitfinex.com.co","bitfienex.com","iconn.foundation","centra.vip","smartcontract.live","icon.community","air-token.com","centra.credit","myetherwallet-singin.com","smartcontractlink.com","shapesshift.io","0xtoken.io","augurproject.co","ethereumus.one","myetherumwalet.com","myetherwalletsignin.com","change-bank.org","charge-bank.com","myetherwalletsingin.com","myetherwalletcontract.com","change-bank.io","chainlink.tech","myetherwallet-confirm.com","tokensale.kybernet.network","kybernet.network","kyberr.network","kybernetwork.io","myetherwalletconfirm.com","kvnuke.github.io","kin.kikpro.co","myethereumwallet.co.uk","tokensale-kyber.network","kyber-network.co","tokensale.kyber-network.co","pyro0.github.io","tokensale.kyber.digital","kyber.digital","omise-go.me","my.etherwallet.com.de","bepartof.change-bank.co","change-bank.co","enigma-tokens.co","coinbase.com.eslogin.co","xn--bittrx-mva.com","ethrdelta.github.io","etherdellta.com","ico-nexus.social","red-pulse.tech","bitj0b.io","xn--bttrex-bwa.com","kin-klk.com","kin-crowdsale.com","ethedelta.com","coindash.su","myethwallet.co.uk","swarm.credit","myethereumwallet.uk","iconexu.social","wanchain.co","enigrna.co","linknetwork.co","qtum-token.com","omisego.com.co","rivetzintl.org","etherdelta.one","the-ether.pro","etherdelta.gitnub.io","kirkik.com","monetha.ltd","vlberate.io","ethereumwallet-kr.info","omise-go.org","iconexus.social","bittirrex.com","aventus.pro","atlant.solutions","aventus.group","metamak.io","omise.com.co","herotokens.io","starbase.pro","etherdelta.githulb.io","herotoken.co","kinico.net","dmarket.ltd","etherdelta.gilthub.io","golem-network.com","etnerscan.io","bllttriex.com","monetha.me","monetha.co","monetha-crowdsale.com","starbase.tech","aventus-crowdsale.com","shapeshift.pro","bllttrex.com","kickico.co","statustoken.im","bilttrex.com","tenxpay.io","bittrex.ltd","metalpay.im","aragon.im","coindash.tech","decentraland.tech","decentraland.pro","status-token.com","bittrex.cam","enigmatoken.com","unocoin.company","unocoin.fund","0xproject.io","0xtoken.com","numerai.tech","decentraiand.org","blockcrein.info","blockchealn.info","bllookchain.info","blockcbhain.info","myetherwallet.com.ethpromonodes.com","mettamask.io","tokenswap.org","netherum.com","etherexx.org","etherume.io","ethereum.plus","ehtereum.org","etereurm.org","etheream.com","ethererum.org","ethereum.io","etherdelta-glthub.com","cryptoalliance.herokuapp.com","bitspark2.com","indorsetoken.com","iconexus.tk","iconexus.ml","iconexus.ga","iconexus.cf","etherwallet.online","wallet-ethereum.net","bitsdigit.com","etherswap.org","eos.ac","uasfwallet.com","ziber.io","multiply-ethereum.info","bittrex.comze.com","karbon.vacau.com","etherdelta.gitlhub.io","etherdelta.glthub.io","digitaldevelopersfund.vacau.com","district-0x.io","coin-dash.com","coindash.ru","district0x.net","aragonproject.io","coin-wallet.info","coinswallet.info","contribute-status.im","ether-api.com","ether-wall.com","mycoinwallet.net","ethereumchamber.com","ethereumchamber.net","ethereumchest.com","ethewallet.com","myetherwallet.com.vc","myetherwallet.com.pe","myetherwallet.us.com","myetherwallet.com.u0387831.cp.regruhosting.ru","myethereumwallet.su","myetherweb.com.de","myetherieumwallet.com","myetehrwallet.com","myeterwalet.com","myetherwaiiet.com","myetherwallet.info","myetherwallet.ch","myetherwallet.om","myethervallet.com","myetherwallet.com.cm","myetherwallet.com.co","myetherwallet.com.de","myetherwallet.com.gl","myetherwallet.com.im","myetherwallet.com.ua","secure-myetherwallet.com","update-myetherwallet.com","wwwmyetherwallet.com","myeatherwallet.com","myetharwallet.com","myelherwallel.com","myetherwaillet.com","myetherwaliet.com","myetherwallel.com","myetherwallet.cam","myetherwallet.cc","myetherwallet.co","myetherwallet.cm","myetherwallet.cz","myetherwallet.org","myetherwallet.tech","myetherwallet.top","myetherwallet.net","myetherwallet.ru.com","myetherwallet.com.ru","metherwallet.com","myetrerwallet.com","myetlerwallet.com","myethterwallet.com","myethwallet.io","myethterwallet.co","myehterwallet.co","myaetherwallet.com","myetthterwallet.com","myetherwallet.one","myelterwallet.com","myetherwallet.gdn","myetherwallt.com","myeterwallet.com","myeteherwallet.com","myethearwailet.com","myetherwallelt.com","myetherwallett.com","etherwallet.org","myetherewallet.com","myeherwallet.com","myethcrwallet.com","myetherwallet.link","myetherwallets.com","myethearwaillet.com","myethearwallet.com","myetherawllet.com","myethereallet.com","myetherswallet.com","myetherwalet.com","myetherwaller.com","myetherwalliet.com","myetherwllet.com","etherwallet.io","myetherwallet.ca","myetherwallet.me","myetherwallet.ru","myetherwallet.xyz","myetherwallte.com","myethirwallet.com","myethrewallet.com","etherwallet.net","maetherwallet.com","meyetherwallet.com","my.ether-wallet.pw","myehterwallet.com","myeitherwallet.com","myelherwallet.com","myeltherwallet.com","myerherwallet.com","myethearwalet.com","myetherewalle.com","myethervvallet.com","myetherwallent.com","myetherwallet.fm","myetherwalllet.com","myetherwalltet.com","myetherwollet.com","myetlherwalet.com","myetlherwallet.com","rnyetherwallet.com","etherclassicwallet.com","omg-omise.co","omise-go.com","omise-go.net","omise-omg.com","omise-go.io","tenx-tech.com","bitclaive.com","tokensale-tenx.tech","ubiqcoin.org","metamask.com","ethtrade.io","myetcwallet.com","account-kigo.net","bitcoin-wallet.net","blocklichan.info","bloclkicihan.info","coindash.ml","eos-bonus.com","eos-io.info","ether-wallet.net","ethereum-wallet.info","ethereum-wallet.net","ethereumchest.net","reservations-kigo.net","reservations-lodgix.com","secure-liverez.com","secure-onerooftop.com","settings-liverez.com","software-liverez.com","software-lodgix.com","unhackableetherwallets.com","www-myetherwallet.com","etherwallet.co.za","etherwalletchain.com","etherwallets.net","etherwallets.nl","my-ethwallet.com","my.ether-wallet.co","myetherwallet.com.am","myetherwallet.com.ht","myetherwalletcom.com","myehterwailet.com","xn--myetherwalle-xoc.com","xn--myetherwalle-44i.com","xn--myetherwalle-xhk.com","xn--myetherwallt-cfb.com","xn--myetherwallt-6tb.com","xn--myetherwallt-xub.com","xn--myetherwallt-ovb.com","xn--myetherwallt-fwb.com","xn--myetherwallt-5wb.com","xn--myetherwallt-jzi.com","xn--myetherwallt-2ck.com","xn--myetherwallt-lok.com","xn--myetherwallt-lsl.com","xn--myetherwallt-ce6f.com","xn--myetherwalet-mcc.com","xn--myetherwalet-xhf.com","xn--myetherwalet-lcc.com","xn--myetherwaet-15ba.com","xn--myetherwalet-whf.com","xn--myetherwaet-v2ea.com","xn--myetherwllet-59a.com","xn--myetherwllet-jbb.com","xn--myetherwllet-wbb.com","xn--myetherwllet-9bb.com","xn--myetherwllet-ncb.com","xn--myetherwllet-0cb.com","xn--myetherwllet-5nb.com","xn--myetherwllet-ktd.com","xn--myetherwllet-mre.com","xn--myetherwllet-76e.com","xn--myetherwllet-o0l.com","xn--myetherwllet-c45f.com","xn--myetherallet-ejn.com","xn--myethewallet-4nf.com","xn--myethewallet-iof.com","xn--myethewallet-mpf.com","xn--myethewallet-6bk.com","xn--myethewallet-i31f.com","xn--myethrwallet-feb.com","xn--myethrwallt-fbbf.com","xn--myethrwallet-seb.com","xn--myethrwallt-rbbf.com","xn--myethrwallet-5eb.com","xn--myethrwallt-3bbf.com","xn--myethrwallet-0tb.com","xn--myethrwallt-tpbf.com","xn--myethrwallet-rub.com","xn--myethrwallt-iqbf.com","xn--myethrwallet-ivb.com","xn--myethrwallt-6qbf.com","xn--myethrwallet-8vb.com","xn--myethrwallt-vrbf.com","xn--myethrwallet-zwb.com","xn--myethrwallt-ksbf.com","xn--myethrwallet-dzi.com","xn--myethrwallt-wbif.com","xn--myethrwallet-wck.com","xn--myethrwallt-skjf.com","xn--myethrwallet-fok.com","xn--myethrwallt-fvjf.com","xn--myethrwallet-fsl.com","xn--myethrwallt-fwkf.com","xn--myethrwallet-5d6f.com","xn--myethrwallt-319ef.com","xn--myeterwallet-ufk.com","xn--myeterwallet-nrl.com","xn--myeterwallet-von.com","xn--myeterwallet-jl6c.com","xn--myeherwallet-ooc.com","xn--myeherwalle-6hci.com","xn--myeherwallet-v4i.com","xn--myeherwalle-zgii.com","xn--myeherwallet-ohk.com","xn--myeherwalle-6oji.com","xn--mytherwallet-ceb.com","xn--mythrwallet-cbbc.com","xn--mythrwallt-c7acf.com","xn--mytherwallet-peb.com","xn--mythrwallet-obbc.com","xn--mythrwallt-n7acf.com","xn--mytherwallet-2eb.com","xn--mythrwallet-0bbc.com","xn--mythrwallt-y7acf.com","xn--mytherwallet-xtb.com","xn--mythrwallet-qpbc.com","xn--mythrwallt-jlbcf.com","xn--mytherwallet-oub.com","xn--mythrwallet-fqbc.com","xn--mythrwallt-5lbcf.com","xn--mythrwallet-3qbc.com","xn--mythrwallt-smbcf.com","xn--mytherwallet-5vb.com","xn--mythrwallet-srbc.com","xn--mythrwallt-fnbcf.com","xn--mytherwallet-wwb.com","xn--mythrwallet-hsbc.com","xn--mythrwallt-1nbcf.com","xn--mytherwallet-9yi.com","xn--mythrwallet-tbic.com","xn--mythrwallt-dnhcf.com","xn--mytherwallet-tck.com","xn--mythrwallet-pkjc.com","xn--mythrwallt-lsicf.com","xn--mytherwallet-cok.com","xn--mythrwallet-cvjc.com","xn--mythrwallt-c2icf.com","xn--mytherwallet-csl.com","xn--mythrwallet-cwkc.com","xn--mythrwallt-c0jcf.com","xn--mytherwallet-2d6f.com","xn--mythrwallet-019ec.com","xn--mythrwallt-yq3ecf.com","xn--metherwallet-qlb.com","xn--metherwallet-1uf.com","xn--metherwallet-iyi.com","xn--metherwallet-zhk.com","xn--metherwallet-3ml.com","xn--mytherwallet-fvb.com","xn--myetherwallt-7db.com","xn--myetherwallt-leb.com","xn--myetherwallt-yeb.com","xn--yetherwallet-vjf.com","xn--yetherwallet-dfk.com","xn--yetherwallet-1t1f.com","xn--yetherwallet-634f.com","xn--myeherwallet-fpc.com","xn--myethewallt-crb.com","xn--metherwallet-1vc.com","xn--myeherwallt-kbb8039g.com","xn--myeherwallet-vk5f.com","xn--yethewallet-iw8ejl.com","xn--bittrx-th8b.com","xn--polniex-n0a.com","thekey.vin","thekey-vip.com","digitexftures.com","ethzero-wallet.org","zeepln.io","wepowers.network","wepower.vision"]}},"CurrencyController":{"currentCurrency":"usd","conversionRate":1112,"conversionDate":1517351401}}} \ No newline at end of file +{"meta":{"version":20},"data":{"config":{},"NetworkController":{"provider":{"type":"mainnet","rpcTarget":"https://mainnet.infura.io/metamask"},"network":"1"},"firstTimeInfo":{"version":"3.12.1","date":1517351427287},"NoticeController":{"noticesList":[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Mon May 08 2017","title":"Privacy Notice","body":"MetaMask is beta software. \n\nWhen you log in to MetaMask, your current account is visible to every new site you visit.\n\nFor your privacy, for now, please sign out of MetaMask when you're done using a site.\n\nAlso, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.\n\n","id":2}]},"BlacklistController":{"phishing":{"version":2,"tolerance":2,"fuzzylist":["metamask.io","myetherwallet.com","cryptokitties.co"],"whitelist":["metahash.io","metahash.net","metahash.org","cryptotitties.com","cryptocities.net","cryptoshitties.co","cryptotitties.fun","cryptokitties.forsale","cryptokitties.care","metamate.cc","metamesh.tech","ico.nexus.social","metamesh.org","metatask.io","metmask.com","metarasa.com","metapack.com","metacase.com","metafas.nl","metamako.com","metamast.com","metamax.ru","metadesk.io","metadisk.com","metallsk.ru","metamag.fr","metamaks.ru","metamap.ru","metamaps.cc","metamats.com","metamax.by","metamax.com","metamax.io","metamuse.net","metarank.com","metaxas.com","megamas2.ru","metamask.io","myetherwallet.com","myethlerwallet.com","ethereum.org","myetheroll.com","myetherapi.com","ledgerwallet.com","databrokerdao.com","etherscan.io","etherid.org","ether.cards","etheroll.com","ethnews.com","ethex.market","ethereumdev.io","ethereumdev.kr","dether.io","ethermine.org","slaask.com","etherbtc.io","ethereal.capital","etherisc.com","m.famalk.net","etherecho.com","ethereum.os.tc","theethereum.wiki","metajack.im","etherhub.io","ethereum.network","ethereum.link","ethereum.com","prethereum.org","ethereumj.io","etheraus.com","ethereum.dev","1ethereum.ru","ethereum.nz","nethereum.com","metabank.com","metamas.com","aventus.io","metabase.com","etherdelta.com","metabase.one","cryptokitties.co"],"blacklist":["myetherwallet.uk.com","kodakone.cc","nyeihitervvallet.com","xn--myeterwalet-cm8eoi.com","nucleus.foundation","beetoken-ico.com","data-token.com","tron-labs.com","ocoin.tech","aionfoundation.com","ico-telegram.org","nyeihitervvallat.com","telegramcoin.us","daddi.cloud","daditoken.com","blockarray.org","dadi-cloud.net","wanchainfunding.org","ico-telegram.io","iconfoundation.site","iost.co","beetoken-ico.eu","cindicator.network","wanchainetwork.org","wamchain.org","wanchainltd.org","wanchainalliance.org","nucleus-vision.net","ledgerwallet.by","nucleuss.vision","myenhterswailct.com","cobin-hood.com","wanchainfoundation.org","xn--polniex-ex4c.com","xn--polniex-s1a.com","xn--polonex-ieb.com","xn--polonex-sza.com","xn--polonex-zw4c.com","xn--polonix-ws4c.com","xn--polonix-y8a.com","xn--pooniex-ojb.com","gramico.info","dimnsions.network","www-gemini.com","login-kucoin.net","venchain.foundation","grampreico.com","tgram.cc","ton-gramico.com","wwwpaywithink.com","coniomi.com","paywithnk.com","paywithlnk.com","iluminatto.com.br","pundix.eu","xn--bttrx-esay.com","xn--bttrex-w8a.com","xn--bnance-bwa.com","xn--shpeshift-11a.com","xn--shapeshif-ts6d.com","xn--shapshift-yf7d.com","wwwbluzelle.com","bluzelie.com","nucleus-vision.org","omisegonetwork.site","etlherzero.com","etlherdelta.com","xn--condesk-0ya.com","xn--condesk-sfb.com","xn--coindsk-vs4c.com","iexecplatform.com","tongramico.com","nucleus-vision.eu","intchain.network","wanchain.cloud","bluzelle-ico.com","ethzero-wallet.com","xn--metherwalle-jb9et7d.com","xn--coinesk-jo3c.com","venchainfoundation.com","myenhtersvvailot.com","ether-zero.net","ins.foundation","nastoken.org","telcointoken.com","ether0.org","eterzero.org","bluzelle-ico.eu","bleuzelle.com","appcoinstoken.org","xn--quanstamp-8s6d.com","myehntersvvailct.com","myeherwalllet.com","ico-bluzelle.com","bluzelle.im","bluzelle.one","bluzele.sale","bluzele.co","sether.ws","xn--myetherwalet-6gf.com","xn--rnyethewaliet-om1g.com","rnyethervailet.com","mvetherwaliet.com","rnyetherwailet.com","myethervaliet.com","rnyethervaliet.com","mvetherwalilet.com","xn--myethewalie-3ic0947g.com","xn--mthrwallet-z6ac3y.com","xn--myeherwalie-vici.com","xn--myethervvalie-8vc.com","xn--mythrwallt-06acf.com","xn--mtherwallet-y9a6y.com","myetherwallet.applytoken.tk","ethereum-zero.com","quanstamptoken.tk","bluzelle.network","ether-wallet.org","tron-wallet.info","appcoinsproject.com","vechain.foundation","tronlab.site","tronlabs.network","bluzelle.cc","ethblender.com","ethpaperwallet.net","waltontoken.org","icoselfkey.org","etherzeroclaim.com","etherzero.promo","bluzelle.pro","token-selfkey.org","xn--etherdlta-0f7d.com","sether.in","xn--ttrex-ysa9423c.com","bluzelle.eu","bluzelle.site","gifto.tech","xn--os-g7s.com","selfkey.co","xn--myeherwalet-ns8exy.com","xn--coinelegraph-wk5f.com","dai-stablecoin.com","eos-token.org","venchain.org","gatcoins.io","deepbrainchain.co","myetherwalililet.info","myehvterwallet.com","myehterumswallet.com","nucleusico.com","tronlab.tech","0x-project.com","gift-token-events.mywebcommunity.org","funfairtoken.org","breadtokenapp.com","cloudpetstore.com","myethwalilet.com","selfkeys.org","wallet-ethereum.com","xn--methrwallt-26ar0z.com","xn--mytherwllet-r8a0c.com","bluzelle.promo","tokensale.bluzelle.promo","cedarlake.org","marketingleads4u.com","cashaa.co","xn--inance-hrb.com","wanchain.tech","zenprolocol.com","ethscan.io","etherscan.in","props-project.com","zilliaq.com","reqestnetwork.com","etherdelta.pw","ethereum-giveaway.org","mysimpletoken.org","binancc.com","blnance.org","elherdelta.io","xn--hapeshit-ez9c2y.com","tenxwallet.co","singularitynet.info","mytlherwaliet.info","iconmainnet.ml","tokenselfkey.org","xn--myetewallet-cm8e5y.com","envione.org","myetherwalletet.com","claimbcd.com","ripiocreditnetwork.in","xn--yeterwallet-ml8euo.com","ethclassicwallet.info","myltherwallet.ru.com","etherdella.com","xn--yeterwallet-bm8ewn.com","singularty.net","cloudkitties.co","iconfoundation.io","kittystat.com","gatscoin.io","singularitynet.in","sale.canay.io","canay.io","wabicoin.co","envion.top","sirinslabs.com","tronlab.co","paxful.com.ng","changellyli.com","ethereum-code.com","xn--plonex-6va6c.com","envion.co","envion.cc","envion.site","ethereumchain.info","xn--envon-1sa.org","xn--btstamp-rfb.net","envlon.org","envion-ico.org","spectivvr.org","sirinlbs.com","ethereumdoubler.life","xn--myetherwllet-fnb.com","sirin-labs.com","sirin-labs.org","envion.one","envion.live","propsproject.org","propsprojects.com","decentralland.org","xn--metherwalet-ns8ep4b.com","redpulsetoken.co","propsproject.tech","xn--myeterwalet-nl8emj.com","powrerledger.com","cryptokitties.com","sirinlabs.pro","sirinlabs.co","sirnlabs.com","superbitcoin-blockchain.info","hellobloom.me","mobus.network","powrrledger.com","xn--myeherwalet-ms8eyy.com","qlink-ico.com","gatcoin.in","tokensale.gamefllp.com","gamefllp.com","xn--myeherwalle-vici.com","xn--myetherwalet-39b.com","xn--polonex-ffb.com","xn--birex-leba.com","raiden-network.org","sirintabs.com","xn--metherwallt-79a30a.com","xn--myethrwllet-2kb3p.com","myethlerwallet.eu","xn--btrex-b4a.com","powerrledger.com","xn--cointeegraph-wz4f.com","myerherwalet.com","qauntstanp.com","myetherermwallet.com","xn--myethewalet-ns8eqq.com","xn--nvion-hza.org","nnyetherwallelt.ru.com","ico-wacoin.com","xn--myeterwalet-nl8enj.com","bitcoinsilver.io","t0zero.com","tokensale.gizer.in","gizer.in","wabitoken.com","gladius.ws","xn--metherwallt-8bb4w.com","quanttstamp.com","gladius.im","ethereumstorage.net","powerledgerr.com","xn--myeherwallet-4j5f.com","quamtstamp.com","quntstamp.com","xn--changely-j59c.com","shapeshlft.com","coinbasenews.co.uk","xn--metherwallet-hmb.com","envoin.org","powerledger.com","bitstannp.net","xn--myetherallet-4k5fwn.com","xn--coinbas-pya.com","requestt.network","oracls.network","sirinlabs.website","powrledger.io","slackconfirm.com","shape-shift.io","oracles-network.org","xn--myeherwalle-zb9eia.com","blockstack.one","urtust.io","bittrex.one","t0-ico.com","xn--cinbase-90a.com","xn--metherwalet-ns8ez1g.com","tzero-ico.com","tzero.su","tzero.website","blockstack.network","ico-tzero.com","spectre.site","tzero.pw","spectre-ai.net","xn--waxtokn-y8a.com","dmarket.pro","bittrex.com11648724328774.cf","bittrex.com1987465798.ga","autcus.org","t-zero.org","xn--zero-zxb.com","myetherwalletfork.com","blokclbain.info","datum.sale","spectre-ai.org","powerledgr.com","simpletoken.live","sale.simpletoken.live","qauntstamp.com","raiden-network.com","metalpayme.com","quantstamp-ico.com","myetherwailetclient.com","biockchain.biz","wallets-blockchain.com","golemairdrop.com","omisegoairdrop.net","blodkchainwallet.info","walton-chain.org","elite888-ico.com","bitflyerjp.com","chainlinksmartcontract.com","stormtoken.eu","omise-go.tech","saltending.com","stormltoken.com","xn--quanttamp-42b.com","stormtoken.co","storntoken.com","stromtoken.com","storm-token.com","stormtokens.io","ether-delta.com","ethconnect.live","ethconnect.trade","xn--bttrex-3va.net","quantstamp.com.co","wancha.in","augur-network.com","quantstamp.com.ua","myetherwalletmew.com","myetherumwalletts.com","xn--quanstamp-tmd.com","quantsstamps.com","changellyl.net","xn--myetherwalet-1fb.com","myethereumwallets.com","xn--myetherwalet-e9b.com","quantslamp.com","metelpay.com","xn--eterdelta-m75d.com","linksmartcontract.com","myetherwalletaccess.com","myetherwalletcheck.com","myetherwalletcheck.info","myetherwalletconf.com","myetherwalleteal.com","myetherwalletec.com","myetherwalletgeth.com","myetherwalletmetamask.com","myetherwalletmm.com","myetherwalletmy.com","myetherwalletnh.com","myetherwalletnod.com","myetherwalletrr.com","myetherwalletrty.com","myetherwalletsec.com","myetherwalletsecure.com","myetherwalletutc.com","myetherwalletver.info","myetherwalletview.com","myetherwalletview.info","myetherwalletvrf.com","myetherwalletmist.com","myetherwalletext.com","myetherwalletjson.com","mettalpay.com","bricklblock.io","bittrexy.com","utrust.so","myethierwallet.org","metallpay.com","kraken-wallet.com","dmarkt.io","etherdeltla.com","unlversa.io","universa.sale","mercuryprotocol.live","ripiocredlt.network","myetlherwa11et.com","dentacoin.in","rdrtg.com","myetherwallet.com.rdrgh.com","rdrgh.com","ripiocreditnetwork.co","riaden.network","hydrominer.biz","rdrblock.com","reqest.network","senstoken.com","myetherwallat.services","ripiocredit.net","xn--metherwallet-c06f.com","ico.ripiocredits.com","ripiocredits.com","raidens.network","artoken.co","myetherwalletlgn.com","etherblog.click","stormtoken.site","httpmyetherwallet.com","myetherwalletverify.com","byzantiumfork.com","myetherwallet.com.byzantiumfork.com","www-myethervvallet.com","ether24.info","block-v.io","bittrex.cash","shapishift.io","ripiocerdit.network","rnyetherwa11et.com","claimether.com","enigmatokensale.com","ethereum-org.com","mvetnerwallet.com","myctherwallet.com","myetherwaltet.com","myetherwatlet.com","privatix.me","myetherwalletcnf.com","myetherwalletver.com","privatix.top","privatix.pro","privatex.io","stormtoken.cc","raiden.online","stormstoken.com","myetereumwallet.com","stormtokens.net","myetherwalletconf.info","storrntoken.com","worldofbattles.io","ico.worldofbattles.io","privatix.live","riden.network","raidan.network","ralden.network","mymyetherwallet.com","myetherwallets.net","myetherwalletverify.info","stormxtoken.com","myethereum-wallet.com","myetherwallet-forkprep.pagedemo.co","myetnerwailet.com","www-mvetherwallet.com","etheirdelta.com","myetherwalletiu.com","myetherwaiiett.com","xn--mytherwalet-cbb87i.com","xn--myethrwallet-ivb.co","xn--myeterwallet-f1b.com","myehterwaliet.com","omegaone.co","myetherwaiietw.com","slack.com.ru","polkodot.network","request-network.net","requestnetwork.live","binancie.com","first-eth.info","myewerthwalliet.com","enjincoin.pw","xn--bitrex-k17b.com","alrswap.io","www-request.network","myetnenwallet.com","www-enigma.co","cryptoinsidenews.com","air-swap.tech","launch.airswap.cc","airswap.cc","airswaptoken.com","launch.airswap.in","airswap.in","security-steemit.com.mx","blockchalnwallet.com","blodkchainwallet.com","blodkchaln.com","myethereumwaiiet.com","myethereumwaliet.com","myethereumwalilet.com","myetherswailet.com","myetherswaliet.com","myetherswalilet.com","myetherwalilett.com","myetherwalletl.com","myetherwalletww.com","myethereunwallet.com","myethereumwallct.com","myetherwaiieti.com","myetherwaiiete.com","upfirng.com","paypie.net","paypie.tech","soam.co","myetherwaiict.com","numerai-token.com","www-bankera.com","vvanchain.org","omisegoairdrop.com","xn--enjncoin-41a.io","suncontract.su","myetherwaiietr.com","shapeshiff.io","warchain.org","myethwallett.com","myethervvaliet.com","wanchains.org","etherparty.in","enjincoin.me","etiam.io","invest.smartlands.tech","smartlands.tech","enijncoin.io","wanchain.network","nimiq.su","enjincoin.sale","tenxwallet.io","golem-network.net","myyethwallet.ml","mywetherwailiet.com","omg-omise.com","district0x.tech","centra-token.com","etherdetla.com","etnerparty.io","etherdelta.su","myetherwallett.neocities.org","myetherwallet-secure.com","myethereumwalletntw.info","real-markets.io","wallet-ethereum.org","request-network.com","shapeshifth.io","shiapeshift.in","coin.red-puise.com","ibittreix.com","coinkbase.com","cindicator.pro","myetherwallet.com.ailogin.me","eventchain.co","kinkik.in","myetherumwalletview.com","protostokenhub.com","coinrbase.com","myetherwalletlogin.com","omisegotoken.com","myethereumwalletntw.com","reall.markets","cobinhood.org","cobinhood.io","happy-coin.org","bitfinex.com.co","bitfienex.com","iconn.foundation","centra.vip","smartcontract.live","icon.community","air-token.com","centra.credit","myetherwallet-singin.com","smartcontractlink.com","shapesshift.io","0xtoken.io","augurproject.co","ethereumus.one","myetherumwalet.com","myetherwalletsignin.com","change-bank.org","charge-bank.com","myetherwalletsingin.com","myetherwalletcontract.com","change-bank.io","chainlink.tech","myetherwallet-confirm.com","tokensale.kybernet.network","kybernet.network","kyberr.network","kybernetwork.io","myetherwalletconfirm.com","kvnuke.github.io","kin.kikpro.co","myethereumwallet.co.uk","tokensale-kyber.network","kyber-network.co","tokensale.kyber-network.co","pyro0.github.io","tokensale.kyber.digital","kyber.digital","omise-go.me","my.etherwallet.com.de","bepartof.change-bank.co","change-bank.co","enigma-tokens.co","coinbase.com.eslogin.co","xn--bittrx-mva.com","ethrdelta.github.io","etherdellta.com","ico-nexus.social","red-pulse.tech","bitj0b.io","xn--bttrex-bwa.com","kin-klk.com","kin-crowdsale.com","ethedelta.com","coindash.su","myethwallet.co.uk","swarm.credit","myethereumwallet.uk","iconexu.social","wanchain.co","enigrna.co","linknetwork.co","qtum-token.com","omisego.com.co","rivetzintl.org","etherdelta.one","the-ether.pro","etherdelta.gitnub.io","kirkik.com","monetha.ltd","vlberate.io","ethereumwallet-kr.info","omise-go.org","iconexus.social","bittirrex.com","aventus.pro","atlant.solutions","aventus.group","metamak.io","omise.com.co","herotokens.io","starbase.pro","etherdelta.githulb.io","herotoken.co","kinico.net","dmarket.ltd","etherdelta.gilthub.io","golem-network.com","etnerscan.io","bllttriex.com","monetha.me","monetha.co","monetha-crowdsale.com","starbase.tech","aventus-crowdsale.com","shapeshift.pro","bllttrex.com","kickico.co","statustoken.im","bilttrex.com","tenxpay.io","bittrex.ltd","metalpay.im","aragon.im","coindash.tech","decentraland.tech","decentraland.pro","status-token.com","bittrex.cam","enigmatoken.com","unocoin.company","unocoin.fund","0xproject.io","0xtoken.com","numerai.tech","decentraiand.org","blockcrein.info","blockchealn.info","bllookchain.info","blockcbhain.info","myetherwallet.com.ethpromonodes.com","mettamask.io","tokenswap.org","netherum.com","etherexx.org","etherume.io","ethereum.plus","ehtereum.org","etereurm.org","etheream.com","ethererum.org","ethereum.io","etherdelta-glthub.com","cryptoalliance.herokuapp.com","bitspark2.com","indorsetoken.com","iconexus.tk","iconexus.ml","iconexus.ga","iconexus.cf","etherwallet.online","wallet-ethereum.net","bitsdigit.com","etherswap.org","eos.ac","uasfwallet.com","ziber.io","multiply-ethereum.info","bittrex.comze.com","karbon.vacau.com","etherdelta.gitlhub.io","etherdelta.glthub.io","digitaldevelopersfund.vacau.com","district-0x.io","coin-dash.com","coindash.ru","district0x.net","aragonproject.io","coin-wallet.info","coinswallet.info","contribute-status.im","ether-api.com","ether-wall.com","mycoinwallet.net","ethereumchamber.com","ethereumchamber.net","ethereumchest.com","ethewallet.com","myetherwallet.com.vc","myetherwallet.com.pe","myetherwallet.us.com","myetherwallet.com.u0387831.cp.regruhosting.ru","myethereumwallet.su","myetherweb.com.de","myetherieumwallet.com","myetehrwallet.com","myeterwalet.com","myetherwaiiet.com","myetherwallet.info","myetherwallet.ch","myetherwallet.om","myethervallet.com","myetherwallet.com.cm","myetherwallet.com.co","myetherwallet.com.de","myetherwallet.com.gl","myetherwallet.com.im","myetherwallet.com.ua","secure-myetherwallet.com","update-myetherwallet.com","wwwmyetherwallet.com","myeatherwallet.com","myetharwallet.com","myelherwallel.com","myetherwaillet.com","myetherwaliet.com","myetherwallel.com","myetherwallet.cam","myetherwallet.cc","myetherwallet.co","myetherwallet.cm","myetherwallet.cz","myetherwallet.org","myetherwallet.tech","myetherwallet.top","myetherwallet.net","myetherwallet.ru.com","myetherwallet.com.ru","metherwallet.com","myetrerwallet.com","myetlerwallet.com","myethterwallet.com","myethwallet.io","myethterwallet.co","myehterwallet.co","myaetherwallet.com","myetthterwallet.com","myetherwallet.one","myelterwallet.com","myetherwallet.gdn","myetherwallt.com","myeterwallet.com","myeteherwallet.com","myethearwailet.com","myetherwallelt.com","myetherwallett.com","etherwallet.org","myetherewallet.com","myeherwallet.com","myethcrwallet.com","myetherwallet.link","myetherwallets.com","myethearwaillet.com","myethearwallet.com","myetherawllet.com","myethereallet.com","myetherswallet.com","myetherwalet.com","myetherwaller.com","myetherwalliet.com","myetherwllet.com","etherwallet.io","myetherwallet.ca","myetherwallet.me","myetherwallet.ru","myetherwallet.xyz","myetherwallte.com","myethirwallet.com","myethrewallet.com","etherwallet.net","maetherwallet.com","meyetherwallet.com","my.ether-wallet.pw","myehterwallet.com","myeitherwallet.com","myelherwallet.com","myeltherwallet.com","myerherwallet.com","myethearwalet.com","myetherewalle.com","myethervvallet.com","myetherwallent.com","myetherwallet.fm","myetherwalllet.com","myetherwalltet.com","myetherwollet.com","myetlherwalet.com","myetlherwallet.com","rnyetherwallet.com","etherclassicwallet.com","omg-omise.co","omise-go.com","omise-go.net","omise-omg.com","omise-go.io","tenx-tech.com","bitclaive.com","tokensale-tenx.tech","ubiqcoin.org","metamask.com","ethtrade.io","myetcwallet.com","account-kigo.net","bitcoin-wallet.net","blocklichan.info","bloclkicihan.info","coindash.ml","eos-bonus.com","eos-io.info","ether-wallet.net","ethereum-wallet.info","ethereum-wallet.net","ethereumchest.net","reservations-kigo.net","reservations-lodgix.com","secure-liverez.com","secure-onerooftop.com","settings-liverez.com","software-liverez.com","software-lodgix.com","unhackableetherwallets.com","www-myetherwallet.com","etherwallet.co.za","etherwalletchain.com","etherwallets.net","etherwallets.nl","my-ethwallet.com","my.ether-wallet.co","myetherwallet.com.am","myetherwallet.com.ht","myetherwalletcom.com","myehterwailet.com","xn--myetherwalle-xoc.com","xn--myetherwalle-44i.com","xn--myetherwalle-xhk.com","xn--myetherwallt-cfb.com","xn--myetherwallt-6tb.com","xn--myetherwallt-xub.com","xn--myetherwallt-ovb.com","xn--myetherwallt-fwb.com","xn--myetherwallt-5wb.com","xn--myetherwallt-jzi.com","xn--myetherwallt-2ck.com","xn--myetherwallt-lok.com","xn--myetherwallt-lsl.com","xn--myetherwallt-ce6f.com","xn--myetherwalet-mcc.com","xn--myetherwalet-xhf.com","xn--myetherwalet-lcc.com","xn--myetherwaet-15ba.com","xn--myetherwalet-whf.com","xn--myetherwaet-v2ea.com","xn--myetherwllet-59a.com","xn--myetherwllet-jbb.com","xn--myetherwllet-wbb.com","xn--myetherwllet-9bb.com","xn--myetherwllet-ncb.com","xn--myetherwllet-0cb.com","xn--myetherwllet-5nb.com","xn--myetherwllet-ktd.com","xn--myetherwllet-mre.com","xn--myetherwllet-76e.com","xn--myetherwllet-o0l.com","xn--myetherwllet-c45f.com","xn--myetherallet-ejn.com","xn--myethewallet-4nf.com","xn--myethewallet-iof.com","xn--myethewallet-mpf.com","xn--myethewallet-6bk.com","xn--myethewallet-i31f.com","xn--myethrwallet-feb.com","xn--myethrwallt-fbbf.com","xn--myethrwallet-seb.com","xn--myethrwallt-rbbf.com","xn--myethrwallet-5eb.com","xn--myethrwallt-3bbf.com","xn--myethrwallet-0tb.com","xn--myethrwallt-tpbf.com","xn--myethrwallet-rub.com","xn--myethrwallt-iqbf.com","xn--myethrwallet-ivb.com","xn--myethrwallt-6qbf.com","xn--myethrwallet-8vb.com","xn--myethrwallt-vrbf.com","xn--myethrwallet-zwb.com","xn--myethrwallt-ksbf.com","xn--myethrwallet-dzi.com","xn--myethrwallt-wbif.com","xn--myethrwallet-wck.com","xn--myethrwallt-skjf.com","xn--myethrwallet-fok.com","xn--myethrwallt-fvjf.com","xn--myethrwallet-fsl.com","xn--myethrwallt-fwkf.com","xn--myethrwallet-5d6f.com","xn--myethrwallt-319ef.com","xn--myeterwallet-ufk.com","xn--myeterwallet-nrl.com","xn--myeterwallet-von.com","xn--myeterwallet-jl6c.com","xn--myeherwallet-ooc.com","xn--myeherwalle-6hci.com","xn--myeherwallet-v4i.com","xn--myeherwalle-zgii.com","xn--myeherwallet-ohk.com","xn--myeherwalle-6oji.com","xn--mytherwallet-ceb.com","xn--mythrwallet-cbbc.com","xn--mythrwallt-c7acf.com","xn--mytherwallet-peb.com","xn--mythrwallet-obbc.com","xn--mythrwallt-n7acf.com","xn--mytherwallet-2eb.com","xn--mythrwallet-0bbc.com","xn--mythrwallt-y7acf.com","xn--mytherwallet-xtb.com","xn--mythrwallet-qpbc.com","xn--mythrwallt-jlbcf.com","xn--mytherwallet-oub.com","xn--mythrwallet-fqbc.com","xn--mythrwallt-5lbcf.com","xn--mythrwallet-3qbc.com","xn--mythrwallt-smbcf.com","xn--mytherwallet-5vb.com","xn--mythrwallet-srbc.com","xn--mythrwallt-fnbcf.com","xn--mytherwallet-wwb.com","xn--mythrwallet-hsbc.com","xn--mythrwallt-1nbcf.com","xn--mytherwallet-9yi.com","xn--mythrwallet-tbic.com","xn--mythrwallt-dnhcf.com","xn--mytherwallet-tck.com","xn--mythrwallet-pkjc.com","xn--mythrwallt-lsicf.com","xn--mytherwallet-cok.com","xn--mythrwallet-cvjc.com","xn--mythrwallt-c2icf.com","xn--mytherwallet-csl.com","xn--mythrwallet-cwkc.com","xn--mythrwallt-c0jcf.com","xn--mytherwallet-2d6f.com","xn--mythrwallet-019ec.com","xn--mythrwallt-yq3ecf.com","xn--metherwallet-qlb.com","xn--metherwallet-1uf.com","xn--metherwallet-iyi.com","xn--metherwallet-zhk.com","xn--metherwallet-3ml.com","xn--mytherwallet-fvb.com","xn--myetherwallt-7db.com","xn--myetherwallt-leb.com","xn--myetherwallt-yeb.com","xn--yetherwallet-vjf.com","xn--yetherwallet-dfk.com","xn--yetherwallet-1t1f.com","xn--yetherwallet-634f.com","xn--myeherwallet-fpc.com","xn--myethewallt-crb.com","xn--metherwallet-1vc.com","xn--myeherwallt-kbb8039g.com","xn--myeherwallet-vk5f.com","xn--yethewallet-iw8ejl.com","xn--bittrx-th8b.com","xn--polniex-n0a.com","thekey.vin","thekey-vip.com","digitexftures.com","ethzero-wallet.org","zeepln.io","wepowers.network","wepower.vision"]}},"CurrencyController":{"currentCurrency":"usd","conversionRate":1112,"conversionDate":1517351401}}} diff --git a/test/lib/mock-encryptor.js b/test/lib/mock-encryptor.js index ef229a82f..48aa9e52c 100644 --- a/test/lib/mock-encryptor.js +++ b/test/lib/mock-encryptor.js @@ -31,6 +31,6 @@ module.exports = { getRandomValues () { return 'SOO RANDO!!!1' - } + }, } diff --git a/test/lib/mock-tx-gen.js b/test/lib/mock-tx-gen.js index 7aea09c59..106101500 100644 --- a/test/lib/mock-tx-gen.js +++ b/test/lib/mock-tx-gen.js @@ -17,14 +17,14 @@ class TxGenerator { } generate (tx = {}, opts = {}) { - let { count, fromNonce } = opts + const { count, fromNonce } = opts let nonce = fromNonce || this.txs.length - let txs = [] + const txs = [] for (let i = 0; i < count; i++) { txs.push(extend(template, { txParams: { nonce: hexify(nonce++), - } + }, }, tx)) } this.txs = this.txs.concat(txs) diff --git a/test/lib/react-trigger-change.js b/test/lib/react-trigger-change.js index a25ddff00..d169dd614 100644 --- a/test/lib/react-trigger-change.js +++ b/test/lib/react-trigger-change.js @@ -1,7 +1,7 @@ // Trigger React's synthetic change events on input, textarea and select elements // https://github.com/vitalyq/react-trigger-change -/******************IMPORTANT NOTE******************/ +/** ****************IMPORTANT NOTE******************/ /* This file is a modification of the */ /* 'react-trigger-change' library linked above. */ /* That library breaks when 'onFocus' events are */ @@ -11,13 +11,13 @@ /* This modification removes the accomodations */ /* 'react-trigger-change' makes for IE to ensure */ /* our tests can pass in chrome and firefox. */ -/**************************************************/ +/** ************************************************/ -'use strict'; +'use strict' // Constants and functions are declared inside the closure. // In this way, reactTriggerChange can be passed directly to executeScript in Selenium. -module.exports = function reactTriggerChange(node) { +module.exports = function reactTriggerChange (node) { var supportedInputTypes = { color: true, date: true, @@ -33,47 +33,47 @@ module.exports = function reactTriggerChange(node) { text: true, time: true, url: true, - week: true - }; - var nodeName = node.nodeName.toLowerCase(); - var type = node.type; - var event; - var descriptor; - var initialValue; - var initialChecked; - var initialCheckedRadio; + week: true, + } + var nodeName = node.nodeName.toLowerCase() + var type = node.type + var event + var descriptor + var initialValue + var initialChecked + var initialCheckedRadio // Do not try to delete non-configurable properties. // Value and checked properties on DOM elements are non-configurable in PhantomJS. - function deletePropertySafe(elem, prop) { - var desc = Object.getOwnPropertyDescriptor(elem, prop); + function deletePropertySafe (elem, prop) { + var desc = Object.getOwnPropertyDescriptor(elem, prop) if (desc && desc.configurable) { - delete elem[prop]; + delete elem[prop] } } - function getCheckedRadio(radio) { - var name = radio.name; - var radios; - var i; + function getCheckedRadio (radio) { + var name = radio.name + var radios + var i if (name) { - radios = document.querySelectorAll('input[type="radio"][name="' + name + '"]'); + radios = document.querySelectorAll('input[type="radio"][name="' + name + '"]') for (i = 0; i < radios.length; i += 1) { if (radios[i].checked) { - return radios[i] !== radio ? radios[i] : null; + return radios[i] !== radio ? radios[i] : null } } } - return null; + return null } - function preventChecking(e) { - e.preventDefault(); + function preventChecking (e) { + e.preventDefault() if (!initialChecked) { - e.target.checked = false; + e.target.checked = false } if (initialCheckedRadio) { - initialCheckedRadio.checked = true; + initialCheckedRadio.checked = true } } @@ -81,81 +81,81 @@ module.exports = function reactTriggerChange(node) { (nodeName === 'input' && type === 'file')) { // IE9-IE11, non-IE // Dispatch change. - event = document.createEvent('HTMLEvents'); - event.initEvent('change', true, false); - node.dispatchEvent(event); + event = document.createEvent('HTMLEvents') + event.initEvent('change', true, false) + node.dispatchEvent(event) } else if ((nodeName === 'input' && supportedInputTypes[type]) || nodeName === 'textarea') { // React 16 // Cache artificial value property descriptor. // Property doesn't exist in React <16, descriptor is undefined. - descriptor = Object.getOwnPropertyDescriptor(node, 'value'); + descriptor = Object.getOwnPropertyDescriptor(node, 'value') // Update inputValueTracking cached value. // Remove artificial value property. // Restore initial value to trigger event with it. - initialValue = node.value; - node.value = initialValue + '#'; - deletePropertySafe(node, 'value'); - node.value = initialValue; + initialValue = node.value + node.value = initialValue + '#' + deletePropertySafe(node, 'value') + node.value = initialValue // React 0.14: IE10-IE11, non-IE // React 15: non-IE // React 16: IE10-IE11, non-IE - event = document.createEvent('HTMLEvents'); - event.initEvent('input', true, false); - node.dispatchEvent(event); + event = document.createEvent('HTMLEvents') + event.initEvent('input', true, false) + node.dispatchEvent(event) // React 16 // Restore artificial value property descriptor. if (descriptor) { - Object.defineProperty(node, 'value', descriptor); + Object.defineProperty(node, 'value', descriptor) } } else if (nodeName === 'input' && type === 'checkbox') { // Invert inputValueTracking cached value. - node.checked = !node.checked; + node.checked = !node.checked // Dispatch click. // Click event inverts checked value. - event = document.createEvent('MouseEvents'); - event.initEvent('click', true, true); - node.dispatchEvent(event); + event = document.createEvent('MouseEvents') + event.initEvent('click', true, true) + node.dispatchEvent(event) } else if (nodeName === 'input' && type === 'radio') { // Cache initial checked value. - initialChecked = node.checked; + initialChecked = node.checked // Find and cache initially checked radio in the group. - initialCheckedRadio = getCheckedRadio(node); + initialCheckedRadio = getCheckedRadio(node) // React 16 // Cache property descriptor. // Invert inputValueTracking cached value. // Remove artificial checked property. // Restore initial value, otherwise preventDefault will eventually revert the value. - descriptor = Object.getOwnPropertyDescriptor(node, 'checked'); - node.checked = !initialChecked; - deletePropertySafe(node, 'checked'); - node.checked = initialChecked; + descriptor = Object.getOwnPropertyDescriptor(node, 'checked') + node.checked = !initialChecked + deletePropertySafe(node, 'checked') + node.checked = initialChecked // Prevent toggling during event capturing phase. // Set checked value to false if initialChecked is false, // otherwise next listeners will see true. // Restore initially checked radio in the group. - node.addEventListener('click', preventChecking, true); + node.addEventListener('click', preventChecking, true) // Dispatch click. // Click event inverts checked value. - event = document.createEvent('MouseEvents'); - event.initEvent('click', true, true); - node.dispatchEvent(event); + event = document.createEvent('MouseEvents') + event.initEvent('click', true, true) + node.dispatchEvent(event) // Remove listener to stop further change prevention. - node.removeEventListener('click', preventChecking, true); + node.removeEventListener('click', preventChecking, true) // React 16 // Restore artificial checked property descriptor. if (descriptor) { - Object.defineProperty(node, 'checked', descriptor); + Object.defineProperty(node, 'checked', descriptor) } } -}; +} diff --git a/test/lib/util.js b/test/lib/util.js index 626280745..858565bb9 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -11,7 +11,7 @@ function timeout (time) { }) } -async function findAsync(container, selector, opts) { +async function findAsync (container, selector, opts) { try { return await pollUntilTruthy(() => { const result = container.find(selector) @@ -22,7 +22,7 @@ async function findAsync(container, selector, opts) { } } -async function queryAsync(jQuery, selector, opts) { +async function queryAsync (jQuery, selector, opts) { try { return await pollUntilTruthy(() => { const result = jQuery(selector) @@ -33,7 +33,7 @@ async function queryAsync(jQuery, selector, opts) { } } -async function pollUntilTruthy(fn, opts = {}){ +async function pollUntilTruthy (fn, opts = {}) { const pollingInterval = opts.pollingInterval || 100 const timeoutInterval = opts.timeoutInterval || 5000 const start = Date.now() diff --git a/test/mascara.conf.js b/test/mascara.conf.js index 97e53fc2b..faf3147bd 100644 --- a/test/mascara.conf.js +++ b/test/mascara.conf.js @@ -1,13 +1,13 @@ const getBaseConfig = require('./base.conf.js') -module.exports = function(config) { +module.exports = function (config) { const settings = getBaseConfig(config) // ui and tests settings.files.push('dist/mascara/ui.js') settings.files.push('dist/mascara/tests.js') // service worker background - settings.files.push({ pattern: 'dist/mascara/background.js', watched: false, included: false, served: true }), + settings.files.push({ pattern: 'dist/mascara/background.js', watched: false, included: false, served: true }) settings.proxies['/background.js'] = '/base/dist/mascara/background.js' // use this to keep the browser open for debugging diff --git a/test/screens/new-ui.js b/test/screens/new-ui.js index e3ba7f6ab..92d1b8378 100644 --- a/test/screens/new-ui.js +++ b/test/screens/new-ui.js @@ -12,7 +12,7 @@ const pngFileStream = require('png-file-stream') const sizeOfPng = require('image-size/lib/types/png') const By = webdriver.By const localesIndex = require('../../app/_locales/index.json') -const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('../e2e/func') +const { delay, buildChromeWebDriver, getExtensionIdChrome } = require('../e2e/func') const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545')) @@ -41,11 +41,9 @@ captureAllScreens() }) -async function captureAllScreens() { +async function captureAllScreens () { // common names - let button let tabs - let element await cleanScreenShotDir() @@ -108,7 +106,7 @@ async function captureAllScreens() { await captureLanguageScreenShots('terms') await delay(300) - element = driver.findElement(By.linkText('Attributions')) + const element = driver.findElement(By.linkText('Attributions')) await driver.executeScript('arguments[0].scrollIntoView(true)', element) await delay(300) await captureLanguageScreenShots('terms-scrolled') @@ -134,10 +132,10 @@ async function captureAllScreens() { // enter seed phrase const seedPhraseButtons = await driver.findElements(By.css('.backup-phrase__confirm-seed-options > button')) const seedPhraseButtonWords = await Promise.all(seedPhraseButtons.map(button => button.getText())) - for (let targetWord of seedPhraseWords) { + for (const targetWord of seedPhraseWords) { const wordIndex = seedPhraseButtonWords.indexOf(targetWord) if (wordIndex === -1) throw new Error(`Captured seed phrase word "${targetWord}" not in found seed phrase button options ${seedPhraseButtonWords.join(' ')}`) - await driver.findElement(By.css(`.backup-phrase__confirm-seed-options > button:nth-child(${wordIndex+1})`)).click() + await driver.findElement(By.css(`.backup-phrase__confirm-seed-options > button:nth-child(${wordIndex + 1})`)).click() await delay(100) } await captureLanguageScreenShots('confirm secret backup phrase - words selected correctly') @@ -191,11 +189,11 @@ async function captureAllScreens() { } -async function captureLanguageScreenShots(label) { +async function captureLanguageScreenShots (label) { const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en') // take english shot await captureScreenShot(`${label} (en)`) - for (let localeMeta of nonEnglishLocales) { + for (const localeMeta of nonEnglishLocales) { // set locale and take shot await setLocale(localeMeta.code) await delay(300) @@ -206,19 +204,19 @@ async function captureLanguageScreenShots(label) { await delay(300) } -async function setLocale(code) { +async function setLocale (code) { await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code) } -async function setProviderType(type) { +async function setProviderType (type) { await driver.executeScript('window.metamask.setProviderType(arguments[0])', type) } -async function cleanScreenShotDir() { +async function cleanScreenShotDir () { await pify(rimraf)(`./test-artifacts/screens/`) } -async function captureScreenShot(label) { +async function captureScreenShot (label) { const shotIndex = screenshotCount.toString().padStart(4, '0') screenshotCount++ const artifactDir = `./test-artifacts/screens/` @@ -228,7 +226,7 @@ async function captureScreenShot(label) { await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' }) } -async function generateGif(){ +async function generateGif () { // calculate screenshot size const screenshot = await driver.takeScreenshot() const pngBuffer = Buffer.from(screenshot, 'base64') @@ -244,7 +242,7 @@ async function generateGif(){ await pify(endOfStream)(stream) } -async function verboseReportOnFailure(test) { +async function verboseReportOnFailure (test) { const artifactDir = `./test-artifacts/${test.title}` const filepathBase = `${artifactDir}/test-failure` await pify(mkdirp)(artifactDir) @@ -256,7 +254,7 @@ async function verboseReportOnFailure(test) { await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource) } -async function requestEther(address) { +async function requestEther (address) { const accounts = await eth.accounts() await eth.sendTransaction({ from: accounts[0], to: address, value: 1 * 1e18, data: '0x0' }) } diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index c110f71fc..160cd4552 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -1,74 +1,54 @@ -// var jsdom = require('mocha-jsdom') var assert = require('assert') -var freeze = require('deep-freeze-strict') var path = require('path') -var sinon = require('sinon') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +const actions = require(path.join(__dirname, '../../../ui/app/actions.js')) + +const middlewares = [thunk] +const mockStore = configureMockStore(middlewares) describe('tx confirmation screen', function () { - beforeEach(function () { - this.sinon = sinon.createSandbox() - }) - - afterEach(function () { - this.sinon.restore() - }) - - var initialState, result - - describe('when there is only one tx', function () { - var firstTxId = 1457634084250832 - - beforeEach(function () { - initialState = { - appState: { - currentView: { - name: 'confTx', - }, + const txId = 1457634084250832 + const initialState = { + appState: { + currentView: { + name: 'confTx', + }, + }, + metamask: { + unapprovedTxs: { + [txId]: { + id: txId, + status: 'unconfirmed', + time: 1457634084250, }, - metamask: { - unapprovedTxs: { - '1457634084250832': { - id: 1457634084250832, - status: 'unconfirmed', - time: 1457634084250, - }, - }, - }, - } - freeze(initialState) + }, + }, + } + + const store = mockStore(initialState) + + describe('cancelTx', function () { + before(function (done) { + actions._setBackgroundConnection({ + approveTransaction (txId, cb) { cb('An error!') }, + cancelTransaction (txId, cb) { cb() }, + clearSeedWordCache (cb) { cb() }, + getState (cb) { cb() }, + }) + done() }) - describe('cancelTx', function () { - before(function (done) { - actions._setBackgroundConnection({ - approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId, cb) { cb() }, - clearSeedWordCache (cb) { cb() }, + it('creates COMPLETED_TX with the cancelled transaction ID', function (done) { + store.dispatch(actions.cancelTx({ id: txId })) + .then(() => { + const storeActions = store.getActions() + const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX) + assert.equal(completedTxAction.value, txId) + done() }) - - actions.cancelTx({value: firstTxId})((action) => { - result = reducers(initialState, action) - }) - done() - }) - - it('should transition to the account detail view', function () { - assert.equal(result.appState.currentView.name, 'accountDetail') - }) - - it('should have no unconfirmed txs remaining', function () { - var count = getUnconfirmedTxCount(result) - assert.equal(count, 0) - }) }) }) }) - -function getUnconfirmedTxCount (state) { - var txs = state.metamask.unapprovedTxs - var count = Object.keys(txs).length - return count -} diff --git a/test/unit/app/account-import-strategies.spec.js b/test/unit/app/account-import-strategies.spec.js index 216c2f698..d20ba0f0b 100644 --- a/test/unit/app/account-import-strategies.spec.js +++ b/test/unit/app/account-import-strategies.spec.js @@ -1,5 +1,4 @@ const assert = require('assert') -const path = require('path') const ethUtil = require('ethereumjs-util') const accountImporter = require('../../../app/scripts/account-import-strategies/index') const { assertRejects } = require('../test-utils') @@ -15,7 +14,7 @@ describe('Account Import Strategies', function () { }) it('throws an error for empty string private key', async () => { - assertRejects(async function() { + assertRejects(async function () { await accountImporter.importAccount('Private Key', [ '' ]) }, Error, 'no empty strings') }) diff --git a/test/unit/app/controllers/address-book-controller.js b/test/unit/app/controllers/address-book-controller.js index dc4b8e3ff..1350e1a61 100644 --- a/test/unit/app/controllers/address-book-controller.js +++ b/test/unit/app/controllers/address-book-controller.js @@ -12,7 +12,7 @@ const stubPreferencesStore = { }, } }, -}; +} describe('address-book-controller', function () { var addressBookController diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js new file mode 100644 index 000000000..426ffe23a --- /dev/null +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -0,0 +1,141 @@ +const assert = require('assert') +const sinon = require('sinon') +const ObservableStore = require('obs-store') +const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens') +const NetworkController = require('../../../../app/scripts/controllers/network/network') +const PreferencesController = require('../../../../app/scripts/controllers/preferences') + +describe('DetectTokensController', () => { + const sandbox = sinon.createSandbox() + let clock + let keyringMemStore + before(async () => { + keyringMemStore = new ObservableStore({ isUnlocked: false}) + }) + after(() => { + sandbox.restore() + }) + + it('should poll on correct interval', async () => { + const stub = sinon.stub(global, 'setInterval') + new DetectTokensController({ interval: 1337 }) // eslint-disable-line no-new + assert.strictEqual(stub.getCall(0).args[1], 1337) + stub.restore() + }) + + it('should be called on every polling period', async () => { + clock = sandbox.useFakeTimers() + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + var stub = sandbox.stub(controller, 'detectNewTokens') + + clock.tick(1) + sandbox.assert.notCalled(stub) + clock.tick(180000) + sandbox.assert.called(stub) + clock.tick(180000) + sandbox.assert.calledTwice(stub) + clock.tick(180000) + sandbox.assert.calledThrice(stub) + }) + + it('should not check tokens while in test network', async () => { + const network = new NetworkController() + network.setProviderType('rinkeby') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + var stub = sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4').returns(true) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388').returns(true) + + await controller.detectNewTokens() + sandbox.assert.notCalled(stub) + }) + + it('should only check and add tokens while in main network', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4') + .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388') + .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18)) + + await controller.detectNewTokens() + assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'}, + {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}]) + }) + + it('should not detect same token while in main network', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8) + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4') + .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388') + .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18)) + + await controller.detectNewTokens() + assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'}, + {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}]) + }) + + it('should trigger detect new tokens when change address', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + var stub = sandbox.stub(controller, 'detectNewTokens') + await preferences.setSelectedAddress('0xbc86727e770de68b1060c91f6bb6945c73e10388') + sandbox.assert.called(stub) + }) + + it('should trigger detect new tokens when submit password', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.selectedAddress = '0x0' + var stub = sandbox.stub(controller, 'detectNewTokens') + await controller._keyringMemStore.updateState({ isUnlocked: true }) + sandbox.assert.called(stub) + }) + + it('should not trigger detect new tokens when not open or not unlocked', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = false + var stub = sandbox.stub(controller, 'detectTokenBalance') + clock.tick(180000) + sandbox.assert.notCalled(stub) + controller.isOpen = false + controller.isUnlocked = true + clock.tick(180000) + sandbox.assert.notCalled(stub) + }) +}) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 0dda4609b..9164fe246 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -222,6 +222,129 @@ describe('MetaMaskController', function () { }) }) + describe('connectHardware', function () { + + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.connectHardware('Some random device name', 0) + } catch (e) { + assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device') + } + }) + + it('should add the Trezor Hardware keyring', async function () { + sinon.spy(metamaskController.keyringController, 'addNewKeyring') + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Trezor Hardware') + assert.equal(keyrings.length, 1) + }) + + }) + + describe('checkHardwareStatus', function () { + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.checkHardwareStatus('Some random device name') + } catch (e) { + assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device') + } + }) + + it('should be locked by default', async function () { + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + const status = await metamaskController.checkHardwareStatus('trezor') + assert.equal(status, false) + }) + }) + + describe('forgetDevice', function () { + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.forgetDevice('Some random device name') + } catch (e) { + assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device') + } + }) + + it('should wipe all the keyring info', async function () { + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.forgetDevice('trezor') + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + + assert.deepEqual(keyrings[0].accounts, []) + assert.deepEqual(keyrings[0].page, 0) + assert.deepEqual(keyrings[0].isUnlocked(), false) + }) + }) + + describe('unlockTrezorAccount', function () { + let accountToUnlock + let windowOpenStub + let addNewAccountStub + let getAccountsStub + beforeEach(async function () { + accountToUnlock = 10 + windowOpenStub = sinon.stub(window, 'open') + windowOpenStub.returns(noop) + + addNewAccountStub = sinon.stub(metamaskController.keyringController, 'addNewAccount') + addNewAccountStub.returns({}) + + getAccountsStub = sinon.stub(metamaskController.keyringController, 'getAccounts') + // Need to return different address to mock the behavior of + // adding a new account from the keyring + getAccountsStub.onCall(0).returns(Promise.resolve(['0x1'])) + getAccountsStub.onCall(1).returns(Promise.resolve(['0x2'])) + getAccountsStub.onCall(2).returns(Promise.resolve(['0x3'])) + getAccountsStub.onCall(3).returns(Promise.resolve(['0x4'])) + sinon.spy(metamaskController.preferencesController, 'setAddresses') + sinon.spy(metamaskController.preferencesController, 'setSelectedAddress') + sinon.spy(metamaskController.preferencesController, 'setAccountLabel') + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null) + }) + + afterEach(function () { + metamaskController.keyringController.addNewAccount.restore() + window.open.restore() + }) + + it('should set accountToUnlock in the keyring', async function () { + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + assert.equal(keyrings[0].unlockedAccount, accountToUnlock) + }) + + + it('should call keyringController.addNewAccount', async function () { + assert(metamaskController.keyringController.addNewAccount.calledOnce) + }) + + it('should call keyringController.getAccounts ', async function () { + assert(metamaskController.keyringController.getAccounts.called) + }) + + it('should call preferencesController.setAddresses', async function () { + assert(metamaskController.preferencesController.setAddresses.calledOnce) + }) + + it('should call preferencesController.setSelectedAddress', async function () { + assert(metamaskController.preferencesController.setSelectedAddress.calledOnce) + }) + + it('should call preferencesController.setAccountLabel', async function () { + assert(metamaskController.preferencesController.setAccountLabel.calledOnce) + }) + + + }) + describe('#setCustomRpc', function () { const customRPC = 'https://custom.rpc/' let rpcTarget @@ -362,6 +485,39 @@ describe('MetaMaskController', function () { }) }) + describe('#removeAccount', function () { + let ret + const addressToRemove = '0x1' + + beforeEach(async function () { + sinon.stub(metamaskController.preferencesController, 'removeAddress') + sinon.stub(metamaskController.accountTracker, 'removeAccount') + sinon.stub(metamaskController.keyringController, 'removeAccount') + + ret = await metamaskController.removeAccount(addressToRemove) + + }) + + afterEach(function () { + metamaskController.keyringController.removeAccount.restore() + metamaskController.accountTracker.removeAccount.restore() + metamaskController.preferencesController.removeAddress.restore() + }) + + it('should call preferencesController.removeAddress', async function () { + assert(metamaskController.preferencesController.removeAddress.calledWith(addressToRemove)) + }) + it('should call accountTracker.removeAccount', async function () { + assert(metamaskController.accountTracker.removeAccount.calledWith(addressToRemove)) + }) + it('should call keyringController.removeAccount', async function () { + assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove)) + }) + it('should return address', async function () { + assert.equal(ret, '0x1') + }) + }) + describe('#clearSeedWordCache', function () { it('should have set seed words', function () { diff --git a/test/unit/app/controllers/network-contoller-test.js b/test/unit/app/controllers/network-contoller-test.js index 789850ef3..e16fb104e 100644 --- a/test/unit/app/controllers/network-contoller-test.js +++ b/test/unit/app/controllers/network-contoller-test.js @@ -5,9 +5,6 @@ const { getNetworkDisplayName, } = require('../../../../app/scripts/controllers/network/util') -const { createTestProviderTools } = require('../../../stub/provider') -const providerResultStub = {} - describe('# Network Controller', function () { let networkController const noop = () => {} diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index e5e751b57..e055500b1 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -52,6 +52,31 @@ describe('preferences controller', function () { }) }) + describe('removeAddress', function () { + it('should remove an address from state', function () { + preferencesController.setAddresses([ + '0xda22le', + '0x7e57e2', + ]) + + preferencesController.removeAddress('0xda22le') + + assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined) + }) + + it('should switch accounts if the selected address is removed', function () { + preferencesController.setAddresses([ + '0xda22le', + '0x7e57e2', + ]) + + preferencesController.setSelectedAddress('0x7e57e2') + preferencesController.removeAddress('0x7e57e2') + + assert.equal(preferencesController.getSelectedAddress(), '0xda22le') + }) + }) + describe('setAccountLabel', function () { it('should update a label for the given account', function () { preferencesController.setAddresses([ diff --git a/test/unit/app/controllers/transactions/nonce-tracker-test.js b/test/unit/app/controllers/transactions/nonce-tracker-test.js index fc852458c..6c0ac759f 100644 --- a/test/unit/app/controllers/transactions/nonce-tracker-test.js +++ b/test/unit/app/controllers/transactions/nonce-tracker-test.js @@ -1,12 +1,10 @@ const assert = require('assert') const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker') const MockTxGen = require('../../../../lib/mock-tx-gen') -let providerResultStub = {} +const providerResultStub = {} describe('Nonce Tracker', function () { - let nonceTracker, provider - let getPendingTransactions, pendingTxs - let getConfirmedTransactions, confirmedTxs + let nonceTracker, pendingTxs, confirmedTxs describe('#getNonceLock', function () { @@ -182,8 +180,8 @@ describe('Nonce Tracker', function () { describe('When all three return different values', function () { beforeEach(function () { const txGen = new MockTxGen() - const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 }) - const pendingTxs = txGen.generate({ + confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 }) + pendingTxs = txGen.generate({ status: 'submitted', nonce: 100, }, { count: 1 }) @@ -202,8 +200,8 @@ describe('Nonce Tracker', function () { describe('Faq issue 67', function () { beforeEach(function () { const txGen = new MockTxGen() - const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 }) - const pendingTxs = txGen.generate({ + confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 }) + pendingTxs = txGen.generate({ status: 'submitted', }, { count: 10 }) // 0x40 is 64 in hex: diff --git a/test/unit/app/controllers/transactions/pending-tx-test.js b/test/unit/app/controllers/transactions/pending-tx-test.js index e7705c594..8bf2da6f8 100644 --- a/test/unit/app/controllers/transactions/pending-tx-test.js +++ b/test/unit/app/controllers/transactions/pending-tx-test.js @@ -1,20 +1,12 @@ const assert = require('assert') -const ethUtil = require('ethereumjs-util') -const EthTx = require('ethereumjs-tx') -const ObservableStore = require('obs-store') -const clone = require('clone') const { createTestProviderTools } = require('../../../../stub/provider') const PendingTransactionTracker = require('../../../../../app/scripts/controllers/transactions/pending-tx-tracker') const MockTxGen = require('../../../../lib/mock-tx-gen') const sinon = require('sinon') -const noop = () => true -const currentNetworkId = 42 -const otherNetworkId = 36 -const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') describe('PendingTransactionTracker', function () { - let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub, + let pendingTxTracker, txMeta, txMetaNoHash, providerResultStub, provider, txMeta3, txList, knownErrors this.timeout(10000) beforeEach(function () { @@ -34,11 +26,7 @@ describe('PendingTransactionTracker', function () { status: 'signed', txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, } - txMetaNoRawTx = { - hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', - status: 'signed', - txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, - } + providerResultStub = {} provider = createTestProviderTools({ scaffold: providerResultStub }).provider @@ -47,10 +35,10 @@ describe('PendingTransactionTracker', function () { nonceTracker: { getGlobalLock: async () => { return { releaseLock: () => {} } - } + }, }, - getPendingTransactions: () => {return []}, - getCompletedTransactions: () => {return []}, + getPendingTransactions: () => { return [] }, + getCompletedTransactions: () => { return [] }, publishTransaction: () => {}, }) }) @@ -133,22 +121,20 @@ describe('PendingTransactionTracker', function () { }) describe('#queryPendingTxs', function () { it('should call #_checkPendingTxs if their is no oldBlock', function (done) { - let newBlock, oldBlock - newBlock = { number: '0x01' } + let oldBlock + const newBlock = { number: '0x01' } pendingTxTracker._checkPendingTxs = done pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) }) it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { - let newBlock, oldBlock - oldBlock = { number: '0x01' } - newBlock = { number: '0x03' } + const oldBlock = { number: '0x01' } + const newBlock = { number: '0x03' } pendingTxTracker._checkPendingTxs = done pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) }) it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { - let newBlock, oldBlock - oldBlock = { number: '0x1' } - newBlock = { number: '0x2' } + const oldBlock = { number: '0x1' } + const newBlock = { number: '0x2' } pendingTxTracker._checkPendingTxs = () => { const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') done(err) @@ -189,7 +175,7 @@ describe('PendingTransactionTracker', function () { txMeta2.id = 2 txMeta3.id = 3 txList = [txMeta, txMeta2, txMeta3].map((tx) => { - tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + tx.processed = new Promise((resolve) => { tx.resolve = resolve }) return tx }) }) @@ -197,7 +183,6 @@ describe('PendingTransactionTracker', function () { it('should warp all txMeta\'s in #_checkPendingTx', function (done) { pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } - const list = txList.map Promise.all(txList.map((tx) => tx.processed)) .then((txCompletedList) => done()) .catch(done) @@ -207,11 +192,11 @@ describe('PendingTransactionTracker', function () { }) describe('#resubmitPendingTxs', function () { - const blockStub = { number: '0x0' }; + const blockStub = { number: '0x0' } beforeEach(function () { const txMeta2 = txMeta3 = txMeta txList = [txMeta, txMeta2, txMeta3].map((tx) => { - tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + tx.processed = new Promise((resolve) => { tx.resolve = resolve }) return tx }) }) @@ -228,7 +213,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.resubmitPendingTxs(blockStub) }) it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) { - knownErrors =[ + knownErrors = [ // geth ' Replacement transaction Underpriced ', ' known transaction', @@ -275,7 +260,7 @@ describe('PendingTransactionTracker', function () { }) describe('#_resubmitTx', function () { const mockFirstRetryBlockNumber = '0x1' - let txMetaToTestExponentialBackoff + let txMetaToTestExponentialBackoff, enoughBalance beforeEach(() => { pendingTxTracker.getBalance = (address) => { @@ -298,7 +283,7 @@ describe('PendingTransactionTracker', function () { }) it('should publish the transaction', function (done) { - const enoughBalance = '0x100000' + enoughBalance = '0x100000' // Stubbing out current account state: // Adding the fake tx: @@ -313,7 +298,7 @@ describe('PendingTransactionTracker', function () { }) it('should not publish the transaction if the limit of retries has been exceeded', function (done) { - const enoughBalance = '0x100000' + enoughBalance = '0x100000' const mockLatestBlockNumber = '0x5' pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber) @@ -327,7 +312,7 @@ describe('PendingTransactionTracker', function () { }) it('should publish the transaction if the number of blocks since last retry exceeds the last set limit', function (done) { - const enoughBalance = '0x100000' + enoughBalance = '0x100000' const mockLatestBlockNumber = '0x11' pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber) @@ -342,8 +327,8 @@ describe('PendingTransactionTracker', function () { }) describe('#_checkIfNonceIsTaken', function () { - beforeEach ( function () { - let confirmedTxList = [{ + beforeEach(function () { + const confirmedTxList = [{ id: 1, hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', status: 'confirmed', diff --git a/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js index 56e8d50db..cb413545f 100644 --- a/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js +++ b/test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js @@ -28,7 +28,7 @@ describe('Recipient Blacklist Checker', function () { it('does not fail on test networks', function () { let callCount = 0 const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE] - for (let networkId in networks) { + for (const networkId in networks) { publicAccounts.forEach((account) => { recipientBlackListChecker.checkAccount(networkId, account) callCount++ @@ -61,7 +61,7 @@ describe('Recipient Blacklist Checker', function () { } catch (err) { assert.equal(err.message, 'Recipient is a public account') } - }) + }) it('fails for public account - lowercase', async function () { const mainnetId = 1 @@ -72,6 +72,6 @@ describe('Recipient Blacklist Checker', function () { } catch (err) { assert.equal(err.message, 'Recipient is a public account') } - }) + }) }) }) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 9bdfe7c1a..26dc7b656 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -1,20 +1,17 @@ const assert = require('assert') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') -const EthjsQuery = require('ethjs-query') const ObservableStore = require('obs-store') const sinon = require('sinon') const TransactionController = require('../../../../../app/scripts/controllers/transactions') -const TxGasUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils') const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider') const noop = () => true const currentNetworkId = 42 -const otherNetworkId = 36 describe('Transaction Controller', function () { - let txController, provider, providerResultStub, query, fromAccount + let txController, provider, providerResultStub, fromAccount beforeEach(function () { providerResultStub = { @@ -24,7 +21,6 @@ describe('Transaction Controller', function () { eth_getCode: '0x', } provider = createTestProviderTools({ scaffold: providerResultStub }).provider - query = new EthjsQuery(provider) fromAccount = getTestAccounts()[0] txController = new TransactionController({ @@ -357,9 +353,16 @@ describe('Transaction Controller', function () { ]) }) - it('should set the transaction to rejected from unapproved', async function () { - await txController.cancelTransaction(0) - assert.equal(txController.txStateManager.getTx(0).status, 'rejected') + it('should emit a status change to rejected', function (done) { + txController.once('tx:status-update', (txId, status) => { + try { + assert.equal(status, 'rejected', 'status should e rejected') + assert.equal(txId, 0, 'id should e 0') + done() + } catch (e) { done(e) } + }) + + txController.cancelTransaction(0) }) }) @@ -388,7 +391,7 @@ describe('Transaction Controller', function () { describe('#retryTransaction', function () { it('should create a new txMeta with the same txParams as the original one', function (done) { - let txParams = { + const txParams = { nonce: '0x00', from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', diff --git a/test/unit/app/controllers/transactions/tx-gas-util-test.js b/test/unit/app/controllers/transactions/tx-gas-util-test.js index d1ee86033..31defd6ed 100644 --- a/test/unit/app/controllers/transactions/tx-gas-util-test.js +++ b/test/unit/app/controllers/transactions/tx-gas-util-test.js @@ -1,6 +1,5 @@ const assert = require('assert') const Transaction = require('ethereumjs-tx') -const BN = require('bn.js') const { hexToBn, bnToHex } = require('../../../../../app/scripts/lib/util') diff --git a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js index f4c3a6be1..fba0e7fda 100644 --- a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js +++ b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js @@ -2,16 +2,16 @@ const assert = require('assert') const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') const testVault = require('../../../../data/v17-long-history.json') -describe ('Transaction state history helper', function () { +describe('Transaction state history helper', function () { describe('#snapshotFromTxMeta', function () { it('should clone deep', function () { const input = { foo: { bar: { - bam: 'baz' - } - } + bam: 'baz', + }, + }, } const output = txStateHistoryHelper.snapshotFromTxMeta(input) assert('foo' in output, 'has a foo key') @@ -50,14 +50,14 @@ describe ('Transaction state history helper', function () { it('replaying history does not mutate the original obj', function () { const initialState = { test: true, message: 'hello', value: 1 } const diff1 = [{ - "op": "replace", - "path": "/message", - "value": "haay", + 'op': 'replace', + 'path': '/message', + 'value': 'haay', }] const diff2 = [{ - "op": "replace", - "path": "/value", - "value": 2, + 'op': 'replace', + 'path': '/value', + 'value': 2, }] const history = [initialState, diff1, diff2] @@ -72,15 +72,15 @@ describe ('Transaction state history helper', function () { describe('#generateHistoryEntry', function () { - function generateHistoryEntryTest(note) { + function generateHistoryEntryTest (note) { const prevState = { someValue: 'value 1', foo: { bar: { - bam: 'baz' - } - } + bam: 'baz', + }, + }, } const nextState = { @@ -89,9 +89,9 @@ describe ('Transaction state history helper', function () { foo: { newPropFirstLevel: 'new property - first level', bar: { - bam: 'baz' - } - } + bam: 'baz', + }, + }, } const before = new Date().getTime() @@ -106,8 +106,7 @@ describe ('Transaction state history helper', function () { assert.equal(result[0].path, expectedEntry1.path) assert.equal(result[0].value, expectedEntry1.value) assert.equal(result[0].value, expectedEntry1.value) - if (note) - assert.equal(result[0].note, note) + if (note) { assert.equal(result[0].note, note) } assert.ok(result[0].timestamp >= before && result[0].timestamp <= after) @@ -124,6 +123,6 @@ describe ('Transaction state history helper', function () { it('should add note to first entry', function () { generateHistoryEntryTest('custom note') - }) + }) }) -}) \ No newline at end of file +}) diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js index 20bc08b94..88bdaa60e 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager-test.js +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -1,6 +1,4 @@ const assert = require('assert') -const clone = require('clone') -const ObservableStore = require('obs-store') const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager') const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') const noop = () => true @@ -16,23 +14,23 @@ describe('TransactionStateManager', function () { transactions: [], }, txHistoryLimit: 10, - getNetwork: () => currentNetworkId + getNetwork: () => currentNetworkId, }) }) describe('#setTxStatusSigned', function () { it('sets the tx status to signed', function () { - let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx, noop) txStateManager.setTxStatusSigned(1) - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 1) assert.equal(result[0].status, 'signed') }) it('should emit a signed event to signal the exciton of callback', (done) => { - let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } const noop = function () { assert(true, 'event listener has been triggered and noop executed') done() @@ -45,22 +43,24 @@ describe('TransactionStateManager', function () { }) describe('#setTxStatusRejected', function () { - it('sets the tx status to rejected', function () { - let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + it('sets the tx status to rejected and removes it from history', function () { + const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx) txStateManager.setTxStatusRejected(1) - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') + assert.equal(result.length, 0) }) it('should emit a rejected event to signal the exciton of callback', (done) => { - let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx) const noop = function (err, txId) { - assert(true, 'event listener has been triggered and noop executed') - done() + if (err) { + console.log('Error: ', err) + } + assert(true, 'event listener has been triggered and noop executed') + done() } txStateManager.on('1:rejected', noop) txStateManager.setTxStatusRejected(1) @@ -69,7 +69,7 @@ describe('TransactionStateManager', function () { describe('#getFullTxList', function () { it('when new should return empty array', function () { - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) @@ -77,7 +77,7 @@ describe('TransactionStateManager', function () { describe('#getTxList', function () { it('when new should return empty array', function () { - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) @@ -85,21 +85,21 @@ describe('TransactionStateManager', function () { describe('#addTx', function () { it('adds a tx returned in getTxList', function () { - let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx, noop) - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 1) assert.equal(result[0].id, 1) }) it('does not override txs from other networks', function () { - let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } + const tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } txStateManager.addTx(tx, noop) txStateManager.addTx(tx2, noop) - let result = txStateManager.getFullTxList() - let result2 = txStateManager.getTxList() + const result = txStateManager.getFullTxList() + const result2 = txStateManager.getTxList() assert.equal(result.length, 2, 'txs were deleted') assert.equal(result2.length, 1, 'incorrect number of txs on network.') }) @@ -110,7 +110,7 @@ describe('TransactionStateManager', function () { const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx, noop) } - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.equal(result.length, limit, `limit of ${limit} txs enforced`) assert.equal(result[0].id, 1, 'early txs truncted') }) @@ -121,20 +121,20 @@ describe('TransactionStateManager', function () { const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx, noop) } - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.equal(result.length, limit, `limit of ${limit} txs enforced`) assert.equal(result[0].id, 1, 'early txs truncted') }) it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { - let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(unconfirmedTx, noop) const limit = txStateManager.txHistoryLimit for (let i = 1; i < limit + 1; i++) { const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx, noop) } - let result = txStateManager.getTxList() + const result = txStateManager.getTxList() assert.equal(result.length, limit, `limit of ${limit} txs enforced`) assert.equal(result[0].id, 0, 'first tx should still be there') assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') @@ -149,7 +149,7 @@ describe('TransactionStateManager', function () { const txMeta = txStateManager.getTx('1') txMeta.hash = 'foo' txStateManager.updateTx(txMeta) - let result = txStateManager.getTx('1') + const result = txStateManager.getTx('1') assert.equal(result.hash, 'foo') }) @@ -166,8 +166,6 @@ describe('TransactionStateManager', function () { }, } - const updatedMeta = clone(txMeta) - txStateManager.addTx(txMeta) const updatedTx = txStateManager.getTx('1') // verify tx was initialized correctly @@ -185,7 +183,7 @@ describe('TransactionStateManager', function () { // validate history was updated assert.equal(result.history.length, 2, 'two history items (initial + diff)') assert.equal(result.history[1].length, 1, 'two history state items (initial + diff)') - + const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice } assert.deepEqual(result.history[1][0].op, expectedEntry.op, 'two history items (initial + diff) operation') assert.deepEqual(result.history[1][0].path, expectedEntry.path, 'two history items (initial + diff) path') @@ -288,4 +286,18 @@ describe('TransactionStateManager', function () { }) }) + + describe('#_removeTx', function () { + it('should remove the transaction from the storage', () => { + txStateManager._saveTxList([ {id: 1} ]) + txStateManager._removeTx(1) + assert(!txStateManager.getFullTxList().length, 'txList should be empty') + }) + + it('should only remove the transaction with ID 1 from the storage', () => { + txStateManager._saveTxList([ {id: 1}, {id: 2} ]) + txStateManager._removeTx(1) + assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2') + }) + }) }) diff --git a/test/unit/app/controllers/transactions/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js index 115127f85..029fab4d5 100644 --- a/test/unit/app/controllers/transactions/tx-utils-test.js +++ b/test/unit/app/controllers/transactions/tx-utils-test.js @@ -27,7 +27,7 @@ describe('txUtils', function () { describe('#normalizeTxParams', () => { it('should normalize txParams', () => { - let txParams = { + const txParams = { chainId: '0x1', from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', to: null, @@ -91,7 +91,7 @@ describe('txUtils', function () { assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`) // should run - txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d' + txParams.from = '0x1678a085c290ebd122dc42cba69373b5953b831d' txUtils.validateFrom(txParams) }) }) diff --git a/test/unit/app/edge-encryptor-test.js b/test/unit/app/edge-encryptor-test.js index cc9777389..1a6255b36 100644 --- a/test/unit/app/edge-encryptor-test.js +++ b/test/unit/app/edge-encryptor-test.js @@ -11,7 +11,7 @@ global.crypto = global.crypto || { array[i] = Math.random() * 100 } return array - } + }, } describe('EdgeEncryptor', function () { @@ -33,10 +33,10 @@ describe('EdgeEncryptor', function () { it('should return proper format.', function (done) { edgeEncryptor.encrypt(password, data) .then(function (encryptedData) { - let encryptedObject = JSON.parse(encryptedData) + const encryptedObject = JSON.parse(encryptedData) assert.ok(encryptedObject.data, 'there is no data') - assert.ok(encryptedObject.iv && encryptedObject.iv.length != 0, 'there is no iv') - assert.ok(encryptedObject.salt && encryptedObject.salt.length != 0, 'there is no salt') + assert.ok(encryptedObject.iv && encryptedObject.iv.length !== 0, 'there is no iv') + assert.ok(encryptedObject.salt && encryptedObject.salt.length !== 0, 'there is no salt') done() }).catch(function (err) { done(err) @@ -56,7 +56,7 @@ describe('EdgeEncryptor', function () { assert.notEqual(encryptedData[1].length, 0) done() }) - }) + }) }) describe('decrypt', function () { diff --git a/test/unit/app/nodeify-test.js b/test/unit/app/nodeify-test.js index 901603c8b..938b76c68 100644 --- a/test/unit/app/nodeify-test.js +++ b/test/unit/app/nodeify-test.js @@ -13,8 +13,12 @@ describe('nodeify', function () { it('should retain original context', function (done) { var nodified = nodeify(obj.promiseFunc, obj) nodified('baz', function (err, res) { - assert.equal(res, 'barbaz') - done() + if (!err) { + assert.equal(res, 'barbaz') + done() + } else { + done(new Error(err.toString())) + } }) }) diff --git a/test/unit/app/pending-balance-test.js b/test/unit/app/pending-balance-test.js index 1418e4a4e..508635c46 100644 --- a/test/unit/app/pending-balance-test.js +++ b/test/unit/app/pending-balance-test.js @@ -2,7 +2,6 @@ const assert = require('assert') const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator') const MockTxGen = require('../../lib/mock-tx-gen') const BN = require('ethereumjs-util').BN -let providerResultStub = {} const zeroBn = new BN(0) const etherBn = new BN(String(1e18)) @@ -20,7 +19,7 @@ describe('PendingBalanceCalculator', function () { value: ether, gasPrice: '0x0', gas: '0x0', - } + }, }, { count: 1 }) const balanceCalculator = generateBalanceCalcWith([], zeroBn) @@ -36,7 +35,7 @@ describe('PendingBalanceCalculator', function () { value: '0x0', gasPrice: '0x2', gas: '0x3', - } + }, }, { count: 1 }) const balanceCalculator = generateBalanceCalcWith([], zeroBn) @@ -66,7 +65,7 @@ describe('PendingBalanceCalculator', function () { value: ether, gasPrice: '0x0', gas: '0x0', - } + }, }, { count: 1 }) balanceCalculator = generateBalanceCalcWith(pendingTxs, etherBn) diff --git a/test/unit/app/seed-phrase-verifier-test.js b/test/unit/app/seed-phrase-verifier-test.js index b0da534da..d8720d5a0 100644 --- a/test/unit/app/seed-phrase-verifier-test.js +++ b/test/unit/app/seed-phrase-verifier-test.js @@ -9,11 +9,10 @@ describe('SeedPhraseVerifier', function () { describe('verifyAccounts', function () { - let password = 'passw0rd1' - let hdKeyTree = 'HD Key Tree' + const password = 'passw0rd1' + const hdKeyTree = 'HD Key Tree' let keyringController - let vault let primaryKeyring beforeEach(async function () { @@ -24,60 +23,60 @@ describe('SeedPhraseVerifier', function () { assert(keyringController) - vault = await keyringController.createNewVaultAndKeychain(password) + await keyringController.createNewVaultAndKeychain(password) primaryKeyring = keyringController.getKeyringsByType(hdKeyTree)[0] }) it('should be able to verify created account with seed words', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let serialized = await primaryKeyring.serialize() - let seedWords = serialized.mnemonic + const serialized = await primaryKeyring.serialize() + const seedWords = serialized.mnemonic assert.notEqual(seedWords.length, 0) - - let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) + + await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) }) it('should be able to verify created account (upper case) with seed words', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let upperCaseAccounts = [createdAccounts[0].toUpperCase()] + const upperCaseAccounts = [createdAccounts[0].toUpperCase()] - let serialized = await primaryKeyring.serialize() - let seedWords = serialized.mnemonic + const serialized = await primaryKeyring.serialize() + const seedWords = serialized.mnemonic assert.notEqual(seedWords.length, 0) - - let result = await seedPhraseVerifier.verifyAccounts(upperCaseAccounts, seedWords) + + await seedPhraseVerifier.verifyAccounts(upperCaseAccounts, seedWords) }) it('should be able to verify created account (lower case) with seed words', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let lowerCaseAccounts = [createdAccounts[0].toLowerCase()] + const lowerCaseAccounts = [createdAccounts[0].toLowerCase()] - let serialized = await primaryKeyring.serialize() - let seedWords = serialized.mnemonic + const serialized = await primaryKeyring.serialize() + const seedWords = serialized.mnemonic assert.notEqual(seedWords.length, 0) - - let result = await seedPhraseVerifier.verifyAccounts(lowerCaseAccounts, seedWords) + + await seedPhraseVerifier.verifyAccounts(lowerCaseAccounts, seedWords) }) it('should return error with good but different seed words', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let serialized = await primaryKeyring.serialize() - let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' - - try { - let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) - assert.fail("Should reject") + await primaryKeyring.serialize() + const seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' + + try { + await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) + assert.fail('Should reject') } catch (err) { assert.ok(err.message.indexOf('Not identical accounts!') >= 0, 'Wrong error message') } @@ -85,15 +84,15 @@ describe('SeedPhraseVerifier', function () { it('should return error with undefined existing accounts', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let serialized = await primaryKeyring.serialize() - let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' + await primaryKeyring.serialize() + const seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' - try { - let result = await seedPhraseVerifier.verifyAccounts(undefined, seedWords) - assert.fail("Should reject") + try { + await seedPhraseVerifier.verifyAccounts(undefined, seedWords) + assert.fail('Should reject') } catch (err) { assert.equal(err.message, 'No created accounts defined.') } @@ -101,15 +100,15 @@ describe('SeedPhraseVerifier', function () { it('should return error with empty accounts array', async function () { - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 1) - let serialized = await primaryKeyring.serialize() - let seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' + await primaryKeyring.serialize() + const seedWords = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' - try { - let result = await seedPhraseVerifier.verifyAccounts([], seedWords) - assert.fail("Should reject") + try { + await seedPhraseVerifier.verifyAccounts([], seedWords) + assert.fail('Should reject') } catch (err) { assert.equal(err.message, 'No created accounts defined.') } @@ -117,17 +116,17 @@ describe('SeedPhraseVerifier', function () { it('should be able to verify more than one created account with seed words', async function () { - const keyState = await keyringController.addNewAccount(primaryKeyring) - const keyState2 = await keyringController.addNewAccount(primaryKeyring) + await keyringController.addNewAccount(primaryKeyring) + await keyringController.addNewAccount(primaryKeyring) - let createdAccounts = await primaryKeyring.getAccounts() + const createdAccounts = await primaryKeyring.getAccounts() assert.equal(createdAccounts.length, 3) - let serialized = await primaryKeyring.serialize() - let seedWords = serialized.mnemonic + const serialized = await primaryKeyring.serialize() + const seedWords = serialized.mnemonic assert.notEqual(seedWords.length, 0) - - let result = await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) + + await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords) }) }) }) diff --git a/test/unit/app/util-test.js b/test/unit/app/util-test.js index 670bc4d22..656b22d92 100644 --- a/test/unit/app/util-test.js +++ b/test/unit/app/util-test.js @@ -38,4 +38,4 @@ describe('SufficientBalance', function () { const result = sufficientBalance(tx, balance) assert.ok(!result, 'insufficient balance found.') }) -}) \ No newline at end of file +}) diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js index 9b1e82acf..81e6fdf9e 100644 --- a/test/unit/components/balance-component-test.js +++ b/test/unit/components/balance-component-test.js @@ -8,7 +8,7 @@ const mockState = { accounts: { abc: {} }, network: 1, selectedAddress: 'abc', - } + }, } describe('BalanceComponent', function () { diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 7b9d9814f..fab396548 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -52,13 +52,13 @@ describe('BnInput', function () { it('can tolerate wei precision', function (done) { const renderer = ReactTestUtils.createRenderer() - let valueStr = '1000000000' + const valueStr = '1000000000' const value = new BN(valueStr, 10) const inputStr = '1.000000001' - let targetStr = '1000000001' + const targetStr = '1000000001' const target = new BN(targetStr, 10) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js deleted file mode 100644 index c6c588e1c..000000000 --- a/test/unit/components/pending-tx-test.js +++ /dev/null @@ -1,67 +0,0 @@ -const assert = require('assert') -const h = require('react-hyperscript') -const PendingTx = require('../../../ui/app/components/pending-tx') -const ethUtil = require('ethereumjs-util') - -const { createMockStore } = require('redux-test-utils') -const { shallowWithStore } = require('../../lib/shallow-with-store') - -const identities = { abc: {}, def: {} } -const mockState = { - metamask: { - accounts: { abc: {} }, - identities, - conversionRate: 10, - selectedAddress: 'abc', - } -} - -describe('PendingTx', function () { - const gasPrice = '0x4A817C800' // 20 Gwei - const txData = { - 'id': 5021615666270214, - 'time': 1494458763011, - 'status': 'unapproved', - 'metamaskNetworkId': '1494442339676', - 'txParams': { - 'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826', - 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - 'value': '0xde0b6b3a7640000', - gasPrice, - 'gas': '0x7b0c', - }, - 'gasLimitSpecified': false, - 'estimatedGas': '0x5208', - } - const newGasPrice = '0x77359400' - - const computedBalances = {} - computedBalances[Object.keys(identities)[0]] = { - ethBalance: '0x00000000000000056bc75e2d63100000', - } - const props = { - txData, - computedBalances, - sendTransaction: (txMeta, event) => { - // Assert changes: - const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice) - assert.notEqual(result, gasPrice, 'gas price should change') - assert.equal(result, newGasPrice, 'gas price assigned.') - }, - } - - let pendingTxComponent - let store - let component - beforeEach(function () { - store = createMockStore(mockState) - component = shallowWithStore(h(PendingTx, props), store) - pendingTxComponent = component - }) - - it('should render correctly', function (done) { - assert.equal(pendingTxComponent.props().identities, identities) - done() - }) -}) - diff --git a/test/unit/migrations/022-test.js b/test/unit/migrations/022-test.js index 1333d929d..f8ee00e38 100644 --- a/test/unit/migrations/022-test.js +++ b/test/unit/migrations/022-test.js @@ -2,14 +2,14 @@ const assert = require('assert') const migration22 = require('../../../app/scripts/migrations/022') const properTime = (new Date()).getTime() const storage = { - "meta": {}, - "data": { - "TransactionController": { - "transactions": [ - { "status": "submitted" }, - { "status": "submitted", "submittedTime": properTime }, - {"status": "confirmed"}, - ] + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + { 'status': 'submitted' }, + { 'status': 'submitted', 'submittedTime': properTime }, + {'status': 'confirmed'}, + ], }, }, } diff --git a/test/unit/migrations/023-test.js b/test/unit/migrations/023-test.js index be432d9fa..7da94448d 100644 --- a/test/unit/migrations/023-test.js +++ b/test/unit/migrations/023-test.js @@ -1,12 +1,11 @@ const assert = require('assert') const migration23 = require('../../../app/scripts/migrations/023') -const properTime = (new Date()).getTime() const storage = { - "meta": {}, - "data": { - "TransactionController": { - "transactions": [ - ] + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], }, }, } @@ -53,7 +52,6 @@ while (transactions20.length < 20) { } - storage.data.TransactionController.transactions = transactions describe('storage is migrated successfully and the proper transactions are remove from state', () => { diff --git a/test/unit/migrations/024-test.js b/test/unit/migrations/024-test.js index c3c03d06b..c7b0611bc 100644 --- a/test/unit/migrations/024-test.js +++ b/test/unit/migrations/024-test.js @@ -4,13 +4,12 @@ const firstTimeState = { meta: {}, data: require('../../../app/scripts/first-time-state'), } -const properTime = (new Date()).getTime() const storage = { - "meta": {}, - "data": { - "TransactionController": { - "transactions": [ - ] + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], }, }, } diff --git a/test/unit/migrations/025-test.js b/test/unit/migrations/025-test.js index 76c25dbb6..1e56913a1 100644 --- a/test/unit/migrations/025-test.js +++ b/test/unit/migrations/025-test.js @@ -6,11 +6,11 @@ const firstTimeState = { } const storage = { - "meta": {}, - "data": { - "TransactionController": { - "transactions": [ - ] + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], }, }, } diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js new file mode 100644 index 000000000..3ec9f0c0e --- /dev/null +++ b/test/unit/migrations/027-test.js @@ -0,0 +1,50 @@ +const assert = require('assert') +const migration27 = require('../../../app/scripts/migrations/027') + +const oldStorage = { + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], + }, + }, +} + +const transactions = [] + + +while (transactions.length < 9) { + transactions.push({status: 'rejected'}) + transactions.push({status: 'unapproved'}) + transactions.push({status: 'approved'}) +} + + +oldStorage.data.TransactionController.transactions = transactions + +describe('migration #27', () => { + it('should remove rejected transactions', (done) => { + migration27.migrate(oldStorage) + .then((newStorage) => { + const newTransactions = newStorage.data.TransactionController.transactions + assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6') + newTransactions.forEach((txMeta) => { + if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected')) + }) + done() + }) + .catch(done) + }) + + it('should successfully migrate first time state', (done) => { + migration27.migrate({ + meta: {}, + data: require('../../../app/scripts/first-time-state'), + }) + .then((migratedData) => { + assert.equal(migratedData.meta.version, migration27.version) + done() + }).catch(done) + }) +}) diff --git a/test/unit/migrations/template-test.js b/test/unit/migrations/template-test.js index 35060e2fe..0db69d65a 100644 --- a/test/unit/migrations/template-test.js +++ b/test/unit/migrations/template-test.js @@ -1,6 +1,5 @@ const assert = require('assert') const migrationTemplate = require('../../../app/scripts/migrations/template') -const properTime = (new Date()).getTime() const storage = { meta: {}, data: {}, diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js index 982d8c6ec..493b01918 100644 --- a/test/unit/responsive/components/dropdown-test.js +++ b/test/unit/responsive/components/dropdown-test.js @@ -1,24 +1,24 @@ -const assert = require('assert'); +const assert = require('assert') -const h = require('react-hyperscript'); -const sinon = require('sinon'); -const path = require('path'); -const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown; +const h = require('react-hyperscript') +const sinon = require('sinon') +const path = require('path') +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown const { createMockStore } = require('redux-test-utils') const { mountWithStore } = require('../../../lib/shallow-with-store') const mockState = { metamask: { - } + }, } describe('Dropdown components', function () { - let onClickOutside; - let closeMenu; - let onClick; + let onClickOutside + let closeMenu + let onClick - let dropdownComponentProps = { + const dropdownComponentProps = { isOpen: true, zIndex: 11, onClickOutside, @@ -34,9 +34,9 @@ describe('Dropdown components', function () { let store let component beforeEach(function () { - onClickOutside = sinon.spy(); - closeMenu = sinon.spy(); - onClick = sinon.spy(); + onClickOutside = sinon.spy() + closeMenu = sinon.spy() + onClick = sinon.spy() store = createMockStore(mockState) component = mountWithStore(h( @@ -61,21 +61,21 @@ describe('Dropdown components', function () { }) it('can render two items', function () { - const items = dropdownComponent.find('li'); - assert.equal(items.length, 2); - }); + const items = dropdownComponent.find('li') + assert.equal(items.length, 2) + }) - it('closes when item clicked', function() { - const items = dropdownComponent.find('li'); - const node = items.at(0); - node.simulate('click'); - assert.equal(node.props().closeMenu, closeMenu); - }); + it('closes when item clicked', function () { + const items = dropdownComponent.find('li') + const node = items.at(0) + node.simulate('click') + assert.equal(node.props().closeMenu, closeMenu) + }) - it('invokes click handler when item clicked', function() { - const items = dropdownComponent.find('li'); - const node = items.at(0); - node.simulate('click'); - assert.equal(onClick.calledOnce, true); - }); -}); + it('invokes click handler when item clicked', function () { + const items = dropdownComponent.find('li') + const node = items.at(0) + node.simulate('click') + assert.equal(onClick.calledOnce, true) + }) +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 1edf692b6..7a8d9667d 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -6,11 +6,12 @@ const { calcGasTotal, calcTokenBalance, estimateGas, - estimateGasPriceFromRecentBlocks, -} = require('./components/send_/send.utils') +} = require('./components/send/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') const log = require('loglevel') +const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums') +const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') var actions = { _setBackgroundConnection: _setBackgroundConnection, @@ -27,6 +28,11 @@ var actions = { SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', showSidebar: showSidebar, hideSidebar: hideSidebar, + // sidebar state + ALERT_OPEN: 'UI_ALERT_OPEN', + ALERT_CLOSE: 'UI_ALERT_CLOSE', + showAlert: showAlert, + hideAlert: hideAlert, // network dropdown open NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN', NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE', @@ -79,9 +85,14 @@ var actions = { addNewKeyring, importNewAccount, addNewAccount, + connectHardware, + checkHardwareStatus, + forgetDevice, + unlockTrezorAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, + removeAccount, showNewVaultSeed: showNewVaultSeed, showInfoPage: showInfoPage, CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN', @@ -165,6 +176,7 @@ var actions = { UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', UPDATE_SEND_FROM: 'UPDATE_SEND_FROM', + UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA', UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE', UPDATE_SEND_TO: 'UPDATE_SEND_TO', UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', @@ -175,6 +187,8 @@ var actions = { CLEAR_SEND: 'CLEAR_SEND', OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', + GAS_LOADING_STARTED: 'GAS_LOADING_STARTED', + GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED', setGasLimit, setGasPrice, updateGasData, @@ -182,6 +196,7 @@ var actions = { setSendTokenBalance, updateSendTokenBalance, updateSendFrom, + updateSendHexData, updateSendTo, updateSendAmount, updateSendMemo, @@ -190,6 +205,8 @@ var actions = { updateSendErrors, clearSend, setSelectedAddress, + gasLoadingStarted, + gasLoadingFinished, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -530,6 +547,26 @@ function resetAccount () { } } +function removeAccount (address) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + background.removeAccount(address, (err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + log.info('Account removed: ' + account) + dispatch(actions.showAccountsPage()) + resolve() + }) + }) + } +} + function addNewKeyring (type, opts) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -596,6 +633,88 @@ function addNewAccount () { } } +function checkHardwareStatus (deviceName) { + log.debug(`background.checkHardwareStatus`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.checkHardwareStatus(deviceName, (err, unlocked) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(unlocked) + }) + }) + } +} + +function forgetDevice (deviceName) { + log.debug(`background.forgetDevice`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.forgetDevice(deviceName, (err, response) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve() + }) + }) + } +} + +function connectHardware (deviceName, page) { + log.debug(`background.connectHardware`, deviceName, page) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.connectHardware(deviceName, page, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(accounts) + }) + }) + } +} + +function unlockTrezorAccount (index) { + log.debug(`background.unlockTrezorAccount`, index) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.unlockTrezorAccount(index, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + return resolve() + }) + }) + } +} + function showInfoPage () { return { type: actions.SHOW_INFO_PAGE, @@ -626,7 +745,7 @@ function setCurrentCurrency (currencyCode) { function signMsg (msgData) { log.debug('action - signMsg') - return (dispatch) => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -643,6 +762,12 @@ function signMsg (msgData) { } dispatch(actions.completedTx(msgData.metamaskId)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -651,7 +776,7 @@ function signMsg (msgData) { function signPersonalMsg (msgData) { log.debug('action - signPersonalMsg') - return dispatch => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -668,6 +793,12 @@ function signPersonalMsg (msgData) { } dispatch(actions.completedTx(msgData.metamaskId)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -676,7 +807,7 @@ function signPersonalMsg (msgData) { function signTypedMsg (msgData) { log.debug('action - signTypedMsg') - return (dispatch) => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -693,6 +824,12 @@ function signTypedMsg (msgData) { } dispatch(actions.completedTx(msgData.metamaskId)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -701,11 +838,10 @@ function signTypedMsg (msgData) { function signTx (txData) { return (dispatch) => { - dispatch(actions.showLoadingIndication()) global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } }) dispatch(actions.showConfTxPage({})) } @@ -740,20 +876,28 @@ function updateGasData ({ to, value, }) { - const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks) return (dispatch) => { - return Promise.all([ - Promise.resolve(estimatedGasPrice), - estimateGas({ - estimateGasMethod: background.estimateGas, - blockGasLimit, - selectedAddress, - selectedToken, - to, - value, - gasPrice: estimatedGasPrice, - }), - ]) + dispatch(actions.gasLoadingStarted()) + return new Promise((resolve, reject) => { + background.getGasPrice((err, data) => { + if (err) return reject(err) + return resolve(data) + }) + }) + .then(estimateGasPrice => { + return Promise.all([ + Promise.resolve(estimateGasPrice), + estimateGas({ + estimateGasMethod: background.estimateGas, + blockGasLimit, + selectedAddress, + selectedToken, + to, + value, + estimateGasPrice, + }), + ]) + }) .then(([gasPrice, gas]) => { dispatch(actions.setGasPrice(gasPrice)) dispatch(actions.setGasLimit(gas)) @@ -762,14 +906,28 @@ function updateGasData ({ .then((gasEstimate) => { dispatch(actions.setGasTotal(gasEstimate)) dispatch(updateSendErrors({ gasLoadingError: null })) + dispatch(actions.gasLoadingFinished()) }) .catch(err => { log.error(err) dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })) + dispatch(actions.gasLoadingFinished()) }) } } +function gasLoadingStarted () { + return { + type: actions.GAS_LOADING_STARTED, + } +} + +function gasLoadingFinished () { + return { + type: actions.GAS_LOADING_FINISHED, + } +} + function updateSendTokenBalance ({ selectedToken, tokenContract, @@ -814,6 +972,13 @@ function updateSendFrom (from) { } } +function updateSendHexData (value) { + return { + type: actions.UPDATE_SEND_HEX_DATA, + value, + } +} + function updateSendTo (to, nickname = '') { return { type: actions.UPDATE_SEND_TO, @@ -858,7 +1023,7 @@ function clearSend () { function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { + return (dispatch, getState) => { log.debug(`actions calling background.approveTransaction`) background.approveTransaction(txData.id, (err) => { if (err) { @@ -866,6 +1031,11 @@ function sendTx (txData) { return log.error(err.message) } dispatch(actions.completedTx(txData.id)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } }) } } @@ -885,29 +1055,41 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) { function updateTransaction (txData) { log.info('actions: updateTx: ' + JSON.stringify(txData)) - return (dispatch) => { + return dispatch => { log.debug(`actions calling background.updateTx`) - background.updateTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) - if (err) { - dispatch(actions.txError(err)) - dispatch(actions.goHome()) - return log.error(err.message) - } - dispatch(actions.showConfTxPage({ id: txData.id })) + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + background.updateTransaction(txData, (err) => { + dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) + if (err) { + dispatch(actions.txError(err)) + dispatch(actions.goHome()) + log.error(err.message) + return reject(err) + } + + resolve(txData) + }) }) + .then(() => updateMetamaskStateFromBackground()) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => { + dispatch(actions.showConfTxPage({ id: txData.id })) + dispatch(actions.hideLoadingIndication()) + return txData + }) } } function updateAndApproveTx (txData) { log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { + return (dispatch, getState) => { log.debug(`actions calling background.updateAndApproveTx`) + dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.updateAndApproveTransaction(txData, err => { - dispatch(actions.hideLoadingIndication()) dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) dispatch(actions.clearSend()) @@ -918,10 +1100,23 @@ function updateAndApproveTx (txData) { reject(err) } - dispatch(actions.completedTx(txData.id)) resolve(txData) }) }) + .then(() => updateMetamaskStateFromBackground()) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => { + dispatch(actions.clearSend()) + dispatch(actions.completedTx(txData.id)) + dispatch(actions.hideLoadingIndication()) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + + return txData + }) } } @@ -948,7 +1143,7 @@ function txError (err) { } function cancelMsg (msgData) { - return dispatch => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -962,6 +1157,12 @@ function cancelMsg (msgData) { } dispatch(actions.completedTx(msgData.id)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -969,7 +1170,7 @@ function cancelMsg (msgData) { } function cancelPersonalMsg (msgData) { - return dispatch => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -983,6 +1184,12 @@ function cancelPersonalMsg (msgData) { } dispatch(actions.completedTx(id)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -990,7 +1197,7 @@ function cancelPersonalMsg (msgData) { } function cancelTypedMsg (msgData) { - return dispatch => { + return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -1004,6 +1211,12 @@ function cancelTypedMsg (msgData) { } dispatch(actions.completedTx(id)) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + return resolve(msgData) }) }) @@ -1011,15 +1224,33 @@ function cancelTypedMsg (msgData) { } function cancelTx (txData) { - return dispatch => { + return (dispatch, getState) => { log.debug(`background.cancelTransaction`) + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { - background.cancelTransaction(txData.id, () => { - dispatch(actions.clearSend()) - dispatch(actions.completedTx(txData.id)) - resolve(txData) + background.cancelTransaction(txData.id, err => { + if (err) { + return reject(err) + } + + resolve() }) }) + .then(() => updateMetamaskStateFromBackground()) + .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then(() => { + dispatch(actions.clearSend()) + dispatch(actions.completedTx(txData.id)) + dispatch(actions.hideLoadingIndication()) + + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + return global.platform.closeCurrentWindow() + } + + return txData + }) } } @@ -1562,6 +1793,19 @@ function hideSidebar () { } } +function showAlert (msg) { + return { + type: actions.ALERT_OPEN, + value: msg, + } +} + +function hideAlert () { + return { + type: actions.ALERT_CLOSE, + } +} + function showLoadingIndication (message) { return { diff --git a/ui/app/app.js b/ui/app/app.js index d0e48a368..dbb6146d1 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -11,8 +11,8 @@ const log = require('loglevel') // init const InitializeScreen = require('../../mascara/src/app/first-time').default // accounts -const SendTransactionScreen = require('./components/send_/send.container') -const ConfirmTxScreen = require('./conf-tx') +const SendTransactionScreen = require('./components/send/send.container') +const ConfirmTransaction = require('./components/pages/confirm-transaction') // slideout menu const WalletView = require('./components/wallet-view') @@ -22,8 +22,7 @@ const Home = require('./components/pages/home') const Authenticated = require('./components/pages/authenticated') const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') -const UnlockPage = require('./components/pages/unlock-page') -const RestoreVaultPage = require('./components/pages/keychains/restore-vault') +const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') @@ -37,9 +36,13 @@ const AccountMenu = require('./components/account-menu') // Global Modals const Modal = require('./components/modals/index').Modal +// Global Alert +const Alert = require('./components/alert') const AppHeader = require('./components/app-header') +import UnlockPage from './components/pages/unlock-page' + // Routes const { DEFAULT_ROUTE, @@ -76,7 +79,10 @@ class App extends Component { h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), - h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), + h(Authenticated, { + path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, + component: ConfirmTransaction, + }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), @@ -89,6 +95,7 @@ class App extends Component { render () { const { isLoading, + alertMessage, loadingMessage, network, isMouseUser, @@ -122,6 +129,9 @@ class App extends Component { // global modal h(Modal, {}, []), + // global alert + h(Alert, {visible: this.props.alertOpen, msg: alertMessage}), + h(AppHeader), // sidebar @@ -145,14 +155,6 @@ class App extends Component { ) } - renderGlobalModal () { - return h(Modal, { - ref: 'modalRef', - }, [ - // h(BuyOptions, {}, []), - ]) - } - renderSidebar () { return h('div', [ h('style', ` @@ -261,11 +263,13 @@ App.propTypes = { setCurrentCurrencyToUSD: PropTypes.func, isLoading: PropTypes.bool, loadingMessage: PropTypes.string, + alertMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, frequentRpcList: PropTypes.array, currentView: PropTypes.object, sidebarOpen: PropTypes.bool, + alertOpen: PropTypes.bool, hideSidebar: PropTypes.func, isMascara: PropTypes.bool, isOnboarding: PropTypes.bool, @@ -301,6 +305,8 @@ function mapStateToProps (state) { const { networkDropdownOpen, sidebarOpen, + alertOpen, + alertMessage, isLoading, loadingMessage, } = appState @@ -326,6 +332,8 @@ function mapStateToProps (state) { // state from plugin networkDropdownOpen, sidebarOpen, + alertOpen, + alertMessage, isLoading, loadingMessage, noActiveNotices, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index f34631ca8..9c063d31e 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -9,11 +9,17 @@ const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') const { formatBalance } = require('../../util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../../../app/scripts/lib/util') +const Tooltip = require('../tooltip') + + const { SETTINGS_ROUTE, INFO_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, DEFAULT_ROUTE, } = require('../../routes') @@ -63,6 +69,9 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, + showRemoveAccountConfirmationModal: (identity) => { + return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) + }, } } @@ -106,6 +115,18 @@ AccountMenu.prototype.render = function () { icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), text: this.context.t('importAccount'), }), + h(Item, { + onClick: () => { + toggleAccountMenu() + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) + } else { + history.push(CONNECT_HARDWARE_ROUTE) + } + }, + icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), + text: this.context.t('connectHardwareWallet'), + }), h(Divider), h(Item, { onClick: () => { @@ -136,7 +157,8 @@ AccountMenu.prototype.renderAccounts = function () { } = this.props const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - return accountOrder.map((address) => { + return accountOrder.filter(address => !!identities[address]).map((address) => { + const identity = identities[address] const isSelected = identity.address === selectedAddress @@ -170,16 +192,53 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__balance', formattedBalance), ]), - this.indicateIfLoose(keyring), + this.renderKeyringType(keyring), + this.renderRemoveAccount(keyring, identity), ], ) }) } -AccountMenu.prototype.indicateIfLoose = function (keyring) { +AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) { + // Any account that's not from the HD wallet Keyring can be removed + const type = keyring.type + const isRemovable = type !== 'HD Key Tree' + if (isRemovable) { + return h(Tooltip, { + title: this.context.t('removeAccount'), + position: 'bottom', + }, [ + h('a.remove-account-icon', { + onClick: (e) => this.removeAccount(e, identity), + }, ''), + ]) + } + return null +} + +AccountMenu.prototype.removeAccount = function (e, identity) { + e.preventDefault() + e.stopPropagation() + const { showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal(identity) +} + +AccountMenu.prototype.renderKeyringType = function (keyring) { try { // Sometimes keyrings aren't loaded yet: const type = keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null + let label + switch (type) { + case 'Trezor Hardware': + label = this.context.t('hardware') + break + case 'Simple Key Pair': + label = this.context.t('imported') + break + default: + label = '' + } + + return label !== '' ? h('.keyring-label.allcaps', label) : null + } catch (e) { return } } diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js new file mode 100644 index 000000000..5620d847a --- /dev/null +++ b/ui/app/components/alert/index.js @@ -0,0 +1,62 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +class Alert extends Component { + + constructor (props) { + super(props) + + this.state = { + visble: false, + msg: false, + className: '', + } + } + + componentWillReceiveProps (nextProps) { + if (!this.props.visible && nextProps.visible) { + this.animateIn(nextProps) + } else if (this.props.visible && !nextProps.visible) { + this.animateOut(nextProps) + } + } + + animateIn (props) { + this.setState({ + msg: props.msg, + visible: true, + className: '.visible', + }) + } + + animateOut (props) { + this.setState({ + msg: null, + className: '.hidden', + }) + + setTimeout(_ => { + this.setState({visible: false}) + }, 500) + + } + + render () { + if (this.state.visible) { + return ( + h(`div.global-alert${this.state.className}`, {}, + h('a.msg', {}, this.state.msg) + ) + ) + } + return null + } +} + +Alert.propTypes = { + visible: PropTypes.bool.isRequired, + msg: PropTypes.string, +} +module.exports = Alert + diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 62b04562a..07ca6cf84 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -91,7 +91,6 @@ class AppHeader extends Component { network, provider, history, - location, isUnlocked, } = this.props @@ -126,7 +125,7 @@ class AppHeader extends Component { network={network} provider={provider} onClick={event => this.handleNetworkIndicatorClick(event)} - disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE} + disabled={this.isConfirming()} />
{ this.renderAccountMenu() } diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js new file mode 100644 index 000000000..f99f710ce --- /dev/null +++ b/ui/app/components/button-group/button-group.component.js @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class ButtonGroup extends PureComponent { + static propTypes = { + defaultActiveButtonIndex: PropTypes.number, + disabled: PropTypes.bool, + children: PropTypes.array, + className: PropTypes.string, + style: PropTypes.object, + } + + static defaultProps = { + className: 'button-group', + } + + state = { + activeButtonIndex: this.props.defaultActiveButtonIndex || 0, + } + + handleButtonClick (activeButtonIndex) { + this.setState({ activeButtonIndex }) + } + + renderButtons () { + const { children, disabled } = this.props + + return React.Children.map(children, (child, index) => { + return child && ( + + ) + }) + } + + render () { + const { className, style } = this.props + + return ( +
+ { this.renderButtons() } +
+ ) + } +} diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js new file mode 100644 index 000000000..14e1a7e49 --- /dev/null +++ b/ui/app/components/button-group/button-group.stories.js @@ -0,0 +1,49 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import ButtonGroup from './' +import Button from '../button' +import { text, boolean } from '@storybook/addon-knobs/react' + +storiesOf('ButtonGroup', module) + .add('with Buttons', () => + + + + + + ) + .add('with a disabled Button', () => + + + + + ) diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js new file mode 100644 index 000000000..df470bd57 --- /dev/null +++ b/ui/app/components/button-group/index.js @@ -0,0 +1 @@ +export { default } from './button-group.component' diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss new file mode 100644 index 000000000..29713c75b --- /dev/null +++ b/ui/app/components/button-group/index.scss @@ -0,0 +1,38 @@ +.button-group { + display: flex; + justify-content: center; + align-items: center; + + &__button { + font-family: Roboto; + font-size: 1rem; + color: $tundora; + border-style: solid; + border-color: $alto; + border-width: 1px 1px 1px; + border-left: 0; + flex: 1; + padding: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:first-child { + border-left: 1px solid $alto; + border-radius: 4px 0 0 4px; + } + + &:last-child { + border-radius: 0 4px 4px 0; + } + + &--active { + background-color: $dodger-blue; + color: $white; + } + + &:disabled { + opacity: .5; + } + } +} \ No newline at end of file diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js new file mode 100644 index 000000000..f07bb97c8 --- /dev/null +++ b/ui/app/components/button-group/tests/button-group-component.test.js @@ -0,0 +1,97 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ButtonGroup from '../button-group.component.js' + +const childButtonSpies = { + onClick: sinon.spy(), +} + +sinon.spy(ButtonGroup.prototype, 'handleButtonClick') +sinon.spy(ButtonGroup.prototype, 'renderButtons') + +const mockButtons = [ + , + , + , +] + +describe('ButtonGroup Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow({mockButtons}) + }) + + afterEach(() => { + childButtonSpies.onClick.resetHistory() + ButtonGroup.prototype.handleButtonClick.resetHistory() + ButtonGroup.prototype.renderButtons.resetHistory() + }) + + describe('handleButtonClick', () => { + it('should set the activeButtonIndex', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.instance().handleButtonClick(2) + assert.equal(wrapper.state('activeButtonIndex'), 2) + }) + }) + + describe('renderButtons', () => { + it('should render a button for each child', () => { + const childButtons = wrapper.find('.button-group__button') + assert.equal(childButtons.length, 3) + }) + + it('should render the correct button with an active state', () => { + const childButtons = wrapper.find('.button-group__button') + const activeChildButton = wrapper.find('.button-group__button--active') + assert.deepEqual(childButtons.get(1), activeChildButton.get(0)) + }) + + it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => { + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0) + assert.equal(childButtonSpies.onClick.callCount, 0) + const childButtons = wrapper.find('.button-group__button') + childButtons.at(0).props().onClick() + childButtons.at(1).props().onClick() + childButtons.at(2).props().onClick() + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3) + assert.equal(childButtonSpies.onClick.callCount, 3) + }) + + it('should render all child buttons as disabled if props.disabled is true', () => { + const childButtons = wrapper.find('.button-group__button') + childButtons.forEach(button => { + assert.equal(button.props().disabled, undefined) + }) + wrapper.setProps({ disabled: true }) + const disabledChildButtons = wrapper.find('[disabled=true]') + assert.equal(disabledChildButtons.length, 3) + }) + + it('should render the children of the button', () => { + const mockClass = wrapper.find('.mockClass') + assert.equal(mockClass.length, 1) + }) + }) + + describe('render', () => { + it('should render a div with the expected class and style', () => { + assert.equal(wrapper.find('div').at(0).props().className, 'someClassName') + assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' }) + }) + + it('should call renderButtons when rendering', () => { + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1) + wrapper.instance().render() + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2) + }) + }) +}) diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js index e8e798445..1e0ef1b64 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/button/button.component.js @@ -5,15 +5,24 @@ import classnames from 'classnames' const CLASSNAME_DEFAULT = 'btn-default' const CLASSNAME_PRIMARY = 'btn-primary' const CLASSNAME_SECONDARY = 'btn-secondary' +const CLASSNAME_CONFIRM = 'btn-confirm' const CLASSNAME_LARGE = 'btn--large' const typeHash = { default: CLASSNAME_DEFAULT, primary: CLASSNAME_PRIMARY, secondary: CLASSNAME_SECONDARY, + confirm: CLASSNAME_CONFIRM, } -class Button extends Component { +export default class Button extends Component { + static propTypes = { + type: PropTypes.string, + large: PropTypes.bool, + className: PropTypes.string, + children: PropTypes.string, + } + render () { const { type, large, className, ...buttonProps } = this.props @@ -31,13 +40,3 @@ class Button extends Component { ) } } - -Button.propTypes = { - type: PropTypes.string, - large: PropTypes.bool, - className: PropTypes.string, - children: PropTypes.string, -} - -export default Button - diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js new file mode 100644 index 000000000..f0703dde2 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +const ConfirmDetailRow = props => { + const { + label, + fiatText, + ethText, + onHeaderClick, + fiatTextColor, + headerText, + headerTextClassName, + } = props + + return ( +
+
+ { label } +
+
+
onHeaderClick && onHeaderClick()} + > + { headerText } +
+
+ { fiatText } +
+
+ { ethText } +
+
+
+ ) +} + +ConfirmDetailRow.propTypes = { + label: PropTypes.string, + fiatText: PropTypes.string, + ethText: PropTypes.string, + fiatTextColor: PropTypes.string, + onHeaderClick: PropTypes.func, + headerText: PropTypes.string, + headerTextClassName: PropTypes.string, +} + +export default ConfirmDetailRow diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/confirm-page-container/confirm-detail-row/index.js new file mode 100644 index 000000000..056afff04 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.js @@ -0,0 +1 @@ +export { default } from './confirm-detail-row.component' diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss new file mode 100644 index 000000000..dd6f87c17 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -0,0 +1,50 @@ +.confirm-detail-row { + padding: 14px 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + &__label { + font-size: .75rem; + font-weight: 500; + color: $scorpion; + text-transform: uppercase; + } + + &__details { + flex: 1; + text-align: end; + min-width: 0; + } + + &__fiat { + font-size: 1.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__eth { + color: $oslo-gray; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__header-text { + font-size: .75rem; + text-transform: uppercase; + margin-bottom: 6px; + color: $scorpion; + + &--edit { + color: $curious-blue; + cursor: pointer; + } + + &--total { + font-size: .625rem; + } + } +} diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js new file mode 100644 index 000000000..6f2489071 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -0,0 +1,64 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import ConfirmDetailRow from '../confirm-detail-row.component.js' +import sinon from 'sinon' + +const propsMethodSpies = { + onHeaderClick: sinon.spy(), +} + +describe('Confirm Detail Row Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render', () => { + it('should render a div with a confirm-detail-row class', () => { + assert.equal(wrapper.find('div.confirm-detail-row').length, 1) + }) + + it('should render the label as a child of the confirm-detail-row__label', () => { + assert.equal(wrapper.find('.confirm-detail-row > .confirm-detail-row__label').childAt(0).text(), 'mockLabel') + }) + + it('should render the headerText as a child of the confirm-detail-row__header-text', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText') + }) + + it('should render the fiatText as a child of the confirm-detail-row__fiat', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText') + }) + + it('should render the ethText as a child of the confirm-detail-row__eth', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText') + }) + + it('should set the fiatTextColor on confirm-detail-row__fiat', () => { + assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor') + }) + + it('should assure the confirm-detail-row__header-text classname is correct', () => { + assert.equal(wrapper.find('.confirm-detail-row__header-text').props().className, 'confirm-detail-row__header-text mockHeaderClass') + }) + + it('should call onHeaderClick when headerText div gets clicked', () => { + wrapper.find('.confirm-detail-row__header-text').props().onClick() + assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) + }) + + + }) +}) diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js new file mode 100644 index 000000000..08923af88 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { Tabs, Tab } from '../../tabs' +import { + ConfirmPageContainerSummary, + ConfirmPageContainerError, + ConfirmPageContainerWarning, +} from './' + +export default class ConfirmPageContainerContent extends Component { + static propTypes = { + action: PropTypes.string, + dataComponent: PropTypes.node, + detailsComponent: PropTypes.node, + errorKey: PropTypes.string, + errorMessage: PropTypes.string, + hideSubtitle: PropTypes.bool, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, + subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + summaryComponent: PropTypes.node, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + titleComponent: PropTypes.func, + warning: PropTypes.string, + } + + renderContent () { + const { detailsComponent, dataComponent } = this.props + + if (detailsComponent && dataComponent) { + return this.renderTabs() + } else { + return detailsComponent || dataComponent + } + } + + renderTabs () { + const { detailsComponent, dataComponent } = this.props + + return ( + + + { detailsComponent } + + + { dataComponent } + + + ) + } + + render () { + const { + action, + errorKey, + errorMessage, + title, + subtitle, + hideSubtitle, + identiconAddress, + nonce, + summaryComponent, + detailsComponent, + dataComponent, + warning, + } = this.props + + return ( +
+ { + warning && ( + + ) + } + { + summaryComponent || ( + + ) + } + { this.renderContent() } + { + (errorKey || errorMessage) && ( +
+ +
+ ) + } +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js new file mode 100644 index 000000000..70ebdeb20 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerError = (props, context) => { + const { errorMessage, errorKey } = props + const error = errorKey ? context.t(errorKey) : errorMessage + + return ( +
+ + { `ALERT: ${error}` } +
+ ) +} + +ConfirmPageContainerError.propTypes = { + errorMessage: PropTypes.string, + errorKey: PropTypes.string, +} + +ConfirmPageContainerError.contextTypes = { + t: PropTypes.func, +} + +export default ConfirmPageContainerError diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js new file mode 100644 index 000000000..4ac95d0e3 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-error.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss new file mode 100644 index 000000000..e99b0f631 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss @@ -0,0 +1,17 @@ +.confirm-page-container-error { + height: 32px; + border: 1px solid $monzo; + color: $monzo; + background: lighten($monzo, 56%); + border-radius: 4px; + font-size: .75rem; + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 16px; + + &__icon { + margin-right: 8px; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js new file mode 100644 index 000000000..3b1ee62c5 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -0,0 +1,56 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../identicon' + +const ConfirmPageContainerSummary = props => { + const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props + + return ( +
+
+
+ { action } +
+ { + nonce && ( +
+ { `#${nonce}` } +
+ ) + } +
+
+ { + identiconAddress && ( + + ) + } +
+ { title } +
+
+ { + hideSubtitle ||
+ { subtitle } +
+ } +
+ ) +} + +ConfirmPageContainerSummary.propTypes = { + action: PropTypes.string, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hideSubtitle: PropTypes.bool, + className: PropTypes.string, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, +} + +export default ConfirmPageContainerSummary diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js new file mode 100644 index 000000000..ed1b28cf2 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-summary.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss new file mode 100644 index 000000000..7f0f5d37a --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -0,0 +1,54 @@ +.confirm-page-container-summary { + padding: 16px 24px 0; + background-color: #f9fafa; + height: 133px; + box-sizing: border-box; + + &__action-row { + display: flex; + justify-content: space-between; + } + + &__action { + text-transform: uppercase; + color: $oslo-gray; + font-size: .75rem; + padding: 3px 8px; + border: 1px solid $oslo-gray; + border-radius: 4px; + display: inline-block; + } + + &__nonce { + color: $oslo-gray; + } + + &__title { + padding: 4px 0; + display: flex; + align-items: center; + } + + &__identicon { + flex: 0 0 auto; + margin-right: 8px; + } + + &__title-text { + font-size: 2.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__subtitle { + color: $oslo-gray; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &--border { + border-bottom: 1px solid $geyser; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js new file mode 100644 index 000000000..79901c8fc --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerWarning = props => { + return ( +
+ +
+ { props.warning } +
+
+ ) +} + +ConfirmPageContainerWarning.propTypes = { + warning: PropTypes.string, +} + +export default ConfirmPageContainerWarning diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js new file mode 100644 index 000000000..6e48bd144 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-warning.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss new file mode 100644 index 000000000..50545a1a2 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss @@ -0,0 +1,18 @@ +.confirm-page-container-warning { + background-color: #fffcdb; + display: flex; + justify-content: center; + align-items: center; + border-bottom: 1px solid $geyser; + padding: 12px 24px; + + &__icon { + flex: 0 0 auto; + margin-right: 16px; + } + + &__warning { + font-size: .75rem; + color: #5f5922; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js new file mode 100644 index 000000000..1469dd438 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js @@ -0,0 +1,4 @@ +export { default } from './confirm-page-container-content.component' +export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary' +export { default as ConfirmPageContainerError } from './confirm-page-container-error' +export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss new file mode 100644 index 000000000..39797a43f --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss @@ -0,0 +1,66 @@ +@import './confirm-page-container-error/index'; + +@import './confirm-page-container-warning/index'; + +@import './confirm-page-container-summary/index'; + +.confirm-page-container-content { + overflow-y: auto; + flex: 1; + + &__error-container { + padding: 0 16px 16px 16px; + } + + &__details { + box-sizing: border-box; + padding: 0 24px; + } + + &__data { + padding: 16px; + color: $oslo-gray; + } + + &__data-box { + background-color: #f9fafa; + padding: 12px; + font-size: .75rem; + margin-bottom: 16px; + word-wrap: break-word; + max-height: 200px; + overflow-y: auto; + + &-label { + text-transform: uppercase; + padding: 8px 0 12px; + font-size: 12px; + } + } + + &__data-field { + display: flex; + flex-direction: row; + + &-label { + font-weight: 500; + padding-right: 16px; + } + + &:not(:last-child) { + margin-bottom: 5px; + } + } + + &__gas-fee { + border-bottom: 1px solid $geyser; + } + + &__function-type { + font-size: .875rem; + font-weight: 500; + text-transform: capitalize; + color: $black; + padding-left: 5px; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js new file mode 100644 index 000000000..e6fe8f82c --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, +} from '../../../../../app/scripts/lib/enums' +import NetworkDisplay from '../../network-display' + +export default class ConfirmPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + showEdit: PropTypes.bool, + onEdit: PropTypes.func, + children: PropTypes.node, + } + + renderTop () { + const { onEdit, showEdit } = this.props + const windowType = window.METAMASK_UI_TYPE + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP + + if (!showEdit && isFullScreen) { + return null + } + + return ( +
+
+ + onEdit()} + > + { this.context.t('edit') } + +
+ { !isFullScreen && } +
+ ) + } + + render () { + const { children } = this.props + + return ( +
+ { this.renderTop() } + { children } +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js new file mode 100644 index 000000000..71feb6931 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-header.component' diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss new file mode 100644 index 000000000..43e1e4427 --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss @@ -0,0 +1,27 @@ +.confirm-page-container-header { + display: flex; + flex-direction: column; + flex: 0 0 auto; + + &__row { + display: flex; + justify-content: space-between; + border-bottom: 1px solid $geyser; + padding: 13px 13px 13px 24px; + flex: 0 0 auto; + } + + &__back-button-container { + display: flex; + justify-content: center; + align-items: center; + } + + &__back-button { + color: #2f9ae0; + font-size: 1rem; + cursor: pointer; + font-weight: 400; + padding-left: 5px; + } +} diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js new file mode 100644 index 000000000..93e4ae7bf --- /dev/null +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SenderToRecipient from '../sender-to-recipient' +import { PageContainerFooter } from '../page-container' +import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './' + +export default class ConfirmPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + // Header + action: PropTypes.string, + hideSubtitle: PropTypes.bool, + onEdit: PropTypes.func, + showEdit: PropTypes.bool, + subtitle: PropTypes.string, + title: PropTypes.string, + titleComponent: PropTypes.func, + // Sender to Recipient + fromAddress: PropTypes.string, + fromName: PropTypes.string, + toAddress: PropTypes.string, + toName: PropTypes.string, + // Content + contentComponent: PropTypes.node, + errorKey: PropTypes.string, + errorMessage: PropTypes.string, + fiatTransactionAmount: PropTypes.string, + fiatTransactionFee: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionAmount: PropTypes.string, + ethTransactionFee: PropTypes.string, + ethTransactionTotal: PropTypes.string, + onEditGas: PropTypes.func, + dataComponent: PropTypes.node, + detailsComponent: PropTypes.node, + identiconAddress: PropTypes.string, + nonce: PropTypes.string, + summaryComponent: PropTypes.node, + warning: PropTypes.string, + // Footer + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + valid: PropTypes.bool, + } + + render () { + const { + showEdit, + onEdit, + fromName, + fromAddress, + toName, + toAddress, + valid, + errorKey, + errorMessage, + contentComponent, + action, + title, + titleComponent, + subtitle, + hideSubtitle, + summaryComponent, + detailsComponent, + dataComponent, + onCancel, + onSubmit, + identiconAddress, + nonce, + warning, + } = this.props + + return ( +
+ onEdit()} + > + + + { + contentComponent || ( + + ) + } + onCancel()} + onSubmit={() => onSubmit()} + submitText={this.context.t('confirm')} + submitButtonType="confirm" + disabled={!valid} + /> +
+ ) + } +} diff --git a/ui/app/components/confirm-page-container/index.js b/ui/app/components/confirm-page-container/index.js new file mode 100644 index 000000000..ee88aa5d3 --- /dev/null +++ b/ui/app/components/confirm-page-container/index.js @@ -0,0 +1,8 @@ +export { default } from './confirm-page-container.component' +export { default as ConfirmPageContainerHeader } from './confirm-page-container-header' +export { default as ConfirmDetailRow } from './confirm-detail-row' +export { + default as ConfirmPageContainerContent, + ConfirmPageContainerSummary, + ConfirmPageContainerError, +} from './confirm-page-container-content' diff --git a/ui/app/components/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss new file mode 100644 index 000000000..af7a5b555 --- /dev/null +++ b/ui/app/components/confirm-page-container/index.scss @@ -0,0 +1,5 @@ +@import './confirm-page-container-content/index'; + +@import './confirm-page-container-header/index'; + +@import './confirm-detail-row/index'; diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index c8522a3c7..c255fd64d 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -16,11 +16,11 @@ const { MIN_GAS_PRICE_DEC, MIN_GAS_LIMIT_DEC, MIN_GAS_PRICE_GWEI, -} = require('../send_/send.constants') +} = require('../send/send.constants') const { isBalanceSufficient, -} = require('../send_/send.utils') +} = require('../send/send.utils') const { conversionUtil, @@ -31,8 +31,7 @@ const { } = require('../../conversion-util') const { - getGasPrice, - getGasLimit, + getGasIsLoading, getForceGasMin, conversionRateSelector, getSendAmount, @@ -43,6 +42,11 @@ const { getSendMaxModeState, } = require('../../selectors') +const { + getGasPrice, + getGasLimit, +} = require('../send/send.selectors') + function mapStateToProps (state) { const selectedToken = getSelectedToken(state) const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state) @@ -51,6 +55,7 @@ function mapStateToProps (state) { return { gasPrice: getGasPrice(state), gasLimit: getGasLimit(state), + gasIsLoading: getGasIsLoading(state), forceGasMin: getForceGasMin(state), conversionRate, amount: getSendAmount(state), @@ -73,7 +78,7 @@ function mapDispatchToProps (dispatch) { } } -function getOriginalState (props) { +function getFreshState (props) { const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC @@ -97,7 +102,11 @@ inherits(CustomizeGasModal, Component) function CustomizeGasModal (props) { Component.call(this) - this.state = getOriginalState(props) + const originalState = getFreshState(props) + this.state = { + ...originalState, + originalState, + } } CustomizeGasModal.contextTypes = { @@ -106,6 +115,36 @@ CustomizeGasModal.contextTypes = { module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) +CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) { + const currentState = getFreshState(this.props) + const { + gasPrice: currentGasPrice, + gasLimit: currentGasLimit, + } = currentState + const newState = getFreshState(nextProps) + const { + gasPrice: newGasPrice, + gasLimit: newGasLimit, + gasTotal: newGasTotal, + } = newState + const gasPriceChanged = currentGasPrice !== newGasPrice + const gasLimitChanged = currentGasLimit !== newGasLimit + + if (gasPriceChanged) { + this.setState({ + gasPrice: newGasPrice, + gasTotal: newGasTotal, + priceSigZeros: '', + priceSigDec: '', + }) + } + if (gasLimitChanged) { + this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal }) + } + if (gasLimitChanged || gasPriceChanged) { + this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal }) + } +} CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { const { @@ -137,7 +176,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { } CustomizeGasModal.prototype.revert = function () { - this.setState(getOriginalState(this.props)) + this.setState(this.state.originalState) } CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) { @@ -233,7 +272,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) { } CustomizeGasModal.prototype.render = function () { - const { hideModal, forceGasMin } = this.props + const { hideModal, forceGasMin, gasIsLoading } = this.props const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state let convertedGasPrice = conversionUtil(gasPrice, { @@ -266,7 +305,7 @@ CustomizeGasModal.prototype.render = function () { toNumericBase: 'dec', }) - return h('div.send-v2__customize-gas', {}, [ + return !gasIsLoading && h('div.send-v2__customize-gas', {}, [ h('div.send-v2__customize-gas__content', { }, [ h('div.send-v2__customize-gas__header', {}, [ @@ -288,6 +327,7 @@ CustomizeGasModal.prototype.render = function () { onChange: value => this.convertAndSetGasPrice(value), title: this.context.t('gasPrice'), copy: this.context.t('gasPriceCalculation'), + gasIsLoading, }), h(GasModalCard, { @@ -297,6 +337,7 @@ CustomizeGasModal.prototype.render = function () { onChange: value => this.convertAndSetGasLimit(value), title: this.context.t('gasLimit'), copy: this.context.t('gasLimitCalculation'), + gasIsLoading, }), ]), diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js index a7a908d3b..261eb0aa2 100644 --- a/ui/app/components/dropdowns/account-dropdown-mini.js +++ b/ui/app/components/dropdowns/account-dropdown-mini.js @@ -1,7 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const AccountListItem = require('../send_/account-list-item/account-list-item.component').default +const AccountListItem = require('../send/account-list-item/account-list-item.component').default module.exports = AccountDropdownMini diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js index 7e94e0af5..a45da4c10 100644 --- a/ui/app/components/dropdowns/components/network-dropdown-icon.js +++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js @@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () { backgroundColor, isSelected, innerBorder = 'none', + diameter = '12', } = this.props return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, @@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () { style: { background: backgroundColor, border: innerBorder, + height: `${diameter}px`, + width: `${diameter}px`, }, }) ) diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index fac7c451b..5a794c7c1 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -54,7 +54,7 @@ TokenMenuDropdown.prototype.render = function () { showHideTokenConfirmationModal(this.props.token) this.props.onClose() }, - text: this.context.t('hideToken'), + text: this.context.t('hideToken'), }), h(Item, { onClick: (e) => { @@ -62,7 +62,7 @@ TokenMenuDropdown.prototype.render = function () { copyToClipboard(this.props.token.address) this.props.onClose() }, - text: this.context.t('copyContractAddress'), + text: this.context.t('copyContractAddress'), }), h(Item, { onClick: (e) => { @@ -71,7 +71,7 @@ TokenMenuDropdown.prototype.render = function () { global.platform.openWindow({ url }) this.props.onClose() }, - text: this.context.t('viewOnEtherscan'), + text: this.context.t('viewOnEtherscan'), }), ]) } diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index aff4b6ef6..b9f99b3d1 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -10,8 +10,9 @@ const networkMap = require('ethjs-ens/lib/network-map.json') const ensRE = /.+\..+$/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const connect = require('react-redux').connect -const ToAutoComplete = require('./send/to-autocomplete') +const ToAutoComplete = require('./send/to-autocomplete').default const log = require('loglevel') +const { isValidENSAddress } = require('../util') EnsInput.contextTypes = { t: PropTypes.func, @@ -25,31 +26,34 @@ function EnsInput () { Component.call(this) } +EnsInput.prototype.onChange = function (recipient) { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + + this.props.onChange({ toAddress: recipient }) + + if (!networkHasEnsSupport) return + + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + toError: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName(recipient) +} + EnsInput.prototype.render = function () { const props = this.props const opts = extend(props, { list: 'addresses', - onChange: (recipient) => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - - props.onChange(recipient) - - if (!networkHasEnsSupport) return - - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName(recipient) - }, + onChange: this.onChange.bind(this), }) return h('div', { style: { width: '100%', position: 'relative' }, @@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) { nickname: recipient.trim(), hoverText: address + '\n' + this.context.t('clickCopy'), ensFailure: false, + toError: null, }) } }) .catch((reason) => { - log.error(reason) - return this.setState({ + const setStateObj = { loadingEns: false, - ensResolution: ZERO_ADDRESS, + ensResolution: recipient, ensFailure: true, - hoverText: reason.message, - }) + toError: null, + } + if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { + setStateObj.hoverText = this.context.t('ensNameNotFound') + setStateObj.toError = 'ensNameNotFound' + setStateObj.ensFailure = false + } else { + log.error(reason) + setStateObj.hoverText = reason.message + } + + return this.setState(setStateObj) }) } @@ -105,9 +119,14 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { // If an address is sent without a nickname, meaning not from ENS or from // the user's own accounts, a default of a one-space string is used. const nickname = state.nickname || ' ' + if (prevProps.network !== this.props.network) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network: this.props.network }) + this.onChange(ensResolution) + } if (prevState && ensResolution && this.props.onChange && ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) + this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError }) } } @@ -124,7 +143,9 @@ EnsInput.prototype.ensIcon = function (recipient) { } EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } + + if (toError) return if (loadingEns) { return h('img', { diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index dce9b0449..424048745 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -36,6 +36,7 @@ IdenticonComponent.prototype.render = function () { key: 'identicon-' + address, style: { display: 'flex', + flexShrink: 0, alignItems: 'center', justifyContent: 'center', height: diameter, diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 351640f6e..b3e14ce23 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,9 +1,21 @@ +@import './button-group/index'; + @import './export-text-container/index'; @import './selected-account/index'; @import './info-box/index'; +@import './network-display/index'; + +@import './confirm-page-container/index'; + +@import './page-container/index'; + @import './pages/index'; @import './modals/index'; + +@import './sender-to-recipient/index'; + +@import './tabs/index'; diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js index de5fcca54..59c6842ef 100644 --- a/ui/app/components/input-number.js +++ b/ui/app/components/input-number.js @@ -22,12 +22,16 @@ function isValidInput (text) { return re.test(text) } +function removeLeadingZeroes (str) { + return str.replace(/^0*(?=\d)/, '') +} + InputNumber.prototype.setValue = function (newValue) { + newValue = removeLeadingZeroes(newValue) if (newValue && !isValidInput(newValue)) return const { fixed, min = -1, max = Infinity, onChange } = this.props newValue = fixed ? newValue.toFixed(4) : newValue - const newValueGreaterThanMin = conversionGTE( { value: newValue || '0', fromNumericBase: 'dec' }, { value: min, fromNumericBase: 'hex' }, @@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) { } InputNumber.prototype.render = function () { - const { unitLabel, step = 1, placeholder, value = 0 } = this.props + const { unitLabel, step = 1, placeholder, value } = this.props return h('div.customize-gas-input-wrapper', {}, [ h('input', { @@ -63,11 +67,11 @@ InputNumber.prototype.render = function () { h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('div.gas-tooltip-input-arrows', {}, [ h('i.fa.fa-angle-up', { - onClick: () => this.setValue(addCurrencies(value, step)), + onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })), }), h('i.fa.fa-angle-down', { style: { cursor: 'pointer' }, - onClick: () => this.setValue(subtractCurrencies(value, step)), + onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })), }), ]), ]) diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js new file mode 100644 index 000000000..5a9f0f289 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -0,0 +1,93 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' +import { addressSummary } from '../../../util' +import Identicon from '../../identicon' +import genAccountLink from '../../../../lib/account-link' + +class ConfirmRemoveAccount extends Component { + static propTypes = { + hideModal: PropTypes.func.isRequired, + removeAccount: PropTypes.func.isRequired, + identity: PropTypes.object.isRequired, + network: PropTypes.string.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleRemove () { + this.props.removeAccount(this.props.identity.address) + .then(() => this.props.hideModal()) + } + + renderSelectedAccount () { + const { identity } = this.props + return ( +
+
+ +
+
+ Name + {identity.name} +
+
+ Public Address + { addressSummary(identity.address, 4, 4) } +
+
+ + + +
+
+ ) + } + + render () { + const { t } = this.context + + return ( +
+
+
+ { `${t('removeAccount')}` }? +
+ { this.renderSelectedAccount() } +
+ { t('removeAccountDescription') } + { t('learnMore') } +
+
+
+ + +
+
+ ) + } +} + +export default ConfirmRemoveAccount diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js new file mode 100644 index 000000000..4b194c995 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import ConfirmRemoveAccount from './confirm-remove-account.component' + +const { hideModal, removeAccount } = require('../../../actions') + +const mapStateToProps = state => { + return { + identity: state.appState.modal.modalState.props.identity, + network: state.metamask.network, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + removeAccount: (address) => dispatch(removeAccount(address)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount) diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js new file mode 100644 index 000000000..9763fbe05 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/index.js @@ -0,0 +1,2 @@ +import ConfirmRemoveAccount from './confirm-remove-account.container' +module.exports = ConfirmRemoveAccount diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js new file mode 100644 index 000000000..0337c5413 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.component.js @@ -0,0 +1,140 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import GasModalCard from '../../customize-gas-modal/gas-modal-card' +import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants' + +import { + getDecimalGasLimit, + getDecimalGasPrice, + getPrefixedHexGasLimit, + getPrefixedHexGasPrice, +} from './customize-gas.util' + +export default class CustomizeGas extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + txData: PropTypes.object.isRequired, + hideModal: PropTypes.func, + validate: PropTypes.func, + onSubmit: PropTypes.func, + } + + state = { + gasPrice: 0, + gasLimit: 0, + originalGasPrice: 0, + originalGasLimit: 0, + } + + componentDidMount () { + const { txData = {} } = this.props + const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData + + const gasLimit = getDecimalGasLimit(hexGasLimit) + const gasPrice = getDecimalGasPrice(hexGasPrice) + + this.setState({ + gasPrice, + gasLimit, + originalGasPrice: gasPrice, + originalGasLimit: gasLimit, + }) + } + + handleRevert () { + const { originalGasPrice, originalGasLimit } = this.state + + this.setState({ + gasPrice: originalGasPrice, + gasLimit: originalGasLimit, + }) + } + + handleSave () { + const { onSubmit, hideModal } = this.props + const { gasLimit, gasPrice } = this.state + const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice) + const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit) + + Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit })) + .then(() => hideModal()) + } + + validate () { + const { gasLimit, gasPrice } = this.state + return this.props.validate({ + gasPrice: getPrefixedHexGasPrice(gasPrice), + gasLimit: getPrefixedHexGasLimit(gasLimit), + }) + } + + render () { + const { t } = this.context + const { hideModal } = this.props + const { gasPrice, gasLimit } = this.state + const { valid, errorKey } = this.validate() + + return ( +
+
+
+
+ { this.context.t('customGas') } +
+
hideModal()} + /> +
+
+ this.setState({ gasPrice: value })} + title={t('gasPrice')} + copy={t('gasPriceCalculation')} + /> + this.setState({ gasLimit: value })} + title={t('gasLimit')} + copy={t('gasLimitCalculation')} + /> +
+
+ { !valid &&
{ t(errorKey) }
} +
this.handleRevert()} + > + { t('revert') } +
+
+ + +
+
+
+
+ ) + } +} diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/modals/customize-gas/customize-gas.container.js new file mode 100644 index 000000000..46a799795 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.container.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import CustomizeGas from './customize-gas.component' +import { hideModal } from '../../../actions' + +const mapStateToProps = state => { + const { appState: { modal: { modalState: { props } } } } = state + const { txData, onSubmit, validate } = props + + return { + txData, + onSubmit, + validate, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas) diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/modals/customize-gas/customize-gas.util.js new file mode 100644 index 000000000..6ba4a7705 --- /dev/null +++ b/ui/app/components/modals/customize-gas/customize-gas.util.js @@ -0,0 +1,34 @@ +import ethUtil from 'ethereumjs-util' +import { conversionUtil } from '../../../conversion-util' + +export function getDecimalGasLimit (hexGasLimit) { + return conversionUtil(hexGasLimit, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) +} + +export function getDecimalGasPrice (hexGasPrice) { + return conversionUtil(hexGasPrice, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }) +} + +export function getPrefixedHexGasLimit (gasLimit) { + return ethUtil.addHexPrefix(conversionUtil(gasLimit, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + })) +} + +export function getPrefixedHexGasPrice (gasPrice) { + return ethUtil.addHexPrefix(conversionUtil(gasPrice, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + })) +} diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/modals/customize-gas/index.js new file mode 100644 index 000000000..3a0ab7edc --- /dev/null +++ b/ui/app/components/modals/customize-gas/index.js @@ -0,0 +1 @@ +export { default } from './customize-gas.container' diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/modals/customize-gas/index.scss new file mode 100644 index 000000000..e10452691 --- /dev/null +++ b/ui/app/components/modals/customize-gas/index.scss @@ -0,0 +1,110 @@ +.customize-gas { + border: 1px solid #D8D8D8; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); + font-family: Roboto; + display: flex; + flex-flow: column; + + @media screen and (max-width: $break-small) { + width: 100vw; + height: 100vh; + } + + &__header { + height: 52px; + border-bottom: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__title { + margin-left: 19.25px; + } + + &__close::after { + content: '\00D7'; + font-size: 1.8em; + color: $dusty-gray; + font-family: sans-serif; + cursor: pointer; + margin-right: 19.25px; + } + + &__content { + display: flex; + flex-flow: column nowrap; + height: 100%; + } + + &__body { + display: flex; + margin-bottom: 24px; + + @media screen and (max-width: $break-small) { + flex-flow: column; + flex: 1 1 auto; + } + } + + &__footer { + height: 75px; + border-top: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + position: relative; + + @media screen and (max-width: $break-small) { + flex: 0 0 auto; + } + } + + &__buttons { + display: flex; + justify-content: space-between; + margin-right: 21.25px; + } + + &__revert, &__cancel, &__save, &__save__error { + display: flex; + justify-content: center; + align-items: center; + padding: 0 3px; + cursor: pointer; + } + + &__revert { + color: $silver-chalice; + font-size: 16px; + margin-left: 21.25px; + } + + &__cancel, &__save, &__save__error { + width: 85.74px; + min-width: initial; + } + + &__save__error { + opacity: 0.5; + cursor: auto; + } + + &__error-message { + display: block; + position: absolute; + top: 4px; + right: 4px; + font-size: 12px; + line-height: 12px; + color: $red; + } +} diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index ad6fe16d3..e198cca44 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -1,3 +1,5 @@ +@import './customize-gas/index'; + .modal-container { width: 100%; height: 100%; @@ -18,6 +20,58 @@ font-size: .875rem; } + &__account { + border: 1px solid #b7b7b7; + border-radius: 4px; + padding: 10px; + display: flex; + margin-top: 10px; + margin-bottom: 20px; + width: 100%; + + &__identicon { + margin-right: 10px; + } + + &__name, + &__address { + margin-right: 10px; + font-size: 14px; + } + + &__name { + width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__label { + font-size: 11px; + display: block; + color: #9b9b9b; + } + + &__link { + margin-top: 14px; + + img { + width: 15px; + height: 15px; + } + } + + @media screen and (max-width: 575px) { + &__name { + width: 90px; + } + } + } + + &__link { + color: #2f9ae0; + } + &__content { overflow-y: auto; flex: 1; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 85e85597a..f59825ed1 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -20,10 +20,13 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const ConfirmResetAccount = require('./confirm-reset-account') +const ConfirmRemoveAccount = require('./confirm-remove-account') const TransactionConfirmed = require('./transaction-confirmed') const WelcomeBeta = require('./welcome-beta') const Notification = require('./notification') +import ConfirmCustomizeGasModal from './customize-gas' + const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', border: '1px solid #CCCFD1', @@ -241,6 +244,19 @@ const MODALS = { }, }, + CONFIRM_REMOVE_ACCOUNT: { + contents: h(ConfirmRemoveAccount), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), @@ -267,7 +283,31 @@ const MODALS = { CUSTOMIZE_GAS: { contents: [ - h(CustomizeGasModal, {}, []), + h(CustomizeGasModal), + ], + mobileModalStyle: { + width: '100vw', + height: '100vh', + top: '0', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: '720px', + height: '377px', + top: '80px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + }, + + CONFIRM_CUSTOMIZE_GAS: { + contents: [ + h(ConfirmCustomizeGasModal), ], mobileModalStyle: { width: '100vw', diff --git a/ui/app/components/network-display.js b/ui/app/components/network-display.js deleted file mode 100644 index 59719d9a4..000000000 --- a/ui/app/components/network-display.js +++ /dev/null @@ -1,56 +0,0 @@ -const { Component } = require('react') -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon') - -const networkToColorHash = { - 1: '#038789', - 3: '#e91550', - 42: '#690496', - 4: '#ebb33f', -} - -class NetworkDisplay extends Component { - renderNetworkIcon () { - const { network } = this.props - const networkColor = networkToColorHash[network] - - return networkColor - ? h(NetworkDropdownIcon, { backgroundColor: networkColor }) - : h('i.fa.fa-question-circle.fa-med', { - style: { - margin: '0 4px', - color: 'rgb(125, 128, 130)', - }, - }) - } - - render () { - const { provider: { type } } = this.props - return h('.network-display__container', [ - this.renderNetworkIcon(), - h('.network-name', this.context.t(type)), - ]) - } -} - -NetworkDisplay.propTypes = { - network: PropTypes.string, - provider: PropTypes.object, - t: PropTypes.func, -} - -const mapStateToProps = ({ metamask: { network, provider } }) => { - return { - network, - provider, - } -} - -NetworkDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(NetworkDisplay) - diff --git a/ui/app/components/network-display/index.js b/ui/app/components/network-display/index.js new file mode 100644 index 000000000..f6878ae5b --- /dev/null +++ b/ui/app/components/network-display/index.js @@ -0,0 +1,2 @@ +import NetworkDisplay from './network-display.container' +module.exports = NetworkDisplay diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss new file mode 100644 index 000000000..2085cff67 --- /dev/null +++ b/ui/app/components/network-display/index.scss @@ -0,0 +1,54 @@ +.network-display { + &__container { + display: flex; + align-items: center; + justify-content: flex-start; + background-color: lighten(rgb(125, 128, 130), 45%); + padding: 0 10px; + border-radius: 4px; + height: 25px; + + &--mainnet { + background-color: lighten($blue-lagoon, 68%); + } + + &--ropsten { + background-color: lighten($crimson, 45%); + } + + &--kovan { + background-color: lighten($purple, 65%); + } + + &--rinkeby { + background-color: lighten($tulip-tree, 35%); + } + } + + &__name { + font-size: .75rem; + padding-left: 5px; + } + + &__icon { + height: 10px; + width: 10px; + border-radius: 10px; + + &--mainnet { + background-color: $blue-lagoon; + } + + &--ropsten { + background-color: $crimson; + } + + &--kovan { + background-color: $purple; + } + + &--rinkeby { + background-color: $tulip-tree; + } + } +} diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js new file mode 100644 index 000000000..38626af20 --- /dev/null +++ b/ui/app/components/network-display/network-display.component.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { + MAINNET_CODE, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, +} from '../../../../app/scripts/controllers/network/enums' + +const networkToClassHash = { + [MAINNET_CODE]: 'mainnet', + [ROPSTEN_CODE]: 'ropsten', + [RINKEYBY_CODE]: 'rinkeby', + [KOVAN_CODE]: 'kovan', +} + +export default class NetworkDisplay extends Component { + static propTypes = { + network: PropTypes.string, + provider: PropTypes.object, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderNetworkIcon () { + const { network } = this.props + const networkClass = networkToClassHash[network] + + return networkClass + ?
+ :
+ } + + render () { + const { network, provider: { type } } = this.props + const networkClass = networkToClassHash[network] + + return ( +
+ { + networkClass + ?
+ :
+ } +
+ { this.context.t(type) } +
+
+ ) + } +} diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/network-display/network-display.container.js new file mode 100644 index 000000000..99a14fff4 --- /dev/null +++ b/ui/app/components/network-display/network-display.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import NetworkDisplay from './network-display.component' + +const mapStateToProps = ({ metamask: { network, provider } }) => { + return { + network, + provider, + } +} + +export default connect(mapStateToProps)(NetworkDisplay) diff --git a/ui/app/components/page-container/index.js b/ui/app/components/page-container/index.js index 415870b37..913b8c9c6 100644 --- a/ui/app/components/page-container/index.js +++ b/ui/app/components/page-container/index.js @@ -1 +1,4 @@ +import PageContainerHeader from './page-container-header' +import PageContainerFooter from './page-container-footer' export { default } from './page-container.component' +export { PageContainerHeader, PageContainerFooter } diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss new file mode 100644 index 000000000..06c3ef709 --- /dev/null +++ b/ui/app/components/page-container/index.scss @@ -0,0 +1,186 @@ +.page-container { + width: 408px; + background-color: $white; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); + z-index: 25; + display: flex; + flex-flow: column; + border-radius: 8px; + + &__header { + display: flex; + flex-flow: column; + border-bottom: 1px solid $geyser; + padding: 16px; + flex: 0 0 auto; + position: relative; + + &--no-padding-bottom { + padding-bottom: 0; + } + } + + &__header-close { + color: $tundora; + position: absolute; + top: 16px; + right: 16px; + cursor: pointer; + overflow: hidden; + + &::after { + content: '\00D7'; + font-size: 40px; + line-height: 20px; + } + } + + &__header-row { + padding-bottom: 10px; + display: flex; + justify-content: space-between; + } + + &__footer { + display: flex; + flex-flow: row; + justify-content: center; + border-top: 1px solid $geyser; + padding: 16px; + flex: 0 0 auto; + + .btn-default, + .btn-confirm { + font-size: 1rem; + } + } + + &__footer-button { + height: 55px; + font-size: 1rem; + text-transform: uppercase; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + } + + &__back-button { + color: #2f9ae0; + font-size: 1rem; + cursor: pointer; + font-weight: 400; + } + + &__title { + color: $black; + font-size: 2rem; + font-weight: 500; + line-height: 2rem; + } + + &__subtitle { + padding-top: .5rem; + line-height: initial; + font-size: .9rem; + color: $gray; + } + + &__tabs { + display: flex; + margin-top: 16px; + } + + &__tab { + min-width: 5rem; + padding: 8px; + color: $dusty-gray; + font-family: Roboto; + font-size: 1rem; + text-align: center; + cursor: pointer; + border-bottom: none; + margin-right: 16px; + + &:last-of-type { + margin-right: 0; + } + + &--selected { + color: $curious-blue; + border-bottom: 3px solid $curious-blue; + } + } + + &--full-width { + width: 100% !important; + } + + &--full-height { + height: 100% !important; + max-height: initial !important; + min-height: initial !important; + } + + &__content { + overflow-y: auto; + flex: 1; + } + + &__warning-container { + background: $linen; + padding: 20px; + display: flex; + align-items: start; + } + + &__warning-message { + padding-left: 15px; + } + + &__warning-title { + font-weight: 500; + } + + &__warning-icon { + padding-top: 5px; + } +} + +@media screen and (max-width: 250px) { + .page-container { + &__footer { + flex-flow: column-reverse; + } + + &__footer-button { + width: 100%; + margin-bottom: 1rem; + margin-right: 0; + + &:first-of-type { + margin-bottom: 0; + } + } + } +} + +@media screen and (max-width: 575px) { + .page-container { + height: 100%; + width: 100%; + overflow-y: auto; + background-color: $white; + border-radius: 0; + flex: 1; + } +} + +@media screen and (min-width: 576px) { + .page-container { + max-height: 82vh; + min-height: 570px; + flex: 0 0 auto; + } +} diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js index 0458ae78a..3d15df294 100644 --- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js @@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component { onSubmit: PropTypes.func, submitText: PropTypes.string, disabled: PropTypes.bool, + submitButtonType: PropTypes.string, } static contextTypes = { @@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component { onSubmit, submitText, disabled, + submitButtonType, } = this.props return ( @@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {