diff --git a/.circleci/config.yml b/.circleci/config.yml index 4955fe38f..4d0d404bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -123,12 +123,18 @@ jobs: - checkout - attach_workspace: at: . + - run: + name: Bump manifest version + command: .circleci/scripts/release-bump-manifest-version.sh + - run: + name: Update changelog + command: yarn update-changelog + - run: + name: Commit changes + command: .circleci/scripts/release-commit-version-bump.sh - run: name: Create GitHub Pull Request for version - command: | - .circleci/scripts/release-bump-changelog-version.sh - .circleci/scripts/release-bump-manifest-version.sh - .circleci/scripts/release-create-release-pr.sh + command: .circleci/scripts/release-create-release-pr.sh prep-deps: executor: node-browsers @@ -470,9 +476,6 @@ jobs: name: Create GitHub release command: | .circleci/scripts/release-create-gh-release.sh - - run: - name: Create GitHub Pull Request to sync master with develop - command: .circleci/scripts/release-create-master-pr.sh job-publish-storybook: executor: node-browsers diff --git a/.circleci/scripts/release-bump-changelog-version.sh b/.circleci/scripts/release-bump-changelog-version.sh index 9fd4ddbb8..04f22cc4f 100755 --- a/.circleci/scripts/release-bump-changelog-version.sh +++ b/.circleci/scripts/release-bump-changelog-version.sh @@ -21,13 +21,12 @@ version="${CIRCLE_BRANCH/Version-v/}" if ! grep --quiet --fixed-strings "$version" CHANGELOG.md then printf '%s\n' 'Adding this release to CHANGELOG.md' - date_str="$(date '+%a %b %d %Y')" cp CHANGELOG.md{,.bak} update_headers=$(cat < develop" --message 'Merge latest release back into develop' \ - --base "$CIRCLE_PROJECT_USERNAME:$base_branch" \ - --head "$CIRCLE_PROJECT_USERNAME:$CIRCLE_BRANCH"; -then - printf '%s\n' 'Pull Request already exists' -fi diff --git a/.eslintrc.js b/.eslintrc.js index 322d41a5e..d78f53a57 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,7 +26,7 @@ module.exports = { 'test-*/**', 'docs/**', 'coverage/', - 'app/scripts/chromereload.js', + 'development/chromereload.js', 'app/vendor/**', 'test/e2e/send-eth-with-private-key-test/**', 'nyc_output/**', @@ -37,12 +37,10 @@ module.exports = { extends: [ '@metamask/eslint-config', '@metamask/eslint-config/config/nodejs', - '@metamask/eslint-config/config/mocha', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', + 'prettier', ], - plugins: ['@babel', 'react', 'import', 'prettier'], + plugins: ['@babel', 'import', 'prettier'], globals: { document: 'readonly', @@ -53,90 +51,10 @@ module.exports = { // Prettier changes and reasoning 'prettier/prettier': 'error', - - // Our eslint config has the default setting for this as error. This - // include beforeBlockComment: true, but in order to match the prettier - // spec you have to enable before and after blocks, objects and arrays - // https://github.com/prettier/eslint-config-prettier#lines-around-comment - 'lines-around-comment': [ - 'error', - { - beforeBlockComment: true, - afterLineComment: false, - allowBlockStart: true, - allowBlockEnd: true, - allowObjectStart: true, - allowObjectEnd: true, - allowArrayStart: true, - allowArrayEnd: true, - }, - ], - // Prettier has some opinions on mixed-operators, and there is ongoing work - // to make the output code clear. It is better today then it was when the first - // PR to add prettier. That being said, the workaround for keeping this rule enabled - // requires breaking parts of operations into different variables -- which I believe - // to be worse. https://github.com/prettier/eslint-config-prettier#no-mixed-operators - 'no-mixed-operators': 'off', - // Prettier wraps single line functions with ternaries, etc in parens by default, but - // if the line is long enough it breaks it into a separate line and removes the parens. - // The second behavior conflicts with this rule. There is some guides on the repo about - // how you can keep it enabled: - // https://github.com/prettier/eslint-config-prettier#no-confusing-arrow - // However, in practice this conflicts with prettier adding parens around short lines, - // when autofixing in vscode and others. - 'no-confusing-arrow': 'off', - // There is no configuration in prettier for how it stylizes regexes, which conflicts - // with wrap-regex. - 'wrap-regex': 'off', - // Prettier handles all indentation automagically. it can be configured here - // https://prettier.io/docs/en/options.html#tab-width but the default matches our - // style. - indent: 'off', - // This rule conflicts with the way that prettier breaks code across multiple lines when - // it exceeds the maximum length. Prettier optimizes for readability while simultaneously - // maximizing the amount of code per line. - 'function-paren-newline': 'off', - // This rule throws an error when there is a line break in an arrow function declaration - // but prettier breaks arrow function declarations to be as readable as possible while - // still conforming to the width rules. - 'implicit-arrow-linebreak': 'off', - // This rule would result in an increase in white space in lines with generator functions, - // which impacts prettier's goal of maximizing code per line and readability. There is no - // current workaround. - 'generator-star-spacing': 'off', - 'default-param-last': 'off', - 'require-atomic-updates': 'off', 'import/no-unassigned-import': 'off', 'prefer-object-spread': 'error', - 'react/no-unused-prop-types': 'error', - 'react/no-unused-state': 'error', - 'react/jsx-boolean-value': 'error', - 'react/jsx-curly-brace-presence': [ - 'error', - { props: 'never', children: 'never' }, - ], - 'react/jsx-equals-spacing': 'error', - 'react/no-deprecated': 'error', - 'react/default-props-match-prop-types': 'error', - 'react/jsx-closing-tag-location': [ - 'error', - { selfClosing: 'tag-aligned', nonEmpty: 'tag-aligned' }, - ], - 'react/jsx-no-duplicate-props': 'error', - 'react/jsx-closing-bracket-location': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline'], - 'react/jsx-max-props-per-line': [ - 'error', - { maximum: 1, when: 'multiline' }, - ], - 'react/jsx-tag-spacing': [ - 'error', - { - closingSlash: 'never', - beforeSelfClosing: 'always', - afterOpening: 'never', - }, - ], + 'default-param-last': 'off', + 'require-atomic-updates': 'off', 'no-invalid-this': 'off', '@babel/no-invalid-this': 'error', @@ -145,7 +63,6 @@ module.exports = { semi: 'off', '@babel/semi': 'off', - 'mocha/no-setup-in-describe': 'off', 'node/no-process-env': 'off', // TODO: re-enable these rules @@ -155,9 +72,28 @@ module.exports = { }, overrides: [ { - files: ['test/e2e/**/*.js'], + files: ['ui/**/*.js', 'test/lib/render-helpers.js'], + plugins: ['react'], + extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], + rules: { + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'error', + 'react/jsx-boolean-value': 'error', + 'react/jsx-curly-brace-presence': [ + 'error', + { props: 'never', children: 'never' }, + ], + 'react/no-deprecated': 'error', + 'react/default-props-match-prop-types': 'error', + 'react/jsx-no-duplicate-props': 'error', + }, + }, + { + files: ['test/e2e/**/*.spec.js'], + extends: ['@metamask/eslint-config/config/mocha'], rules: { 'mocha/no-hooks-for-single-case': 'off', + 'mocha/no-setup-in-describe': 'off', }, }, { @@ -173,14 +109,18 @@ module.exports = { }, }, { - files: ['test/**/*-test.js', 'test/**/*.spec.js'], + files: ['**/*.test.js'], + extends: ['@metamask/eslint-config/config/mocha'], rules: { - // Mocha will re-assign `this` in a test context - '@babel/no-invalid-this': 'off', + 'mocha/no-setup-in-describe': 'off', }, }, { - files: ['development/**/*.js', 'test/e2e/benchmark.js', 'test/helper.js'], + files: [ + 'development/**/*.js', + 'test/e2e/benchmark.js', + 'test/helpers/setup-helper.js', + ], rules: { 'node/no-process-exit': 'off', 'node/shebang': 'off', diff --git a/.gitignore b/.gitignore index 5e46dd1e0..d2f051b18 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ audit.json app/bower_components test/bower_components package +.eslintcache # IDEs .idea diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 99295cab0..ef9e15e64 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -5,7 +5,6 @@ const state = { isInitialized: true, isUnlocked: true, featureFlags: { sendHexData: true }, - rpcUrl: 'https://rawtestrpc.metamask.io/', identities: { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e96f45c6..042013027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,18 @@ # Changelog -## Current Develop Branch +## [Unreleased] -## 9.3.0 Fri Mar 26 2021 +## [9.3.0] - 2021-04-02 - [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen - [#10721](https://github.com/MetaMask/metamask-extension/pull/10721): Swaps support for the Binance network - [#10658](https://github.com/MetaMask/metamask-extension/pull/10658): Swaps support for forked Mainnet on localhost - [#10650](https://github.com/MetaMask/metamask-extension/pull/10650): Fix: ETH now only appears once in the swaps "to" and "from" dropdowns. -## 9.2.1 Thu Mar 25 2021 +## [9.2.1] - 2021-03-26 - [#10692](https://github.com/MetaMask/metamask-extension/pull/10692): Prevent UI crash when a 'wallet_requestPermissions" confirmation is queued behind a "wallet_addEthereumChain" confirmation - [#10712](https://github.com/MetaMask/metamask-extension/pull/10712): Fix infinite spinner when request for token symbol fails while attempting an approve transaction -## 9.2.0 Tue Mar 09 2021 +## [9.2.0] - 2021-03-15 - [#10546](https://github.com/MetaMask/metamask-extension/pull/10546): Add a warning when sending a token to its own contract address - [#10563](https://github.com/MetaMask/metamask-extension/pull/10563): Update references to MetaMask support - [#10126](https://github.com/MetaMask/metamask-extension/pull/10126): Update Italian translation @@ -25,11 +25,11 @@ - [#10505](https://github.com/MetaMask/metamask-extension/pull/10505): Add support for multiple Ledger & Trezor hardware accounts - [#10587](https://github.com/MetaMask/metamask-extension/pull/10587): Show correct block explorer for custom RPC endpoints for built-in networks -## 9.1.1 Wed Mar 03 2021 +## [9.1.1] - 2021-03-03 - [#10560](https://github.com/MetaMask/metamask-extension/pull/10560): Fix ENS resolution related crashes when switching networks on send screen - [#10561](https://github.com/MetaMask/metamask-extension/pull/10561): Fix crash when speeding up an attempt to cancel a transaction on custom networks -## 9.1.0 Mon Mar 01 2021 +## [9.1.0] - 2021-02-01 - [#10265](https://github.com/MetaMask/metamask-extension/pull/10265): Update Japanese translations. - [#9388](https://github.com/MetaMask/metamask-extension/pull/9388): Update Chinese(Simplified) translations. - [#10270](https://github.com/MetaMask/metamask-extension/pull/10270): Update Vietnamese translations. @@ -64,7 +64,7 @@ - [#9187](https://github.com/MetaMask/metamask-extension/pull/9187): Warn users when an ENS name contains 'confusable' characters - [#10507](https://github.com/MetaMask/metamask-extension/pull/10507): Fixes ENS IPFS resolution on custom networks with the chainID of 1. -## 9.0.5 Mon Feb 08 2021 +## [9.0.5] - 2021-02-09 - [#10278](https://github.com/MetaMask/metamask-extension/pull/10278): Allow editing transaction amount after clicking max - [#10214](https://github.com/MetaMask/metamask-extension/pull/10214): Standardize size, shape and color of network color indicators - [#10298](https://github.com/MetaMask/metamask-extension/pull/10298): Use network primary currency instead of always defaulting to ETH in the confirm approve screen @@ -79,7 +79,7 @@ - [#10326](https://github.com/MetaMask/metamask-extension/pull/10326): Throw error when attempting to get an encryption key via eth_getEncryptionPublicKey when connected to Ledger HW - [#10386](https://github.com/MetaMask/metamask-extension/pull/10386): Make action buttons on message components in swaps flow accessible -## 9.0.4 Fri Jan 22 2021 +## [9.0.4] - 2021-01-27 - [#10285](https://github.com/MetaMask/metamask-extension/pull/10285): Update @metamask/contract-metadata from v1.21.0 to 1.22.0 - [#10264](https://github.com/MetaMask/metamask-extension/pull/10264): Update `hi` localized messages - [#10174](https://github.com/MetaMask/metamask-extension/pull/10174): Move fox to bottom of 'About' page @@ -93,24 +93,24 @@ - [#10166](https://github.com/MetaMask/metamask-extension/pull/10166): Fix back button on swaps loading page - [#9947](https://github.com/MetaMask/metamask-extension/pull/9947): Do not publish swaps transaction if the estimateGas call made when adding the transaction fails. -## 9.0.3 Fri Jan 22 2021 +## [9.0.3] - 2021-01-22 - [#10243](https://github.com/MetaMask/metamask-extension/pull/10243): Fix site metadata handling - [#10252](https://github.com/MetaMask/metamask-extension/pull/10252): Fix decrypt message confirmation UI crash -## 9.0.2 Wed Jan 20 2021 +## [9.0.2] - 2021-01-20 - [#10191](https://github.com/MetaMask/metamask-extension/pull/10191): zh_TW: 乙太 -> 以太 (#10191) - [#10207](https://github.com/MetaMask/metamask-extension/pull/10207): zh_TW: Translate buy, assets, activity (#10207) - [#10219](https://github.com/MetaMask/metamask-extension/pull/10219): Restore provider 'data' event (#10219) -## 9.0.1 Wed Jan 13 2021 +## [9.0.1] - 2021-01-13 - [#10169](https://github.com/MetaMask/metamask-extension/pull/10169): Improved detection of contract methods with array parameters - [#10178](https://github.com/MetaMask/metamask-extension/pull/10178): Only warn of injected web3 usage once per page - [#10179](https://github.com/MetaMask/metamask-extension/pull/10179): Restore support for @metamask/inpage provider@"< 8.0.0" - [#10180](https://github.com/MetaMask/metamask-extension/pull/10180): Fix UI crash when domain metadata is missing on public encryption key confirmation page -## 9.0.0 Fri Jan 8 2021 +## [9.0.0] - 2021-01-12 - [#9156](https://github.com/MetaMask/metamask-extension/pull/9156): Remove window.web3 injection - [#10039](https://github.com/MetaMask/metamask-extension/pull/10039): Add web3 shim usage notification @@ -132,11 +132,11 @@ - [#9772](https://github.com/MetaMask/metamask-extension/pull/9772): Improve zh_CN translation - [#10170](https://github.com/MetaMask/metamask-extension/pull/10170): Fix bug where swaps button was disabled on Mainnet if the user hadn't switched networks in a long time -## 8.1.11 Thu Jan 07 2021 +## [8.1.11] - 2021-01-07 - [#10155](https://github.com/MetaMask/metamask-extension/pull/10155): Disable swaps when the current network's chainId does not match the mainnet chain ID, instead of disabling based on network ID -## 8.1.10 Fri Dec 18 2020 +## [8.1.10] - 2021-01-04 - [#10084](https://github.com/MetaMask/metamask-extension/pull/10084): Set last provider when switching to a customRPC - [#10096](https://github.com/MetaMask/metamask-extension/pull/10096): Update `@metamask/controllers` to v5.1.0 @@ -144,7 +144,7 @@ - [#10104](https://github.com/MetaMask/metamask-extension/pull/10104): Bump @metamask/contract-metadata from 1.19.0 to 1.20.0 - [#10110](https://github.com/MetaMask/metamask-extension/pull/10110): Fix frozen loading screen on Firefox when strict Enhanced Tracking Protection is enabled -## 8.1.9 Tue Dec 15 2020 +## [8.1.9] - 2020-12-15 - [#10034](https://github.com/MetaMask/metamask-extension/pull/10034): Fix contentscript injection failure on Firefox 56 - [#10045](https://github.com/MetaMask/metamask-extension/pull/10045): Fix token validation in Send flow @@ -154,18 +154,18 @@ - [#10069](https://github.com/MetaMask/metamask-extension/pull/10069): Fetch swap quote refresh time from API - [#10040](https://github.com/MetaMask/metamask-extension/pull/10040): Disable console in contentscript to reduce noise -## 8.1.8 Wed Dec 09 2020 +## [8.1.8] - 2020-12-09 - [#9992](https://github.com/MetaMask/metamask-extension/pull/9992): Improve transaction params validation - [#9991](https://github.com/MetaMask/metamask-extension/pull/9991): Don't allow more than 15% slippage - [#9994](https://github.com/MetaMask/metamask-extension/pull/9994): Prevent unwanted 'no quotes available' message when going back to build quote screen while having insufficient funds - [#9999](https://github.com/MetaMask/metamask-extension/pull/9999): Fix missing contacts upon restart -## 8.1.7 Tue Dec 08 2020 +## [8.1.7] - 2020-12-09 - Revert SES lockdown -## 8.1.6 Wed Dec 02 2020 +## [8.1.6] - 2020-12-04 - [#9916](https://github.com/MetaMask/metamask-extension/pull/9916): Fix QR code scans interpretting payment requests as token addresses - [#9847](https://github.com/MetaMask/metamask-extension/pull/9847): Add alt text for images in list items @@ -177,7 +177,7 @@ - [#9984](https://github.com/MetaMask/metamask-extension/pull/9984): Show correct gas estimates when users don't have sufficient balance for contract transaction - [#9993](https://github.com/MetaMask/metamask-extension/pull/9993): Add 48x48 MetaMask icon for use by browsers -## 8.1.5 Wed Nov 18 2020 +## [8.1.5] - 2020-11-19 - [#9871](https://github.com/MetaMask/metamask-extension/pull/9871): Show send text upon hover in main asset list - [#9855](https://github.com/MetaMask/metamask-extension/pull/9855): Make edit icon and account name in account details modal focusable @@ -192,7 +192,7 @@ - [#9911](https://github.com/MetaMask/metamask-extension/pull/9911): Fix display of Ledger connection error - [#9918](https://github.com/MetaMask/metamask-extension/pull/9918): Fix missing icon in asset page dropdown and in advanced gas modal button group -## 8.1.4 Tue Nov 10 2020 +## [8.1.4] - 2020-11-16 - [#9687](https://github.com/MetaMask/metamask-extension/pull/9687): Allow speeding up of underpriced transactions - [#9694](https://github.com/MetaMask/metamask-extension/pull/9694): normalize UI component font styles @@ -227,7 +227,7 @@ - [#9871](https://github.com/MetaMask/metamask-extension/pull/9871): Show send text upon hover in main asset list - [#9880](https://github.com/MetaMask/metamask-extension/pull/9880): Properly detect U2F errors in hardware wallet -## 8.1.3 Mon Oct 26 2020 +## [8.1.3] - 2020-10-29 - [#9642](https://github.com/MetaMask/metamask-extension/pull/9642) Prevent excessive overflow from swap dropdowns - [#9658](https://github.com/MetaMask/metamask-extension/pull/9658): Fix sorting Quote Source column of quote sort list @@ -246,7 +246,7 @@ - [#9743](https://github.com/MetaMask/metamask-extension/pull/9743): Fix "+-" prefix on swap token amount - [#9715](https://github.com/MetaMask/metamask-extension/pull/9715): Focus on wallet address in buy workflow -## 8.1.2 Mon Oct 19 2020 +## [8.1.2] - 2020-10-20 - [#9608](https://github.com/MetaMask/metamask-extension/pull/9608): Ensure QR code scanner works - [#9624](https://github.com/MetaMask/metamask-extension/pull/9624): Help users avoid insufficient gas prices in swaps @@ -255,7 +255,7 @@ - [#9630](https://github.com/MetaMask/metamask-extension/pull/9630): Fix UI crash when trying to render estimated time remaining of non-submitted transaction - [#9633](https://github.com/MetaMask/metamask-extension/pull/9633): Update View Quote page to better represent the MetaMask fee -## 8.1.1 Tue Oct 13 2020 +## [8.1.1] - 2020-10-15 - [#9586](https://github.com/MetaMask/metamask-extension/pull/9586): Prevent build quote crash when swapping from non-tracked token with balance (#9586) - [#9592](https://github.com/MetaMask/metamask-extension/pull/9592): Remove commitment to maintain a public metrics dashboard (#9592) @@ -268,7 +268,7 @@ - [#9602](https://github.com/MetaMask/metamask-extension/pull/9602): Prevent swap button from being focused when disabled (#9602) - [#9609](https://github.com/MetaMask/metamask-extension/pull/9609): Ensure swaps customize gas modal values are set correctly (#9609) -## 8.1.0 Tue Oct 13 2020 +## [8.1.0] - 2020-10-13 - [#9565](https://github.com/MetaMask/metamask-extension/pull/9565): Ensure address book entries are shared between networks with the same chain ID - [#9552](https://github.com/MetaMask/metamask-extension/pull/9552): Fix `eth_signTypedData_v4` chain ID validation for non-default networks @@ -301,25 +301,25 @@ - [#9152](https://github.com/MetaMask/metamask-extension/pull/9152): Fix vertical align of the network name in network dropdown button - [#9073](https://github.com/MetaMask/metamask-extension/pull/9073): Use new Euclid font throughout MetaMask -## 8.0.10 Wed Sep 16 2020 +## [8.0.10] - 2020-09-16 - [#9423](https://github.com/MetaMask/metamask-extension/pull/9423): Update default phishing list - [#9416](https://github.com/MetaMask/metamask-extension/pull/9416): Fix fetching a new phishing list on Firefox -## 8.0.9 Wed Aug 19 2020 +## [8.0.9] - 2020-08-19 - [#9228](https://github.com/MetaMask/metamask-extension/pull/9228): Move transaction confirmation footer buttons to scrollable area - [#9256](https://github.com/MetaMask/metamask-extension/pull/9256): Handle non-String web3 property access - [#9266](https://github.com/MetaMask/metamask-extension/pull/9266): Use @metamask/controllers@2.0.5 - [#9189](https://github.com/MetaMask/metamask-extension/pull/9189): Hide ETH Gas Station estimates on non-main network -## 8.0.8 Fri Aug 14 2020 +## [8.0.8] - 2020-08-14 - [#9211](https://github.com/MetaMask/metamask-extension/pull/9211): Fix Etherscan redirect on notification click - [#9237](https://github.com/MetaMask/metamask-extension/pull/9237): Reduce volume of web3 usage metrics - [#9227](https://github.com/MetaMask/metamask-extension/pull/9227): Permit all-caps addresses -## 8.0.7 Fri Aug 07 2020 +## [8.0.7] - 2020-08-10 - [#9065](https://github.com/MetaMask/metamask-extension/pull/9065): Change title of "Reveal Seed Words" page to "Reveal Seed Phrase" - [#8974](https://github.com/MetaMask/metamask-extension/pull/8974): Add tooltip to copy button for contacts and seed phrase @@ -332,7 +332,7 @@ - [#9152](https://github.com/MetaMask/metamask-extension/pull/9152): Fix network name alignment - [#9144](https://github.com/MetaMask/metamask-extension/pull/9144): Add web3 usage metrics and prepare for web3 removal -## 8.0.6 Wed Jul 22 2020 +## [8.0.6] - 2020-07-23 - [#9030](https://github.com/MetaMask/metamask-extension/pull/9030): Hide "delete" button when editing contact of wallet account - [#9031](https://github.com/MetaMask/metamask-extension/pull/9031): Fix crash upon removing contact @@ -342,7 +342,7 @@ - [#9051](https://github.com/MetaMask/metamask-extension/pull/9051): Use content-hash@2.5.2 - [#9056](https://github.com/MetaMask/metamask-extension/pull/9056): Display at least one significant digit of small non-zero token balances -## 8.0.5 Thu Jul 16 2020 +## [8.0.5] - 2020-07-17 - [#8942](https://github.com/MetaMask/metamask-extension/pull/8942): Fix display of incoming transactions (#8942) - [#8998](https://github.com/MetaMask/metamask-extension/pull/8998): Fix `web3_clientVersion` method (#8998) @@ -355,19 +355,19 @@ - [#9025](https://github.com/MetaMask/metamask-extension/pull/9025): Catch gas estimate errors (#9025) - [#9026](https://github.com/MetaMask/metamask-extension/pull/9026): Clear transactions on createNewVaultAndRestore (#9026) -## 8.0.4 Tue Jul 07 2020 +## [8.0.4] - 2020-07-08 - [#8934](https://github.com/MetaMask/metamask-extension/pull/8934): Fix transaction activity on custom networks - [#8936](https://github.com/MetaMask/metamask-extension/pull/8936): Fix account tracker optimization -## 8.0.3 Mon Jul 06 2020 +## [8.0.3] - 2020-07-06 - [#8921](https://github.com/MetaMask/metamask-extension/pull/8921): Restore missing 'data' provider event, and fix 'notification' event - [#8923](https://github.com/MetaMask/metamask-extension/pull/8923): Normalize the 'from' parameter for `eth_sendTransaction` - [#8924](https://github.com/MetaMask/metamask-extension/pull/8924): Fix handling of multiple `eth_requestAccount` messages from the same domain - [#8917](https://github.com/MetaMask/metamask-extension/pull/8917): Update Italian translations -## 8.0.2 Fri Jul 03 2020 +## [8.0.2] - 2020-07-03 - [#8907](https://github.com/MetaMask/metamask-extension/pull/8907): Tolerate missing or falsey substitutions - [#8908](https://github.com/MetaMask/metamask-extension/pull/8908): Fix activity log inline buttons @@ -375,7 +375,7 @@ - [#8910](https://github.com/MetaMask/metamask-extension/pull/8910): Handle suggested token resolved elsewhere - [#8913](https://github.com/MetaMask/metamask-extension/pull/8913): Fix Kovan chain ID constant -## 8.0.1 Thu Jul 02 2020 +## [8.0.1] - 2020-07-02 - [#8874](https://github.com/MetaMask/metamask-extension/pull/8874): Fx overflow behaviour of add token list - [#8885](https://github.com/MetaMask/metamask-extension/pull/8885): Show `origin` in connect flow rather than site name @@ -388,7 +388,7 @@ - [#8896](https://github.com/MetaMask/metamask-extension/pull/8896): Include relative time polyfill locale data - [#8898](https://github.com/MetaMask/metamask-extension/pull/8898): Replace percentage opacity value -## 8.0.0 Mon Jun 23 2020 +## [8.0.0] - 2020-07-01 - [#7004](https://github.com/MetaMask/metamask-extension/pull/7004): Add permission system - [#7261](https://github.com/MetaMask/metamask-extension/pull/7261): Search accounts by name @@ -480,7 +480,7 @@ - [#8850](https://github.com/MetaMask/metamask-extension/pull/8850): Stop upper-casing exported private key - [#8631](https://github.com/MetaMask/metamask-extension/pull/8631): Include imported accounts in mobile sync -## 7.7.9 Tue Apr 28 2020 +## [7.7.9] - 2020-05-04 - [#8446](https://github.com/MetaMask/metamask-extension/pull/8446): Fix popup not opening - [#8449](https://github.com/MetaMask/metamask-extension/pull/8449): Skip adding history entry for empty txMeta diffs @@ -498,28 +498,28 @@ - [#8476](https://github.com/MetaMask/metamask-extension/pull/8476): Update eth-contract-metadata - [#8509](https://github.com/MetaMask/metamask-extension/pull/8509): Fix Tohen Typo -## 7.7.8 Wed Mar 11 2020 +## [7.7.8] - 2020-03-13 - [#8176](https://github.com/MetaMask/metamask-extension/pull/8176): Handle and set gas estimation when max mode is clicked - [#8178](https://github.com/MetaMask/metamask-extension/pull/8178): Use specified gas limit when speeding up a transaction -## 7.7.7 Wed Mar 04 2020 +## [7.7.7] - 2020-03-04 - [#8162](https://github.com/MetaMask/metamask-extension/pull/8162): Remove invalid Ledger accounts - [#8163](https://github.com/MetaMask/metamask-extension/pull/8163): Fix account index check -## 7.7.6 Mon Mar 02 2020 +## [7.7.6] - 2020-03-03 - [#8154](https://github.com/MetaMask/metamask-extension/pull/8154): Prevent signing from incorrect Ledger account -## 7.7.5 Fri Feb 14 2020 +## [7.7.5] - 2020-02-18 - [#8053](https://github.com/MetaMask/metamask-extension/pull/8053): Inline the source text not the binary encoding for inpage script - [#8049](https://github.com/MetaMask/metamask-extension/pull/8049): Add warning to watchAsset API when editing a known token - [#8051](https://github.com/MetaMask/metamask-extension/pull/8051): Update Wyre ETH purchase url - [#8059](https://github.com/MetaMask/metamask-extension/pull/8059): Attempt ENS resolution on any valid domain name -## 7.7.4 Wed Jan 29 2020 +## [7.7.4] - 2020-01-31 - [#7918](https://github.com/MetaMask/metamask-extension/pull/7918): Update data on Approve screen after updating custom spend limit - [#7919](https://github.com/MetaMask/metamask-extension/pull/7919): Allow editing max spend limit @@ -527,18 +527,18 @@ - [#7944](https://github.com/MetaMask/metamask-extension/pull/7944): Only resolve ENS on mainnet - [#7954](https://github.com/MetaMask/metamask-extension/pull/7954): Update ENS registry addresses -## 7.7.3 Fri Jan 24 2020 +## [7.7.3] - 2020-01-27 - [#7894](https://github.com/MetaMask/metamask-extension/pull/7894): Update GABA dependency version - [#7901](https://github.com/MetaMask/metamask-extension/pull/7901): Use eth-contract-metadata@1.12.1 - [#7910](https://github.com/MetaMask/metamask-extension/pull/7910): Fixing broken JSON import help link -## 7.7.2 Fri Jan 10 2020 +## [7.7.2] - 2020-01-13 - [#7753](https://github.com/MetaMask/metamask-extension/pull/7753): Fix gas estimate for tokens - [#7473](https://github.com/MetaMask/metamask-extension/pull/7473): Fix transaction order on transaction confirmation screen -## 7.7.1 Wed Dec 04 2019 +## [7.7.1] - 2019-12-09 - [#7488](https://github.com/MetaMask/metamask-extension/pull/7488): Fix text overlap when expanding transaction - [#7491](https://github.com/MetaMask/metamask-extension/pull/7491): Update gas when asset is changed on send screen @@ -553,7 +553,7 @@ - [#7628](https://github.com/MetaMask/metamask-extension/pull/7628): Fix typo that resulted in degrated account menu performance - [#7558](https://github.com/MetaMask/metamask-extension/pull/7558): Use localized messages for NotificationModal buttons -## 7.7.0 Thu Nov 28 2019 [WITHDRAWN] +## [7.7.0] - 2019-12-03 [WITHDRAWN] - [#7004](https://github.com/MetaMask/metamask-extension/pull/7004): Connect distinct accounts per site - [#7480](https://github.com/MetaMask/metamask-extension/pull/7480): Fixed link on root README.md @@ -567,19 +567,19 @@ - [#7558](https://github.com/MetaMask/metamask-extension/pull/7558): Use localized messages for NotificationModal buttons - [#7488](https://github.com/MetaMask/metamask-extension/pull/7488): Fix text overlap when expanding transaction -## 7.6.1 Tue Nov 19 2019 +## [7.6.1] - 2019-11-19 - [#7475](https://github.com/MetaMask/metamask-extension/pull/7475): Add 'Remind Me Later' to the Maker notification - [#7436](https://github.com/MetaMask/metamask-extension/pull/7436): Add additional rpcUrl verification - [#7468](https://github.com/MetaMask/metamask-extension/pull/7468): Show transaction fee units on approve screen -## 7.6.0 Mon Nov 18 2019 +## [7.6.0] - 2019-11-18 - [#7450](https://github.com/MetaMask/metamask-extension/pull/7450): Add migration notification for users with non-zero Sai - [#7461](https://github.com/MetaMask/metamask-extension/pull/7461): Import styles for showing multiple notifications - [#7451](https://github.com/MetaMask/metamask-extension/pull/7451): Add button disabled when password is empty -## 7.5.3 Fri Nov 15 2019 +## [7.5.3] - 2019-11-15 - [#7412](https://github.com/MetaMask/metamask-extension/pull/7412): lock eth-contract-metadata (#7412) - [#7416](https://github.com/MetaMask/metamask-extension/pull/7416): Add eslint import plugin to help detect unresolved paths @@ -594,17 +594,17 @@ - [#7439](https://github.com/MetaMask/metamask-extension/pull/7439): Add metricsEvent to contextTypes (#7439) - [#7419](https://github.com/MetaMask/metamask-extension/pull/7419): Added webRequest.RequestFilter to filter main_frame .eth requests (#7419) -## 7.5.2 Thu Nov 14 2019 +## [7.5.2] - 2019-11-14 - [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound -## 7.5.1 Tuesday Nov 13 2019 +## [7.5.1] - 2019-11-13 - [#7402](https://github.com/MetaMask/metamask-extension/pull/7402): Fix regression for signed types data screens - [#7390](https://github.com/MetaMask/metamask-extension/pull/7390): Update json-rpc-engine - [#7401](https://github.com/MetaMask/metamask-extension/pull/7401): Reject connection request on window close -## 7.5.0 Mon Nov 04 2019 +## [7.5.0] - 2019-11-12 - [#7328](https://github.com/MetaMask/metamask-extension/pull/7328): ignore known transactions on first broadcast and continue with normal flow - [#7327](https://github.com/MetaMask/metamask-extension/pull/7327): eth_getTransactionByHash will now check metamask's local history for pending transactions @@ -623,7 +623,7 @@ - [#7357](https://github.com/MetaMask/metamask-extension/pull/7357): Update to gaba@1.8.0 - [#7335](https://github.com/MetaMask/metamask-extension/pull/7335): Add onbeforeunload and have it call onCancel -## 7.4.0 Tue Oct 29 2019 +## [7.4.0] - 2019-11-04 - [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent` - [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings @@ -637,11 +637,11 @@ - [#7325](https://github.com/MetaMask/metamask-extension/pull/7325): Update eth-json-rpc-filters to fix memory leak - [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning -## 7.3.1 Mon Oct 21 2019 +## [7.3.1] - 2019-10-22 - [#7298](https://github.com/MetaMask/metamask-extension/pull/7298): Turn off full screen vs popup a/b test -## 7.3.0 Fri Sep 27 2019 +## [7.3.0] - 2019-10-21 - [#6972](https://github.com/MetaMask/metamask-extension/pull/6972): 3box integration - [#7168](https://github.com/MetaMask/metamask-extension/pull/7168): Add fixes for German translations @@ -661,21 +661,21 @@ - [#7285](https://github.com/MetaMask/metamask-extension/pull/7285): Lessen the length of ENS validation to 3 - [#7287](https://github.com/MetaMask/metamask-extension/pull/7287): Fix phishing detect script -## 7.2.3 Fri Oct 04 2019 +## [7.2.3] - 2019-10-08 - [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Fix gas limit when sending tx without data to a contract - [#7260](https://github.com/MetaMask/metamask-extension/pull/7260): Do not transate on seed phrases - [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Ensure correct tx category when sending to contracts without tx data -## 7.2.2 Tue Sep 24 2019 +## [7.2.2] - 2019-09-25 - [#7213](https://github.com/MetaMask/metamask-extension/pull/7213): Update minimum Firefox verison to 56.0 -## 7.2.1 Tue Sep 17 2019 +## [7.2.1] - 2019-09-17 - [#7180](https://github.com/MetaMask/metamask-extension/pull/7180): Add `appName` message to each locale -## 7.2.0 Mon Sep 8, 2019 +## [7.2.0] - 2019-09-17 - [#7099](https://github.com/MetaMask/metamask-extension/pull/7099): Update localization from Transifex Brave - [#7137](https://github.com/MetaMask/metamask-extension/pull/7137): Fix validation of empty block explorer url's in custom network form @@ -688,7 +688,7 @@ - [#7161](https://github.com/MetaMask/metamask-extension/pull/7161): Replace `undefined` selectedAddress with `null` - [#7171](https://github.com/MetaMask/metamask-extension/pull/7171): Fix recipient field of approve screen -## 7.1.1 Tue Aug 27 2019 +## [7.1.1] - 2019-09-03 - [#7059](https://github.com/MetaMask/metamask-extension/pull/7059): Remove blockscale, replace with ethgasstation - [#7037](https://github.com/MetaMask/metamask-extension/pull/7037): Remove Babel 6 from internal dependencies @@ -700,7 +700,7 @@ - [#6878](https://github.com/MetaMask/metamask-extension/pull/6878): Persian translation - [#7012](https://github.com/MetaMask/metamask-extension/pull/7012): Added missed phrases to RU locale -## 7.1.0 Fri Aug 16 2019 +## [7.1.0] - 2019-08-26 - [#7035](https://github.com/MetaMask/metamask-extension/pull/7035): Filter non-ERC-20 assets during mobile sync (#7035) - [#7021](https://github.com/MetaMask/metamask-extension/pull/7021): Using translated string for end of flow messaging (#7021) @@ -714,11 +714,11 @@ - [#7046](https://github.com/MetaMask/metamask-extension/pull/7046): Update Italian translation (#7046) - [#7047](https://github.com/MetaMask/metamask-extension/pull/7047): Add warning about reload on network change -## 7.0.1 Thu Aug 08 2019 +## [7.0.1] - 2019-08-08 - [#6975](https://github.com/MetaMask/metamask-extension/pull/6975): Ensure seed phrase backup notification only shows up for new users -## 7.0.0 Fri Aug 02 2019 +## [7.0.0] - 2019-08-07 - [#6828](https://github.com/MetaMask/metamask-extension/pull/6828): Capitalized speed up label to match rest of UI - [#6874](https://github.com/MetaMask/metamask-extension/pull/6928): Allows skipping of seed phrase challenge during onboarding, and completing it at a later time @@ -728,11 +728,11 @@ - [#6928](https://github.com/MetaMask/metamask-extension/pull/6928): Disable Copy Tx ID and block explorer link for transactions without hash - [#6967](https://github.com/MetaMask/metamask-extension/pull/6967): Fix mobile sync -## 6.7.3 Thu Jul 18 2019 +## [6.7.3] - 2019-07-19 - [#6888](https://github.com/MetaMask/metamask-extension/pull/6888): Fix bug with resubmitting unsigned transactions. -## 6.7.2 Mon Jul 01 2019 +## [6.7.2] - 2019-07-03 - [#6713](https://github.com/MetaMask/metamask-extension/pull/6713): \* Normalize and Validate txParams in TransactionStateManager.addTx too - [#6759](https://github.com/MetaMask/metamask-extension/pull/6759): Update to Node.js v10 @@ -747,11 +747,11 @@ - [#6648](https://github.com/MetaMask/metamask-extension/pull/6648): Add loading view to notification.html - [#6731](https://github.com/MetaMask/metamask-extension/pull/6731): Add brave as a platform type for MetaMask -## 6.7.1 Fri Jun 28 2019 +## [6.7.1] - 2019-07-28 - [#6764](https://github.com/MetaMask/metamask-extension/pull/6764): Fix display of token amount on confirm transaction screen -## 6.7.0 Tue Jun 18 2019 +## [6.7.0] - 2019-07-26 - [#6623](https://github.com/MetaMask/metamask-extension/pull/6623): Improve contract method data fetching (#6623) - [#6551](https://github.com/MetaMask/metamask-extension/pull/6551): Adds 4byte registry fallback to getMethodData() (#6435) @@ -762,39 +762,39 @@ - [#6700](https://github.com/MetaMask/metamask-extension/pull/6700): Fix styles on 'import account' page, update help link - [#6775](https://github.com/MetaMask/metamask-extension/pull/6775): Started adding visual documentation of MetaMask plugin components with the account menu component first -## 6.6.2 Fri Jun 07 2019 +## [6.6.2] - 2019-07-17 - [#6690](https://github.com/MetaMask/metamask-extension/pull/6690): Update dependencies, re-enable npm audit CI job - [#6700](https://github.com/MetaMask/metamask-extension/pull/6700): Fix styles on 'import account' page, update help link -## 6.6.1 Thu Jun 06 2019 +## [6.6.1] - 2019-06-06 - [#6691](https://github.com/MetaMask/metamask-extension/pull/6691): Revert "Improve ENS Address Input" to fix bugs on input field on non-main networks. -## 6.6.0 Mon Jun 03 2019 +## [6.6.0] - 2019-06-04 - [#6659](https://github.com/MetaMask/metamask-extension/pull/6659): Enable Ledger hardware wallet support on Firefox - [#6671](https://github.com/MetaMask/metamask-extension/pull/6671): bugfix: reject enable promise on user rejection - [#6625](https://github.com/MetaMask/metamask-extension/pull/6625): Ensures that transactions cannot be confirmed if gas limit is below 21000. - [#6633](https://github.com/MetaMask/metamask-extension/pull/6633): Fix grammatical error in i18n endOfFlowMessage6 -## 6.5.3 Thu May 16 2019 +## [6.5.3] - 2019-05-16 - [#6619](https://github.com/MetaMask/metamask-extension/pull/6619): bugfix: show extension window if locked regardless of approval - [#6388](https://github.com/MetaMask/metamask-extension/pull/6388): Transactions/pending - check nonce against the network and mark as dropped if not included in a block - [#6606](https://github.com/MetaMask/metamask-extension/pull/6606): Improve ENS Address Input - [#6615](https://github.com/MetaMask/metamask-extension/pull/6615): Adds e2e test for removing imported accounts. -## 6.5.2 Wed May 15 2019 +## [6.5.2] - 2019-05-15 - [#6613](https://github.com/MetaMask/metamask-extension/pull/6613): Hardware Wallet Fix -## 6.5.1 Tue May 14 2019 +## [6.5.1] - 2019-05-14 - Fix bug where approve method would show a warning. #6602 - [#6593](https://github.com/MetaMask/metamask-extension/pull/6593): Fix wording of autoLogoutTimeLimitDescription -## 6.5.0 Fri May 10 2019 +## [6.5.0] - 2019-05-13 - [#6568](https://github.com/MetaMask/metamask-extension/pull/6568): feature: integrate gaba/PhishingController - [#6490](https://github.com/MetaMask/metamask-extension/pull/6490): Redesign custom RPC form @@ -805,11 +805,11 @@ - [#6502](https://github.com/MetaMask/metamask-extension/pull/6502): Add subheader to all settings subviews - [#6501](https://github.com/MetaMask/metamask-extension/pull/6501): Improve confirm screen loading performance by fixing home screen rendering bug -## 6.4.1 Fri Apr 26 2019 +## [6.4.1] - 2019-04-26 - [#6521](https://github.com/MetaMask/metamask-extension/pull/6521): Revert "Adds 4byte registry fallback to getMethodData()" to fix stalling bug. -## 6.4.0 Wed Apr 17 2019 +## [6.4.0] - 2019-04-18 - [#6445](https://github.com/MetaMask/metamask-extension/pull/6445): \* Move send to pages/ - [#6470](https://github.com/MetaMask/metamask-extension/pull/6470): update publishing.md with dev diagram @@ -835,19 +835,19 @@ - [#6389](https://github.com/MetaMask/metamask-extension/pull/6389): Fix display of gas chart on Ethereum networks - [#6382](https://github.com/MetaMask/metamask-extension/pull/6382): Remove NoticeController -## 6.3.2 Mon Apr 8 2019 +## [6.3.2] - 2019-04-08 - [#6389](https://github.com/MetaMask/metamask-extension/pull/6389): Fix display of gas chart on ethereum networks - [#6395](https://github.com/MetaMask/metamask-extension/pull/6395): Fixes for signing methods for ledger and trezor devices - [#6397](https://github.com/MetaMask/metamask-extension/pull/6397): Fix Wyre link -## 6.3.1 Fri Mar 26 2019 +## [6.3.1] - 2019-03-29 - [#6353](https://github.com/MetaMask/metamask-extension/pull/6353): Open restore vault in full screen when clicked from popup - [#6372](https://github.com/MetaMask/metamask-extension/pull/6372): Prevents duplicates of account addresses from showing in send screen "To" dropdown - [#6374](https://github.com/MetaMask/metamask-extension/pull/6374): Ensures users are placed on correct confirm screens even when registry service fails -## 6.3.0 Mon Mar 25 2019 +## [6.3.0] - 2019-03-26 - [#6300](https://github.com/MetaMask/metamask-extension/pull/6300): Gas chart hidden on custom networks - [#6301](https://github.com/MetaMask/metamask-extension/pull/6301): Fix gas fee in the submitted step of the transaction details activity log @@ -862,23 +862,23 @@ - [#6341](https://github.com/MetaMask/metamask-extension/pull/6341): Disable transaction "Cancel" button when balance is insufficient - [#6347](https://github.com/MetaMask/metamask-extension/pull/6347): Enable privacy mode by default for first time users -## 6.2.2 Tue Mar 12 2019 +## [6.2.2] - 2019-03-12 - [#6271](https://github.com/MetaMask/metamask-extension/pull/6271): Centre all notification popups - [#6268](https://github.com/MetaMask/metamask-extension/pull/6268): Improve Korean translations - [#6279](https://github.com/MetaMask/metamask-extension/pull/6279): Nonmultiple notifications for batch txs - [#6280](https://github.com/MetaMask/metamask-extension/pull/6280): No longer check network when validating checksum addresses -## 6.2.1 Wed Mar 06 2019 +## [6.2.1] - 2019-03-11 -## 6.2.0 Tue Mar 05 2019 +## [6.2.0] - 2019-03-05 - [#6192](https://github.com/MetaMask/metamask-extension/pull/6192): Improves design and UX of onboarding flow - [#6195](https://github.com/MetaMask/metamask-extension/pull/6195): Fixes gas estimation when sending to contracts - [#6223](https://github.com/MetaMask/metamask-extension/pull/6223): Fixes display of notification windows when metamask is active in a tab - [#6171](https://github.com/MetaMask/metamask-extension/pull/6171): Adds MetaMetrics usage analytics system -## 6.1.0 Tue Feb 19 2019 +## [6.1.0] - 2019-02-20 - [#6182](https://github.com/MetaMask/metamask-extension/pull/6182): Change "Token Address" to "Token Contract Address" - [#6177](https://github.com/MetaMask/metamask-extension/pull/6177): Fixes #6176 @@ -887,14 +887,14 @@ - [#6147](https://github.com/MetaMask/metamask-extension/pull/6147): Add button to force edit token symbol when adding custom token - [#6124](https://github.com/MetaMask/metamask-extension/pull/6124): recent-blocks - dont listen for block when on infura providers -[#5973] (https://github.com/MetaMask/metamask-extension/pull/5973): Fix incorrectly showing checksums on non-ETH blockchains (issue 5838) -## 6.0.1 Tue Feb 12 2019 +## [6.0.1] - 2019-02-12 - [#6139](https://github.com/MetaMask/metamask-extension/pull/6139) Fix advanced gas controls on the confirm screen - [#6134](https://github.com/MetaMask/metamask-extension/pull/6134) Trim whitespace from seed phrase during import - [#6119](https://github.com/MetaMask/metamask-extension/pull/6119) Update Italian translation - [#6125](https://github.com/MetaMask/metamask-extension/pull/6125) Improved Traditional Chinese translation -## 6.0.0 Thu Feb 07 2019 +## [6.0.0] - 2019-02-11 - [#6082](https://github.com/MetaMask/metamask-extension/pull/6082): Migrate all users to the new UI - [#6114](https://github.com/MetaMask/metamask-extension/pull/6114): Add setting for inputting gas price with a text field for advanced users. @@ -905,22 +905,22 @@ - [#6120](https://github.com/MetaMask/metamask-extension/pull/6120): Add class to sign footer button - [#6116](https://github.com/MetaMask/metamask-extension/pull/6116): Fix locale codes contains underscore never being preferred -## 5.3.5 Mon Feb 4 2019 +## [5.3.5] - 2019-02-04 - [#6084](https://github.com/MetaMask/metamask-extension/pull/6087): Privacy mode fixes -## 5.3.4 Thu Jan 31 2019 +## [5.3.4] - 2019-01-31 - [#6079](https://github.com/MetaMask/metamask-extension/pull/6079): fix - migration 30 -## 5.3.3 Wed Jan 30 2019 +## [5.3.3] - 2019-01-30 - [#6006](https://github.com/MetaMask/metamask-extension/pull/6006): Update privacy notice - [#6072](https://github.com/MetaMask/metamask-extension/pull/6072): Improved Spanish translations - [#5854](https://github.com/MetaMask/metamask-extension/pull/5854): Add visual indicator when displaying a cached balance. - [#6044](https://github.com/MetaMask/metamask-extension/pull/6044): Fix bug that interferred with using multiple custom networks. -## 5.3.2 Mon Jan 28 2019 +## [5.3.2] - 2019-01-28 - [#6021](https://github.com/MetaMask/metamask-extension/pull/6021): Order shapeshift transactions by time within the transactions list - [#6052](https://github.com/MetaMask/metamask-extension/pull/6052): Add and use cached method signatures to reduce provider requests @@ -929,7 +929,7 @@ - [#6029](https://github.com/MetaMask/metamask-extension/pull/6029): Fix grammar error in Current Conversion - [#6024](https://github.com/MetaMask/metamask-extension/pull/6024): Disable account dropdown on signing screens -## 5.3.1 Wed Jan 16 2019 +## [5.3.1] - 2019-01-16 - [#5966](https://github.com/MetaMask/metamask-extension/pull/5966): Update Slovenian translation - [#6005](https://github.com/MetaMask/metamask-extension/pull/6005): Set auto conversion off for token/eth conversion @@ -942,7 +942,7 @@ - [#5992](https://github.com/MetaMask/metamask-extension/pull/5992): Add scrolling button to account list - [#5989](https://github.com/MetaMask/metamask-extension/pull/5989): fix typo in phishing.html title -## 5.3.0 Wed Jan 02 2019 +## [5.3.0] - 2019-01-02 - [#5978](https://github.com/MetaMask/metamask-extension/pull/5978): Fix etherscan links on notifications - [#5980](https://github.com/MetaMask/metamask-extension/pull/5980): Fix drizzle tests @@ -951,31 +951,31 @@ - [#5924](https://github.com/MetaMask/metamask-extension/pull/5924): transactions - throw an error if a transaction is generated while the network is loading - [#5893](https://github.com/MetaMask/metamask-extension/pull/5893): Add loading network screen -## 5.2.2 Wed Dec 12 2018 +## [5.2.2] - 2018-12-13 - [#5925](https://github.com/MetaMask/metamask-extension/pull/5925): Fix speed up button not showing for transactions with the lowest nonce - [#5923](https://github.com/MetaMask/metamask-extension/pull/5923): Update the Phishing Warning notice text to not use inline URLs - [#5919](https://github.com/MetaMask/metamask-extension/pull/5919): Fix some styling and translations in the gas customization modal -## 5.2.1 Wed Dec 12 2018 +## [5.2.1] - 2018-12-12 - [#5917] bugfix: Ensures that advanced tab gas limit reflects tx gas limit -## 5.2.0 Mon Dec 11 2018 +## [5.2.0] - 2018-12-11 - [#5704] Implements new gas customization features for sending, confirming and speeding up transactions - [#5886] Groups transactions - speed up, cancel and original - by nonce in the transaction history list - [#5892] bugfix: eliminates infinite spinner issues caused by switching quickly from a loading network that ultimately fails to resolve - [$5902] bugfix: provider crashes caused caching issues in `json-rpc-engine`. Fixed in (https://github.com/MetaMask/json-rpc-engine/commit/6de511afbd03ccef4550ea43ff4010b7d7a84039) -## 5.1.0 Mon Dec 03 2018 +## [5.1.0] - 2018-12-03 - [#5860](https://github.com/MetaMask/metamask-extension/pull/5860): Fixed an infinite spinner bug. - [#5875](https://github.com/MetaMask/metamask-extension/pull/5875): Update phishing warning copy - [#5863](https://github.com/MetaMask/metamask-extension/pull/5863): bugfix: normalize contract addresss when fetching exchange rates - [#5843](https://github.com/MetaMask/metamask-extension/pull/5843): Use selector for state.metamask.accounts in all cases. -## 5.0.4 Thu Nov 29 2018 +## [5.0.4] - 2018-11-29 - [#5878](https://github.com/MetaMask/metamask-extension/pull/5878): Formats 32-length byte strings passed to personal_sign as hex, rather than UTF8. - [#5840](https://github.com/MetaMask/metamask-extension/pull/5840): transactions/tx-gas-utils - add the acctual response for eth_getCode for NO_CONTRACT_ERROR's && add a debug object to simulationFailed @@ -998,30 +998,30 @@ - [#5334](https://github.com/MetaMask/metamask-extension/pull/5334): Default to the new UI for first time users - [#5791](https://github.com/MetaMask/metamask-extension/pull/5791): Bump eth-ledger-bridge-keyring -## 5.0.3 Mon Nov 19 2018 +## [5.0.3] - 2018-11-20 - [#5547](https://github.com/MetaMask/metamask-extension/pull/5547): Bundle some ui dependencies separately to limit the build size of ui.js - Resubmit approved transactions on new block, to fix bug where an error can stick transactions in this state. - Fixed a bug that could cause an error when sending the max number of tokens. -## 5.0.2 Friday November 9 2018 +## [5.0.2] - 2018-11-10 - Fixed bug that caused accounts to update slowly to sites. #5717 - Fixed bug that could lead to some sites crashing. #5709 -## 5.0.1 Wednesday November 7 2018 +## [5.0.1] - 2018-11-07 - Fixed bug in privacy mode that made it not work correctly on Firefox. -## 5.0.0 Tuesday November 6 2018 +## [5.0.0] - 2018-11-06 - Implements EIP 1102 as a user-activated "Privacy Mode". -## 4.17.1 Saturday November 3 2018 +## [4.17.1] - 2018-11-03 - Revert chain ID lookup change which introduced a bug which caused problems when connecting to mainnet via Infura's RESTful API. -## 4.17.0 Thursday November 1 2018 +## [4.17.0] - 2018-11-01 - Fix bug where data lookups like balances would get stale data (stopped block-tracker bug) - Transaction Details now show entry for onchain failure @@ -1033,7 +1033,7 @@ - Attempt chain ID lookup via `eth_chainId` before `net_version` - Fix account display width for large currency values -## 4.16.0 Wednesday October 17 2018 +## [4.16.0] - 2018-10-17 - Feature: Add toggle for primary currency (eth/fiat) - Feature: add tooltip for view etherscan tx @@ -1045,34 +1045,34 @@ - Bug Fix: Fix document extension check when injecting web3 - Bug Fix: Fix some support links -## 4.15.0 Thursday October 11 2018 +## [4.15.0] - 2018-10-11 - A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.14.0` is found to have bugs. -## 4.14.0 Thursday October 11 2018 +## [4.14.0] - 2018-10-11 - Update transaction statuses when switching networks. - [#5470](https://github.com/MetaMask/metamask-extension/pull/5470) 100% coverage in French locale, fixed the procedure to verify proposed locale. - Added rudimentary support for the subscription API to support web3 1.0 and Truffle's Drizzle. - [#5502](https://github.com/MetaMask/metamask-extension/pull/5502) Update Italian translation. -## 4.13.0 +## [4.13.0] - 2018-10-04 - A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.12.0` is found to have bugs. -## 4.12.0 Thursday September 27 2018 +## [4.12.0] - 2018-09-27 - Reintroduces changes from 4.10.0 -## 4.11.1 Tuesday September 25 2018 +## [4.11.1] - 2018-09-25 - Adds Ledger support. -## 4.11.0 Monday September 24 2018 +## [4.11.0] - 2018-09-24 - Identical to 4.9.3. A rollback version to give time to fix bugs in the 4.10.x branch. -## 4.10.0 Mon Sep 17 2018 +## [4.10.0] - 2018-09-18 - [#4803](https://github.com/MetaMask/metamask-extension/pull/4803): Implement EIP-712: Sign typed data, but continue to support v1. - [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. @@ -1086,17 +1086,17 @@ - [#5189](https://github.com/MetaMask/metamask-extension/pull/5189): Fix bug where Ropsten loading message is shown when connecting to Kovan. - [#5256](https://github.com/MetaMask/metamask-extension/pull/5256): Add mock EIP-1102 support -## 4.9.3 Wed Aug 15 2018 +## [4.9.3] - 2018-08-16 - [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses. - [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link. - [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly. -## 4.9.2 Mon Aug 09 2018 +## [4.9.2] - 2018-08-10 - [#5020](https://github.com/MetaMask/metamask-extension/pull/5020): Fix bug in migration #28 ( moving tokens to specific accounts ) -## 4.9.1 Mon Aug 09 2018 +## [4.9.1] - 2018-08-09 - [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network. - [#4989](https://github.com/MetaMask/metamask-extension/pull/4989): Continue to use original signedTypedData. @@ -1104,7 +1104,7 @@ - [#5000](https://github.com/MetaMask/metamask-extension/pull/5000): Show error while allowing confirmation of tx where simulation fails. - [#4995](https://github.com/MetaMask/metamask-extension/pull/4995): Shows retry button on dApp initialized transactions. -## 4.9.0 Mon Aug 07 2018 +## [4.9.0] - 2018-08-07 - [#4926](https://github.com/MetaMask/metamask-extension/pull/4926): Show retry button on the latest tx of the earliest nonce. - [#4888](https://github.com/MetaMask/metamask-extension/pull/4888): Suggest using the new user interface. @@ -1120,7 +1120,7 @@ - [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case. - [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. -## 4.8.0 Thur Jun 14 2018 +## [4.8.0] - 2018-06-18 - [#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. @@ -1130,30 +1130,30 @@ - [#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 +## [4.7.4] - 2018-06-05 - Add diagnostic reporting for users with multiple HD keyrings - Throw explicit error when selected account is unset -## 4.7.3 Mon Jun 04 2018 +## [4.7.3] - 2018-06-04 - Hide token now uses new modal - Indicate the current selected account on the popup account view - Reduce height of notice container in onboarding - Fixes issue where old nicknames were kept around causing errors -## 4.7.2 Sun Jun 03 2018 +## [4.7.2] - 2018-06-03 - Fix bug preventing users from logging in. Internally accounts and identities were out of sync. - Fix support links to point to new support system (Zendesk) - Fix bug in migration #26 ( moving account nicknames to preferences ) - Clears account nicknames on restore from seedPhrase -## 4.7.1 Fri Jun 01 2018 +## [4.7.1] - 2018-06-01 - Fix bug where errors were not returned to Dapps. -## 4.7.0 Wed May 30 2018 +## [4.7.0] - 2018-05-30 - Fix Brave support - Adds error messages when passwords don't match in onboarding flow. @@ -1168,7 +1168,7 @@ - Styling improvements to labels in first time flow and signature request headers. - Allow other extensions to make access our ethereum provider API ([#3997](https://github.com/MetaMask/metamask-extension/pull/3997)) -## 4.6.1 Mon Apr 30 2018 +## [4.6.1] - 2018-04-30 - Fix bug where sending a transaction resulted in an infinite spinner - Allow transactions with a 0 gwei gas price @@ -1176,7 +1176,7 @@ - Fix ShapeShift forms (new + old ui) - Fix sourcemaps -## 4.6.0 Thu Apr 26 2018 +## [4.6.0] - 2018-04-26 - Correctly format currency conversion for locally selected preferred currency. - Improved performance of 3D fox logo. @@ -1186,7 +1186,7 @@ - Allow transactions with a 0 gwei gas price - Made provider RPC errors contain useful messages -## 4.5.5 Fri Apr 06 2018 +## [4.5.5] - 2018-04-06 - Graceful handling of unknown keys in txParams - Fixes buggy handling of historical transactions with unknown keys in txParams @@ -1194,35 +1194,35 @@ - Fix Download State Logs button [#3791](https://github.com/MetaMask/metamask-extension/issues/3791) - Enhanced migration error handling + reporting -## 4.5.4 (aborted) Thu Apr 05 2018 +## [4.5.4] - 2018-04-05 [WITHDRAWN] - Graceful handling of unknown keys in txParams - Fix link for 'Learn More' in the Add Token Screen to open to a new tab. - Fix Download State Logs button [#3791](https://github.com/MetaMask/metamask-extension/issues/3791) - Fix migration error reporting -## 4.5.3 Wed Apr 04 2018 +## [4.5.3] - 2018-04-04 - Fix bug where checksum address are messing with balance issue [#3843](https://github.com/MetaMask/metamask-extension/issues/3843) - new ui: fix the confirm transaction screen -## 4.5.2 Wed Apr 04 2018 +## [4.5.2] - 2018-04-04 - Fix overly strict validation where transactions were rejected with hex encoded "chainId" -## 4.5.1 Tue Apr 03 2018 +## [4.5.1] - 2018-04-03 - Fix default network (should be mainnet not Rinkeby) - Fix Sentry automated error reporting endpoint -## 4.5.0 Mon Apr 02 2018 +## [4.5.0] - 2018-04-02 - (beta ui) Internationalization: Select your preferred language in the settings screen - Internationalization: various locale improvements - Fix bug where the "Reset account" feature would not clear the network cache. - Increase maximum gas limit, to allow very gas heavy transactions, since block gas limits have been stable. -## 4.4.0 Mon Mar 26 2018 +## [4.4.0] - 2018-03-27 - Internationalization: Taiwanese, Thai, Slovenian - Fixes bug where MetaMask would not open once its storage grew too large. @@ -1233,7 +1233,7 @@ - Popup extension in new-ui uses new on-boarding designs - Buy ether step of new-ui on-boarding uses new buy ether modal designs -## 4.3.0 Wed Mar 21 2018 +## [4.3.0] - 2018-03-21 - (beta) Add internationalization support! Includes translations for 13 (!!) new languages: French, Spanish, Italian, German, Dutch, Portuguese, Japanese, Korean, Vietnamese, Mandarin, Hindi, Tagalog, and Russian! Select "Try Beta" in the menu to take them for a spin. Read more about the community effort [here](https://medium.com/gitcoin/metamask-internationalizes-via-gitcoin-bf1390c0301c) - No longer uses nonces specified by the dapp @@ -1249,7 +1249,7 @@ - Hide network dropdown before account is initialized - Fix bug that could prevent MetaMask from saving the latest vault. -## 4.2.0 Tue Mar 06 2018 +## [4.2.0] - 2018-03-06 - Replace "Loose" wording to "Imported". - Replace "Unlock" wording with "Log In". @@ -1259,17 +1259,17 @@ - NewUI shapeshift form can select all coins (not just BTC) - Add most of Microsoft Edge support. -## 4.1.3 2018-2-28 +## [4.1.3] - 2018-03-02 - Ensure MetaMask's inpage provider is named MetamaskInpageProvider to keep some sites from breaking. - Add retry transaction button back into classic ui. - Add network dropdown styles to support long custom RPC urls -## 4.1.2 2018-2-28 +## [4.1.2] - 2018-02-28 - Actually includes all the fixes mentioned in 4.1.1 (sorry) -## 4.1.1 2018-2-28 +## [4.1.1] - 2018-02-28 - Fix "Add Token" screen referencing missing token logo urls - Prevent user from switching network during signature request @@ -1277,50 +1277,50 @@ - Fix cancel button on "Buy Eth" screen - Improve new-ui onboarding flow style -## 4.1.0 2018-2-27 +## [4.1.0] - 2018-02-27 - Report failed txs to Sentry with more specific message - Fix internal feature flags being sometimes undefined - Standardized license to MIT -## 4.0.0 2018-2-22 +## [4.0.0] - 2018-02-22 - Introduce new MetaMask user interface. -## 3.14.2 2018-2-15 +## [3.14.2] - 2018-02-27 - Fix bug where log subscriptions would break when switching network. - Fix bug where storage values were cached across blocks. - Add MetaMask light client [testing container](https://github.com/MetaMask/mesh-testing) -## 3.14.1 2018-2-1 +## [3.14.1] - 2018-02-01 - Further fix scrolling for Firefox. -## 3.14.0 2018-2-1 +## [3.14.0] - 2018-02-01 - Removed unneeded data from storage - Add a "reset account" feature to Settings - Add warning for importing some kinds of files. - Scrollable Setting view for Firefox. -## 3.13.8 2018-1-29 +## [3.13.8] - 2018-01-29 - Fix provider for Kovan network. - Bump limit for EventEmitter listeners before warning. - Display Error when empty string is entered as a token address. -## 3.13.7 2018-1-22 +## [3.13.7] - 2018-01-22 - Add ability to bypass gas estimation loading indicator. - Forward failed transactions to Sentry error reporting service - Re-add changes from 3.13.5 -## 3.13.6 2017-1-18 +## [3.13.6] - 2017-01-18 - Roll back changes to 3.13.4 to fix some issues with the new Infura REST provider. -## 3.13.5 2018-1-16 +## [3.13.5] - 2018-01-16 - Estimating gas limit for simple ether sends now faster & cheaper, by avoiding VM usage on recipients with no code. - Add an extra px to address for Firefox clipping. @@ -1329,7 +1329,7 @@ - Fix bug that prevented eth_signTypedData from signing bytes. - Further improve gas price estimation. -## 3.13.4 2018-1-9 +## [3.13.4] - 2018-01-09 - Remove recipient field if application initializes a tx with an empty string, or 0x, and tx data. Throw an error with the same condition, but without tx data. - Improve gas price suggestion to be closer to the lowest that will be accepted. @@ -1339,95 +1339,95 @@ - Fix rounding error when specifying an ether amount that has too much precision. - Fix bug where incorrectly inputting seed phrase would prevent any future attempts from succeeding. -## 3.13.3 2017-12-14 +## [3.13.3] - 2017-12-14 - Show tokens that are held that have no balance. - Reduce load on Infura by using a new block polling endpoint. -## 3.13.2 2017-12-9 +## [3.13.2] - 2017-12-09 - Reduce new block polling interval to 8000 ms, to ease server load. -## 3.13.1 2017-12-7 +## [3.13.1] - 2017-12-07 - Allow Dapps to specify a transaction nonce, allowing dapps to propose resubmit and force-cancel transactions. -## 3.13.0 2017-12-7 +## [3.13.0] - 2017-12-07 - Allow resubmitting transactions that are taking long to complete. -## 3.12.1 2017-11-29 +## [3.12.1] - 2017-11-29 - Fix bug where a user could be shown two different seed phrases. - Detect when multiple web3 extensions are active, and provide useful error. - Adds notice about seed phrase backup. -## 3.12.0 2017-10-25 +## [3.12.0] - 2017-10-26 - Add support for alternative ENS TLDs (Ethereum Name Service Top-Level Domains). - Lower minimum gas price to 0.1 GWEI. - Remove web3 injection message from production (thanks to @ChainsawBaby) - Add additional debugging info to our state logs, specifically OS version and browser version. -## 3.11.2 2017-10-21 +## [3.11.2] - 2017-10-21 - Fix bug where reject button would sometimes not work. - Fixed bug where sometimes MetaMask's connection to a page would be unreliable. -## 3.11.1 2017-10-20 +## [3.11.1] - 2017-10-20 - Fix bug where log filters were not populated correctly - Fix bug where web3 API was sometimes injected after the page loaded. - Fix bug where first account was sometimes not selected correctly after creating or restoring a vault. - Fix bug where imported accounts could not use new eth_signTypedData method. -## 3.11.0 2017-10-11 +## [3.11.0] - 2017-10-11 - Add support for new eth_signTypedData method per EIP 712. - Fix bug where some transactions would be shown as pending forever, even after successfully mined. - Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined. - Fix link to support article on token addresses. -## 3.10.9 2017-10-5 +## [3.10.9] - 2017-10-05 - Only rebrodcast transactions for a day not a days worth of blocks - Remove Slack link from info page, since it is a big phishing target. - Stop computing balance based on pending transactions, to avoid edge case where users are unable to send transactions. -## 3.10.8 2017-9-28 +## [3.10.8] - 2017-09-30 - Fixed usage of new currency fetching API. -## 3.10.7 2017-9-28 +## [3.10.7] - 2017-09-29 - Fixed bug where sometimes the current account was not correctly set and exposed to web apps. - Added AUD, HKD, SGD, IDR, PHP to currency conversion list -## 3.10.6 2017-9-27 +## [3.10.6] - 2017-09-27 - Fix bug where newly created accounts were not selected. - Fix bug where selected account was not persisted between lockings. -## 3.10.5 2017-9-27 +## [3.10.5] - 2017-09-27 - Fix block gas limit estimation. -## 3.10.4 2017-9-27 +## [3.10.4] - 2017-09-27 - Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) - Fix memory leak warning. - Fix bug where new event filters would not include historical events. -## 3.10.3 2017-9-21 +## [3.10.3] - 2017-09-21 - Fix bug where metamask-dapp connections are lost on rpc error - Fix bug that would sometimes display transactions as failed that could be successfully mined. -## 3.10.2 2017-9-18 +## [3.10.2] - 2017-09-19 rollback to 3.10.0 due to bug -## 3.10.1 2017-9-18 +## [3.10.1] - 2017-09-18 - Add ability to export private keys as a file. - Add ability to export seed words as a file. @@ -1439,7 +1439,7 @@ rollback to 3.10.0 due to bug - Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher - Sort currencies by currency name (thanks to strelok1: https://github.com/strelok1). -## 3.10.0 2017-9-11 +## [3.10.0] - 2017-09-11 - Readded loose keyring label back into the account list. - Remove cryptonator from chrome permissions. @@ -1447,11 +1447,11 @@ rollback to 3.10.0 due to bug - Add validation preventing users from inputting their own addresses as token tracking addresses. - Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94) -## 3.9.13 2017-9-8 +## [3.9.13] - 2017-09-08 - Changed the way we initialize the inpage provider to fix a bug affecting some developers. -## 3.9.12 2017-9-6 +## [3.9.12] - 2017-09-06 - Fix bug that prevented Web3 1.0 compatibility - Make eth_sign deprecation warning less noisy @@ -1463,59 +1463,59 @@ rollback to 3.10.0 due to bug - Update Support center link to new one with HTTPS. - Make web3 deprecation notice more useful by linking to a descriptive article. -## 3.9.11 2017-8-24 +## [3.9.11] - 2017-08-24 - Fix nonce calculation bug that would sometimes generate very wrong nonces. - Give up resubmitting a transaction after 3500 blocks. -## 3.9.10 2017-8-23 +## [3.9.10] - 2017-08-23 - Improve nonce calculation, to prevent bug where people are unable to send transactions reliably. - Remove link to eth-tx-viz from identicons in tx history. -## 3.9.9 2017-8-18 +## [3.9.9] - 2017-08-18 - Fix bug where some transaction submission errors would show an empty screen. - Fix bug that could mis-render token balances when very small. - Fix formatting of eth_sign "Sign Message" view. - Add deprecation warning to eth_sign "Sign Message" view. -## 3.9.8 2017-8-16 +## [3.9.8] - 2017-08-16 - Reenable token list. - Remove default tokens. -## 3.9.7 2017-8-15 +## [3.9.7] - 2017-08-15 - hotfix - disable token list - Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0 -## 3.9.6 2017-8-09 +## [3.9.6] - 2017-08-10 - Replace account screen with an account drop-down menu. - Replace account buttons with a new account-specific drop-down menu. -## 3.9.5 2017-8-04 +## [3.9.5] - 2017-08-04 - Improved phishing detection configuration update rate -## 3.9.4 2017-8-03 +## [3.9.4] - 2017-08-04 - Fixed bug that prevented transactions from being rejected. -## 3.9.3 2017-8-03 +## [3.9.3] - 2017-08-03 - Add support for EGO ujo token - Continuously update blacklist for known phishing sites in background. - Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. -## 3.9.2 2017-7-26 +## [3.9.2] - 2017-07-26 - Fix bugs that could sometimes result in failed transactions after switching networks. - Include stack traces in txMeta's to better understand the life cycle of transactions - Enhance blacklister functionality to include levenshtein logic. (credit to @sogoiii and @409H for their help!) -## 3.9.1 2017-7-19 +## [3.9.1] - 2017-07-19 - No longer automatically request 1 ropsten ether for the first account in a new vault. - Now redirects from known malicious sites faster. @@ -1524,37 +1524,37 @@ rollback to 3.10.0 due to bug - Fixed bug in nonce tracker where an incorrect nonce would be calculated. - Lowered minimum gas price to 1 Gwei. -## 3.9.0 2017-7-12 +## [3.9.0] - 2017-07-12 - Now detects and blocks known phishing sites. -## 3.8.6 2017-7-11 +## [3.8.6] - 2017-07-11 - Make transaction resubmission more resilient. - No longer validate nonce client-side in retry loop. - Fix bug where insufficient balance error was sometimes shown on successful transactions. -## 3.8.5 2017-7-7 +## [3.8.5] - 2017-07-08 - Fix transaction resubmit logic to fail slightly less eagerly. -## 3.8.4 2017-7-7 +## [3.8.4] - 2017-07-07 - Improve transaction resubmit logic to fail more eagerly when a user would expect it to. -## 3.8.3 2017-7-6 +## [3.8.3] - 2017-07-06 - Re-enable default token list. - Add origin header to dapp-bound requests to allow providers to throttle sites. - Fix bug that could sometimes resubmit a transaction that had been stalled due to low balance after balance was restored. -## 3.8.2 2017-7-3 +## [3.8.2] - 2017-07-03 - No longer show network loading indication on config screen, to allow selecting custom RPCs. - Visually indicate that network spinner is a menu. - Indicate what network is being searched for when disconnected. -## 3.8.1 2017-6-30 +## [3.8.1] - 2017-06-30 - Temporarily disabled loading popular tokens by default to improve performance. - Remove SEND token button until a better token sending form can be built, due to some precision issues. @@ -1562,7 +1562,7 @@ rollback to 3.10.0 due to bug - Cache token symbol and precisions to reduce network load. - Transpile some newer JavaScript, restores compatibility with some older browsers. -## 3.8.0 2017-6-28 +## [3.8.0] - 2017-06-28 - No longer stop rebroadcasting transactions - Add list of popular tokens held to the account detail view. @@ -1577,7 +1577,7 @@ rollback to 3.10.0 due to bug - Allow Dapps to specify gas price as hex string. - Add button for copying state logs to clipboard. -## 3.7.8 2017-6-12 +## [3.7.8] - 2017-06-12 - Add an `ethereum:` prefix to the QR code address - The default network on installation is now MainNet @@ -1585,30 +1585,30 @@ rollback to 3.10.0 due to bug - Update gasLimit params with every new block seen. - Fix ENS resolver symbol UI. -## 3.7.7 2017-6-8 +## [3.7.7] - 2017-06-08 - Fix bug where metamask would show old data after computer being asleep or disconnected from the internet. -## 3.7.6 2017-6-5 +## [3.7.6] - 2017-06-05 - Fix bug that prevented publishing contracts. -## 3.7.5 2017-6-5 +## [3.7.5] - 2017-06-05 - Prevent users from sending to the `0x0` address. - Provide useful errors when entering bad characters in ENS name. - Add ability to copy addresses from transaction confirmation view. -## 3.7.4 2017-6-2 +## [3.7.4] - 2017-06-02 - Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex). - Fixed bug with gas limit calculation that would sometimes create unsubmittable gas limits. -## 3.7.3 2017-6-1 +## [3.7.3] - 2017-06-01 - Rebuilt to fix cache clearing bug. -## 3.7.2 2017-5-31 +## [3.7.2] - 2017-05-31 - Now when switching networks sites that use web3 will reload - Now when switching networks the extension does not restart @@ -1621,7 +1621,7 @@ rollback to 3.10.0 due to bug - Some contracts will now display logos instead of jazzicons. - Some contracts will now have names displayed in the confirmation view. -## 3.7.0 2017-5-23 +## [3.7.0] - 2017-05-23 - Add Transaction Number (nonce) to transaction list. - Label the pending tx icon with a tooltip. @@ -1629,7 +1629,7 @@ rollback to 3.10.0 due to bug - Continually resubmit pending txs for a period of time to ensure successful broadcast. - ENS names will no longer resolve to their owner if no resolver is set. Resolvers must be explicitly set and configured. -## 3.6.5 2017-5-17 +## [3.6.5] - 2017-05-17 - Fix bug where edited gas parameters would not take effect. - Trim currency list. @@ -1638,15 +1638,15 @@ rollback to 3.10.0 due to bug - Fix event filter bug introduced by newer versions of Geth. - Fix bug where decimals in gas inputs could result in strange values. -## 3.6.4 2017-5-8 +## [3.6.4] - 2017-05-09 - Fix main-net ENS resolution. -## 3.6.3 2017-5-8 +## [3.6.3] - 2017-05-09 - Fix bug that could stop newer versions of Geth from working with MetaMask. -## 3.6.2 2017-5-8 +## [3.6.2] - 2017-05-08 - Input gas price in Gwei. - Enforce Safe Gas Minimum recommended by EthGasStation. @@ -1654,39 +1654,39 @@ rollback to 3.10.0 due to bug - Reduce UI size by removing internal web3. - Fix bug where gas parameters would not properly update on adjustment. -## 3.6.1 2017-4-30 +## [3.6.1] - 2017-05-07 - Made fox less nosy. - Fix bug where error was reported in debugger console when Chrome opened a new window. -## 3.6.0 2017-4-26 +## [3.6.0] - 2017-04-27 - Add Rinkeby Test Network to our network list. -## 3.5.4 2017-4-25 +## [3.5.4] - 2017-04-25 - Fix occasional nonce tracking issue. - Fix bug where some events would not be emitted by web3. - Fix bug where an error would be thrown when composing signatures for networks with large ID values. -## 3.5.3 2017-4-24 +## [3.5.3] - 2017-04-24 - Popup new transactions in Firefox. - Fix transition issue from account detail screen. - Revise buy screen for more modularity. - Fixed some other small bugs. -## 3.5.2 2017-3-28 +## [3.5.2] - 2017-03-28 - Fix bug where gas estimate totals were sometimes wrong. - Add link to Kovan Test Faucet instructions on buy view. - Inject web3 into loaded iFrames. -## 3.5.1 2017-3-27 +## [3.5.1] - 2017-03-27 - Fix edge case where users were unable to enable the notice button if notices were short enough to not require a scrollbar. -## 3.5.0 2017-3-27 +## [3.5.0] - 2017-03-27 - Add better error messages for when a transaction fails on approval - Allow sending to ENS names in send form on Ropsten. @@ -1701,7 +1701,7 @@ rollback to 3.10.0 due to bug - Add Kovan as an option on our network list. - Fixed bug where transactions on other networks would disappear when submitting a transaction on another network. -## 3.4.0 2017-3-8 +## [3.4.0] - 2017-03-08 - Add two most recently used custom RPCs to network dropdown menu. - Add personal_sign method support. @@ -1709,53 +1709,49 @@ rollback to 3.10.0 due to bug - Add ability to customize gas and gasPrice on the transaction approval screen. - Increase default gas buffer to 1.5x estimated gas value. -## 3.3.0 2017-2-20 +## [3.3.0] - 2017-02-20 - net_version has been made synchronous. - Test suite for migrations expanded. - Network now changeable from lock screen. - Improve test coverage of eth.sign behavior, including a code example of verifying a signature. -## 3.2.2 2017-2-8 +## [3.2.2] - 2017-02-09 - Revert eth.sign behavior to the previous one with a big warning. We will be gradually implementing the new behavior over the coming time. https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign - Improve test coverage of eth.sign behavior, including a code example of verifying a signature. -## 3.2.2 2017-2-8 - -- Revert eth.sign behavior to the previous one with a big warning. We will be gradually implementing the new behavior over the coming time. https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign - -## 3.2.1 2017-2-8 +## [3.2.1] - 2017-02-09 - Revert back to old style message signing. - Fixed some build errors that were causing a variety of bugs. -## 3.2.0 2017-2-8 +## [3.2.0] - 2017-02-08 - Add ability to import accounts in JSON file format (used by Mist, Geth, MyEtherWallet, and more!) - Fix unapproved messages not being included in extension badge. - Fix rendering bug where the Confirm transaction view would let you approve transactions when the account has insufficient balance. -## 3.1.2 2017-1-24 +## [3.1.2] - 2017-01-24 - Fix "New Account" default keychain -## 3.1.1 2017-1-20 +## [3.1.1] - 2017-01-20 - Fix HD wallet seed export -## 3.1.0 2017-1-18 +## [3.1.0] - 2017-01-18 - Add ability to import accounts by private key. - Fixed bug that returned the wrong transaction hashes on private networks that had not implemented EIP 155 replay protection (like TestRPC). -## 3.0.1 2017-1-17 +## [3.0.1] - 2017-01-17 - Fixed bug that prevented eth.sign from working. - Fix the displaying of transactions that have been submitted to the network in Transaction History -## 3.0.0 2017-1-16 +## [3.0.0] - 2017-01-16 - Fix seed word account generation (https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.t4i1qmmsz). - Fix Bug where you see an empty transaction flash by on the confirm transaction view. @@ -1770,33 +1766,33 @@ rollback to 3.10.0 due to bug - Implement replay attack protections allowed by EIP 155. - Fix bug where sometimes loading account data would fail by querying a future block. -## 2.14.1 2016-12-20 +## [2.14.1] - 2016-12-20 - Update Coinbase info. and increase the buy amount to $15 - Fixed ropsten transaction links - Temporarily disable extension reload detection causing infinite reload bug. - Implemented basic checking for valid RPC URIs. -## 2.14.0 2016-12-16 +## [2.14.0] - 2016-12-16 - Removed Morden testnet provider from provider menu. - Add support for notices. - Fix broken reload detection. - Fix transaction forever cached-as-pending bug. -## 2.13.11 2016-11-23 +## [2.13.11] - 2016-11-23 - Add support for synchronous RPC method "eth_uninstallFilter". - Forgotten password prompts now send users directly to seed word restoration. -## 2.13.10 2016-11-22 +## [2.13.10] - 2016-11-22 - Improve gas calculation logic. - Default to Dapp-specified gas limits for transactions. - Ropsten networks now properly point to the faucet when attempting to buy ether. - Ropsten transactions now link to etherscan correctly. -## 2.13.9 2016-11-21 +## [2.13.9] - 2016-11-21 - Add support for the new, default Ropsten Test Network. - Fix bug that would cause MetaMask to occasionally lose its StreamProvider connection and drop requests. @@ -1804,19 +1800,19 @@ rollback to 3.10.0 due to bug - Point ropsten faucet button to actual faucet. - Phase out ethereumjs-util from our encryptor module. -## 2.13.8 2016-11-16 +## [2.13.8] - 2016-11-16 - Show a warning when a transaction fails during simulation. - Fix bug where 20% of gas estimate was not being added properly. - Render error messages in confirmation screen more gracefully. -## 2.13.7 2016-11-8 +## [2.13.7] - 2016-11-08 - Fix bug where gas estimate would sometimes be very high. - Increased our gas estimate from 100k gas to 20% of estimate. - Fix GitHub link on info page to point at current repository. -## 2.13.6 2016-10-26 +## [2.13.6] - 2016-10-26 - Add a check for improper Transaction data. - Inject up to date version of web3.js @@ -1825,18 +1821,18 @@ rollback to 3.10.0 due to bug - Fix bug where connecting to a local morden node would make two providers appear selected. - Fix bug that was sometimes preventing transactions from being sent. -## 2.13.5 2016-10-18 +## [2.13.5] - 2016-10-18 - Increase default max gas to `100000` over the RPC's `estimateGas` response. - Fix bug where slow-loading dapps would sometimes trigger infinite reload loops. -## 2.13.4 2016-10-17 +## [2.13.4] - 2016-10-17 - Add custom transaction fee field to send form. - Fix bug where web3 was being injected into XML files. - Fix bug where changing network would not reload current Dapps. -## 2.13.3 2016-10-4 +## [2.13.3] - 2016-10-05 - Fix bug where log queries were filtered out. - Decreased vault confirmation button font size to help some Linux users who could not see it. @@ -1847,42 +1843,42 @@ rollback to 3.10.0 due to bug - Updated Terms of Service and Usage. - Prompt users to re-agree to the Terms of Service when they are updated. -## 2.13.2 2016-10-4 +## [2.13.2] - 2016-10-04 - Fix bug where chosen FIAT exchange rate does no persist when switching networks - Fix additional parameters that made MetaMask sometimes receive errors from Parity. - Fix bug where invalid transactions would still open the MetaMask popup. - Removed hex prefix from private key export, to increase compatibility with Geth, MyEtherWallet, and Jaxx. -## 2.13.1 2016-09-23 +## [2.13.1] - 2016-09-23 - Fix a bug with estimating gas on Parity - Show loading indication when selecting ShapeShift as purchasing method. -## 2.13.0 2016-09-18 +## [2.13.0] - 2016-09-18 - Add Parity compatibility, fixing Geth dependency issues. - Add a link to the transaction in history that goes to https://metamask.github.io/eth-tx-viz too help visualize transactions and to where they are going. - Show "Buy Ether" button and warning on tx confirmation when sender balance is insufficient -## 2.12.1 2016-09-14 +## [2.12.1] - 2016-09-14 - Fixed bug where if you send a transaction from within MetaMask extension the popup notification opens up. - Fixed bug where some tx errors would block subsequent txs until the plugin was refreshed. -## 2.12.0 2016-09-14 +## [2.12.0] - 2016-09-14 - Add a QR button to the Account detail screen - Fixed bug where opening MetaMask could close a non-metamask popup. - Fixed memory leak that caused occasional crashes. -## 2.11.1 2016-09-12 +## [2.11.1] - 2016-09-13 - Fix bug that prevented caches from being cleared in Opera. -## 2.11.0 2016-09-12 +## [2.11.0] - 2016-09-12 - Fix bug where pending transactions from Test net (or other networks) show up In Main net. - Add fiat conversion values to more views. @@ -1893,29 +1889,29 @@ rollback to 3.10.0 due to bug - Now showing loading indication during vault unlocking, to clarify behavior for users who are experiencing slow unlocks. - Now only initially creates one wallet when restoring a vault, to reduce some users' confusion. -## 2.10.2 2016-09-02 +## [2.10.2] - 2016-09-02 - Fix bug where notification popup would not display. -## 2.10.1 2016-09-02 +## [2.10.1] - 2016-09-02 - Fix bug where provider menu did not allow switching to custom network from a custom network. - Sending a transaction from within MetaMask no longer triggers a popup. - The ability to build without livereload features (such as for production) can be enabled with the gulp --disableLiveReload flag. - Fix Ethereum JSON RPC Filters bug. -## 2.10.0 2016-08-29 +## [2.10.0] - 2016-08-29 - Changed transaction approval from notifications system to popup system. - Add a back button to locked screen to allow restoring vault from seed words when password is forgotten. - Forms now retain their values even when closing the popup and reopening it. - Fixed a spelling error in provider menu. -## 2.9.2 2016-08-24 +## [2.9.2] - 2016-08-24 - Fixed shortcut bug from preventing installation. -## 2.9.1 2016-08-24 +## [2.9.1] - 2016-08-24 - Added static image as fallback for when WebGL isn't supported. - Transaction history now has a hard limit. @@ -1925,14 +1921,14 @@ rollback to 3.10.0 due to bug - Prevent API calls in tests. - Fixed bug where sign message confirmation would sometimes render blank. -## 2.9.0 2016-08-22 +## [2.9.0] - 2016-08-22 - Added ShapeShift to the transaction history - Added affiliate key to Shapeshift requests - Added feature to reflect current conversion rates of current vault balance. - Modify balance display logic. -## 2.8.0 2016-08-15 +## [2.8.0] - 2016-08-15 - Integrate ShapeShift - Add a form for Coinbase to specify amount to buy @@ -1940,35 +1936,35 @@ rollback to 3.10.0 due to bug - Make dapp-metamask connection more reliable - Remove Ethereum Classic from provider menu. -## 2.7.3 2016-07-29 +## [2.7.3] - 2016-07-29 - Fix bug where changing an account would not update in a live Dapp. -## 2.7.2 2016-07-29 +## [2.7.2] - 2016-07-29 - Add Ethereum Classic to provider menu - Fix bug where host store would fail to receive updates. -## 2.7.1 2016-07-27 +## [2.7.1] - 2016-07-27 - Fix bug where web3 would sometimes not be injected in time for the application. - Fixed bug where sometimes when opening the plugin, it would not fully open until closing and re-opening. - Got most functionality working within Firefox (still working on review process before it can be available). - Fixed menu dropdown bug introduced in Chrome 52. -## 2.7.0 2016-07-21 +## [2.7.0] - 2016-07-21 - Added a Warning screen about storing ETH - Add buy Button! - MetaMask now throws descriptive errors when apps try to use synchronous web3 methods. - Removed firefox-specific line in manifest. -## 2.6.2 2016-07-20 +## [2.6.2] - 2016-07-20 - Fixed bug that would prevent the plugin from reopening on the first try after receiving a new transaction while locked. - Fixed bug that would render 0 ETH as a non-exact amount. -## 2.6.1 2016-07-13 +## [2.6.1] - 2016-07-13 - Fix tool tips on Eth balance to show the 6 decimals - Fix rendering of recipient SVG in tx approval notification. @@ -1977,7 +1973,7 @@ rollback to 3.10.0 due to bug - Fixed bug where some lowercase or uppercase addresses were not being recognized as valid. - Fixed bug where gas cost was misestimated on the tx confirmation view. -## 2.6.0 2016-07-11 +## [2.6.0] - 2016-07-11 - Fix formatting of ETH balance - Fix formatting of account details. @@ -1989,7 +1985,7 @@ rollback to 3.10.0 due to bug - Abbreviate ether balances on transaction details to maintain formatting. - General code cleanup. -## 2.5.0 2016-06-29 +## [2.5.0] - 2016-06-29 - Implement new account design. - Added a network indicator mark in dropdown menu @@ -1998,7 +1994,7 @@ rollback to 3.10.0 due to bug - Unify wording for transaction approve/reject options on notifications and the extension. - Fix bug where confirmation view would be shown twice. -## 2.4.5 2016-06-29 +## [2.4.5] - 2016-06-29 - Fixed bug where MetaMask interfered with PDF loading. - Moved switch account icon into menu bar. @@ -2010,23 +2006,23 @@ rollback to 3.10.0 due to bug - Fix out-of-place positioning of pending transaction badges on wallet list. - Change network status icons to reflect current design. -## 2.4.4 2016-06-23 +## [2.4.4] - 2016-06-23 - Update web3-stream-provider for batch payload bug fix -## 2.4.3 2016-06-23 +## [2.4.3] - 2016-06-23 - Remove redundant network option buttons from settings page - Switch out font family Transat for Montserrat -## 2.4.2 2016-06-22 +## [2.4.2] - 2016-06-22 - Change out export icon for key. - Unify copy to clipboard icon - Fixed eth.sign behavior. - Fix behavior of batched outbound transactions. -## 2.4.0 2016-06-20 +## [2.4.0] - 2016-06-20 - Clean up UI. - Remove nonfunctional QR code button. @@ -2035,26 +2031,26 @@ rollback to 3.10.0 due to bug - Fixed bug when signing messages under 64 hex characters long. - Add disclaimer view with placeholder text for first time users. -## 2.3.1 2016-06-09 +## [2.3.1] - 2016-06-09 - Style up the info page - Cache identicon images to optimize for long lists of transactions. - Fix out of gas errors -## 2.3.0 2016-06-06 +## [2.3.0] - 2016-06-06 - Show network status in title bar - Added seed word recovery to config screen. - Clicking network status indicator now reveals a provider menu. -## 2.2.0 2016-06-02 +## [2.2.0] - 2016-06-02 - Redesigned init, vault create, vault restore and seed confirmation screens. - Added pending transactions to transaction list on account screen. - Clicking a pending transaction takes you back to the transaction approval screen. - Update provider-engine to fix intermittent out of gas errors. -## 2.1.0 2016-05-26 +## [2.1.0] - 2016-05-26 - Added copy address button to account list. - Fixed back button on confirm transaction screen. @@ -2062,7 +2058,7 @@ rollback to 3.10.0 due to bug - Fixed bug where error warning was sometimes not cleared on view transition. - Updated eth-lightwallet to fix a critical security issue. -## 2.0.0 2016-05-23 +## [2.0.0] - 2016-05-23 - UI Overhaul per Vlad Todirut's designs. - Replaced identicons with jazzicons. @@ -2072,27 +2068,27 @@ rollback to 3.10.0 due to bug - Added ability to generate new accounts. - Added ability to locally nickname accounts. -## 1.8.4 2016-05-13 +## [1.8.4] - 2016-05-13 - Point rpc servers to https endpoints. -## 1.8.3 2016-05-12 +## [1.8.3] - 2016-05-12 - Bumped web3 to 0.6.0 - Really fixed `eth_syncing` method response. -## 1.8.2 2016-05-11 +## [1.8.2] - 2016-05-11 - Fixed bug where send view would not load correctly the first time it was visited per account. - Migrated all users to new scalable backend. - Fixed `eth_syncing` method response. -## 1.8.1 2016-05-10 +## [1.8.1] - 2016-05-10 - Initial usage of scalable blockchain backend. - Made official providers more easily configurable for us internally. -## 1.8.0 2016-05-10 +## [1.8.0] - 2016-05-10 - Add support for calls to `eth.sign`. - Moved account exporting within subview of the account detail view. @@ -2103,7 +2099,7 @@ rollback to 3.10.0 due to bug - Changing provider now reloads current Dapps - Improved appearance of transaction list in account detail view. -## 1.7.0 2016-04-29 +## [1.7.0] - 2016-04-29 - Account detail view is now the primary view. - The account detail view now has a "Change acct" button which shows the account list. @@ -2117,7 +2113,7 @@ rollback to 3.10.0 due to bug - Fixed transaction links to etherscan blockchain explorer. - Fixed some UI transitions that had weird behavior. -## 1.6.0 2016-04-22 +## [1.6.0] - 2016-04-22 - Pending transactions are now persisted to localStorage and resume even after browser is closed. - Completed transactions are now persisted and can be displayed via UI. @@ -2129,7 +2125,7 @@ rollback to 3.10.0 due to bug - Improve config view styling. - Users have been migrated from old test-net RPC to a newer test-net RPC. -## 1.5.1 2016-04-15 +## [1.5.1] - 2016-04-15 - Corrected text above account list. Selected account is visible to all sites, not just the current domain. - Merged the UI codebase into the main plugin codebase for simpler maintenance. @@ -2137,13 +2133,13 @@ rollback to 3.10.0 due to bug - Fix some inpage synchronous methods - Change account rendering to show four decimals and a leading zero. -## 1.5.0 2016-04-13 +## [1.5.0] - 2016-04-13 - Added ability to send ether. - Fixed bugs related to using Javascript numbers, which lacked appropriate precision. - Replaced Etherscan main-net provider with our own production RPC. -## 1.4.0 2016-04-08 +## [1.4.0] - 2016-04-08 - Removed extra entropy text field for simplified vault creation. - Now supports exporting an account's private key. @@ -2152,20 +2148,285 @@ rollback to 3.10.0 due to bug - Fix popup's web3 stream provider - Temporarily deactivated fauceting indication because it would activate when restoring an empty account. -## 1.3.2 2016-04-04 +## [1.3.2] - 2016-04-04 - When unlocking, first account is auto-selected. - When creating a first vault on the test-net, the first account is auto-funded. - Fixed some styling issues. -## 1.0.1-1.3.1 - -Many changes not logged. Hopefully beginning to log consistently now! - -## 1.0.0 +## [1.0.0] - 2016-03-25 Made seed word restoring BIP44 compatible. -## 0.14.0 +## [0.14.0] - 2016-03-16 Added the ability to restore accounts from seed words. + +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.3.0...HEAD +[9.3.0]: https://github.com/MetaMask/metamask-extension/compare/v9.2.1...v9.3.0 +[9.2.1]: https://github.com/MetaMask/metamask-extension/compare/v9.2.0...v9.2.1 +[9.2.0]: https://github.com/MetaMask/metamask-extension/compare/v9.1.1...v9.2.0 +[9.1.1]: https://github.com/MetaMask/metamask-extension/compare/v9.1.0...v9.1.1 +[9.1.0]: https://github.com/MetaMask/metamask-extension/compare/v9.0.5...v9.1.0 +[9.0.5]: https://github.com/MetaMask/metamask-extension/compare/v9.0.4...v9.0.5 +[9.0.4]: https://github.com/MetaMask/metamask-extension/compare/v9.0.3...v9.0.4 +[9.0.3]: https://github.com/MetaMask/metamask-extension/compare/v9.0.2...v9.0.3 +[9.0.2]: https://github.com/MetaMask/metamask-extension/compare/v9.0.1...v9.0.2 +[9.0.1]: https://github.com/MetaMask/metamask-extension/compare/v9.0.0...v9.0.1 +[9.0.0]: https://github.com/MetaMask/metamask-extension/compare/v8.1.11...v9.0.0 +[8.1.11]: https://github.com/MetaMask/metamask-extension/compare/v8.1.10...v8.1.11 +[8.1.10]: https://github.com/MetaMask/metamask-extension/compare/v8.1.9...v8.1.10 +[8.1.9]: https://github.com/MetaMask/metamask-extension/compare/v8.1.8...v8.1.9 +[8.1.8]: https://github.com/MetaMask/metamask-extension/compare/v8.1.7...v8.1.8 +[8.1.7]: https://github.com/MetaMask/metamask-extension/compare/v8.1.6...v8.1.7 +[8.1.6]: https://github.com/MetaMask/metamask-extension/compare/v8.1.5...v8.1.6 +[8.1.5]: https://github.com/MetaMask/metamask-extension/compare/v8.1.4...v8.1.5 +[8.1.4]: https://github.com/MetaMask/metamask-extension/compare/v8.1.3...v8.1.4 +[8.1.3]: https://github.com/MetaMask/metamask-extension/compare/v8.1.2...v8.1.3 +[8.1.2]: https://github.com/MetaMask/metamask-extension/compare/v8.1.1...v8.1.2 +[8.1.1]: https://github.com/MetaMask/metamask-extension/compare/v8.1.0...v8.1.1 +[8.1.0]: https://github.com/MetaMask/metamask-extension/compare/v8.0.10...v8.1.0 +[8.0.10]: https://github.com/MetaMask/metamask-extension/compare/v8.0.9...v8.0.10 +[8.0.9]: https://github.com/MetaMask/metamask-extension/compare/v8.0.8...v8.0.9 +[8.0.8]: https://github.com/MetaMask/metamask-extension/compare/v8.0.7...v8.0.8 +[8.0.7]: https://github.com/MetaMask/metamask-extension/compare/v8.0.6...v8.0.7 +[8.0.6]: https://github.com/MetaMask/metamask-extension/compare/v8.0.5...v8.0.6 +[8.0.5]: https://github.com/MetaMask/metamask-extension/compare/v8.0.4...v8.0.5 +[8.0.4]: https://github.com/MetaMask/metamask-extension/compare/v8.0.3...v8.0.4 +[8.0.3]: https://github.com/MetaMask/metamask-extension/compare/v8.0.2...v8.0.3 +[8.0.2]: https://github.com/MetaMask/metamask-extension/compare/v8.0.1...v8.0.2 +[8.0.1]: https://github.com/MetaMask/metamask-extension/compare/v8.0.0...v8.0.1 +[8.0.0]: https://github.com/MetaMask/metamask-extension/compare/v7.7.9...v8.0.0 +[7.7.9]: https://github.com/MetaMask/metamask-extension/compare/v7.7.8...v7.7.9 +[7.7.8]: https://github.com/MetaMask/metamask-extension/compare/v7.7.7...v7.7.8 +[7.7.7]: https://github.com/MetaMask/metamask-extension/compare/v7.7.6...v7.7.7 +[7.7.6]: https://github.com/MetaMask/metamask-extension/compare/v7.7.5...v7.7.6 +[7.7.5]: https://github.com/MetaMask/metamask-extension/compare/v7.7.4...v7.7.5 +[7.7.4]: https://github.com/MetaMask/metamask-extension/compare/v7.7.3...v7.7.4 +[7.7.3]: https://github.com/MetaMask/metamask-extension/compare/v7.7.2...v7.7.3 +[7.7.2]: https://github.com/MetaMask/metamask-extension/compare/v7.7.1...v7.7.2 +[7.7.1]: https://github.com/MetaMask/metamask-extension/compare/v7.7.0...v7.7.1 +[7.7.0]: https://github.com/MetaMask/metamask-extension/compare/v7.6.1...v7.7.0 +[7.6.1]: https://github.com/MetaMask/metamask-extension/compare/v7.6.0...v7.6.1 +[7.6.0]: https://github.com/MetaMask/metamask-extension/compare/v7.5.3...v7.6.0 +[7.5.3]: https://github.com/MetaMask/metamask-extension/compare/v7.5.2...v7.5.3 +[7.5.2]: https://github.com/MetaMask/metamask-extension/compare/v7.5.1...v7.5.2 +[7.5.1]: https://github.com/MetaMask/metamask-extension/compare/v7.5.0...v7.5.1 +[7.5.0]: https://github.com/MetaMask/metamask-extension/compare/v7.4.0...v7.5.0 +[7.4.0]: https://github.com/MetaMask/metamask-extension/compare/v7.3.1...v7.4.0 +[7.3.1]: https://github.com/MetaMask/metamask-extension/compare/v7.3.0...v7.3.1 +[7.3.0]: https://github.com/MetaMask/metamask-extension/compare/v7.2.3...v7.3.0 +[7.2.3]: https://github.com/MetaMask/metamask-extension/compare/v7.2.2...v7.2.3 +[7.2.2]: https://github.com/MetaMask/metamask-extension/compare/v7.2.1...v7.2.2 +[7.2.1]: https://github.com/MetaMask/metamask-extension/compare/v7.2.0...v7.2.1 +[7.2.0]: https://github.com/MetaMask/metamask-extension/compare/v7.1.1...v7.2.0 +[7.1.1]: https://github.com/MetaMask/metamask-extension/compare/v7.1.0...v7.1.1 +[7.1.0]: https://github.com/MetaMask/metamask-extension/compare/v7.0.1...v7.1.0 +[7.0.1]: https://github.com/MetaMask/metamask-extension/compare/v7.0.0...v7.0.1 +[7.0.0]: https://github.com/MetaMask/metamask-extension/compare/v6.7.3...v7.0.0 +[6.7.3]: https://github.com/MetaMask/metamask-extension/compare/v6.7.2...v6.7.3 +[6.7.2]: https://github.com/MetaMask/metamask-extension/compare/v6.7.1...v6.7.2 +[6.7.1]: https://github.com/MetaMask/metamask-extension/compare/v6.7.0...v6.7.1 +[6.7.0]: https://github.com/MetaMask/metamask-extension/compare/v6.6.2...v6.7.0 +[6.6.2]: https://github.com/MetaMask/metamask-extension/compare/v6.6.1...v6.6.2 +[6.6.1]: https://github.com/MetaMask/metamask-extension/compare/v6.6.0...v6.6.1 +[6.6.0]: https://github.com/MetaMask/metamask-extension/compare/v6.5.3...v6.6.0 +[6.5.3]: https://github.com/MetaMask/metamask-extension/compare/v6.5.2...v6.5.3 +[6.5.2]: https://github.com/MetaMask/metamask-extension/compare/v6.5.1...v6.5.2 +[6.5.1]: https://github.com/MetaMask/metamask-extension/compare/v6.5.0...v6.5.1 +[6.5.0]: https://github.com/MetaMask/metamask-extension/compare/v6.4.1...v6.5.0 +[6.4.1]: https://github.com/MetaMask/metamask-extension/compare/v6.4.0...v6.4.1 +[6.4.0]: https://github.com/MetaMask/metamask-extension/compare/v6.3.2...v6.4.0 +[6.3.2]: https://github.com/MetaMask/metamask-extension/compare/v6.3.1...v6.3.2 +[6.3.1]: https://github.com/MetaMask/metamask-extension/compare/v6.3.0...v6.3.1 +[6.3.0]: https://github.com/MetaMask/metamask-extension/compare/v6.2.2...v6.3.0 +[6.2.2]: https://github.com/MetaMask/metamask-extension/compare/v6.2.1...v6.2.2 +[6.2.1]: https://github.com/MetaMask/metamask-extension/compare/v6.2.0...v6.2.1 +[6.2.0]: https://github.com/MetaMask/metamask-extension/compare/v6.1.0...v6.2.0 +[6.1.0]: https://github.com/MetaMask/metamask-extension/compare/v6.0.1...v6.1.0 +[6.0.1]: https://github.com/MetaMask/metamask-extension/compare/v6.0.0...v6.0.1 +[6.0.0]: https://github.com/MetaMask/metamask-extension/compare/v5.3.5...v6.0.0 +[5.3.5]: https://github.com/MetaMask/metamask-extension/compare/v5.3.4...v5.3.5 +[5.3.4]: https://github.com/MetaMask/metamask-extension/compare/v5.3.3...v5.3.4 +[5.3.3]: https://github.com/MetaMask/metamask-extension/compare/v5.3.2...v5.3.3 +[5.3.2]: https://github.com/MetaMask/metamask-extension/compare/v5.3.1...v5.3.2 +[5.3.1]: https://github.com/MetaMask/metamask-extension/compare/v5.3.0...v5.3.1 +[5.3.0]: https://github.com/MetaMask/metamask-extension/compare/v5.2.2...v5.3.0 +[5.2.2]: https://github.com/MetaMask/metamask-extension/compare/v5.2.1...v5.2.2 +[5.2.1]: https://github.com/MetaMask/metamask-extension/compare/v5.2.0...v5.2.1 +[5.2.0]: https://github.com/MetaMask/metamask-extension/compare/v5.1.0...v5.2.0 +[5.1.0]: https://github.com/MetaMask/metamask-extension/compare/v5.0.4...v5.1.0 +[5.0.4]: https://github.com/MetaMask/metamask-extension/compare/v5.0.3...v5.0.4 +[5.0.3]: https://github.com/MetaMask/metamask-extension/compare/v5.0.2...v5.0.3 +[5.0.2]: https://github.com/MetaMask/metamask-extension/compare/v5.0.1...v5.0.2 +[5.0.1]: https://github.com/MetaMask/metamask-extension/compare/v5.0.0...v5.0.1 +[5.0.0]: https://github.com/MetaMask/metamask-extension/compare/v4.17.1...v5.0.0 +[4.17.1]: https://github.com/MetaMask/metamask-extension/compare/v4.17.0...v4.17.1 +[4.17.0]: https://github.com/MetaMask/metamask-extension/compare/v4.16.0...v4.17.0 +[4.16.0]: https://github.com/MetaMask/metamask-extension/compare/v4.15.0...v4.16.0 +[4.15.0]: https://github.com/MetaMask/metamask-extension/compare/v4.14.0...v4.15.0 +[4.14.0]: https://github.com/MetaMask/metamask-extension/compare/v4.13.0...v4.14.0 +[4.13.0]: https://github.com/MetaMask/metamask-extension/compare/v4.12.0...v4.13.0 +[4.12.0]: https://github.com/MetaMask/metamask-extension/compare/v4.11.1...v4.12.0 +[4.11.1]: https://github.com/MetaMask/metamask-extension/compare/v4.11.0...v4.11.1 +[4.11.0]: https://github.com/MetaMask/metamask-extension/compare/v4.10.0...v4.11.0 +[4.10.0]: https://github.com/MetaMask/metamask-extension/compare/v4.9.3...v4.10.0 +[4.9.3]: https://github.com/MetaMask/metamask-extension/compare/v4.9.2...v4.9.3 +[4.9.2]: https://github.com/MetaMask/metamask-extension/compare/v4.9.1...v4.9.2 +[4.9.1]: https://github.com/MetaMask/metamask-extension/compare/v4.9.0...v4.9.1 +[4.9.0]: https://github.com/MetaMask/metamask-extension/compare/v4.8.0...v4.9.0 +[4.8.0]: https://github.com/MetaMask/metamask-extension/compare/v4.7.4...v4.8.0 +[4.7.4]: https://github.com/MetaMask/metamask-extension/compare/v4.7.3...v4.7.4 +[4.7.3]: https://github.com/MetaMask/metamask-extension/compare/v4.7.2...v4.7.3 +[4.7.2]: https://github.com/MetaMask/metamask-extension/compare/v4.7.1...v4.7.2 +[4.7.1]: https://github.com/MetaMask/metamask-extension/compare/v4.7.0...v4.7.1 +[4.7.0]: https://github.com/MetaMask/metamask-extension/compare/v4.6.1...v4.7.0 +[4.6.1]: https://github.com/MetaMask/metamask-extension/compare/v4.6.0...v4.6.1 +[4.6.0]: https://github.com/MetaMask/metamask-extension/compare/v4.5.5...v4.6.0 +[4.5.5]: https://github.com/MetaMask/metamask-extension/compare/v4.5.4...v4.5.5 +[4.5.4]: https://github.com/MetaMask/metamask-extension/compare/v4.5.3...v4.5.4 +[4.5.3]: https://github.com/MetaMask/metamask-extension/compare/v4.5.2...v4.5.3 +[4.5.2]: https://github.com/MetaMask/metamask-extension/compare/v4.5.1...v4.5.2 +[4.5.1]: https://github.com/MetaMask/metamask-extension/compare/v4.5.0...v4.5.1 +[4.5.0]: https://github.com/MetaMask/metamask-extension/compare/v4.4.0...v4.5.0 +[4.4.0]: https://github.com/MetaMask/metamask-extension/compare/v4.3.0...v4.4.0 +[4.3.0]: https://github.com/MetaMask/metamask-extension/compare/v4.2.0...v4.3.0 +[4.2.0]: https://github.com/MetaMask/metamask-extension/compare/v4.1.3...v4.2.0 +[4.1.3]: https://github.com/MetaMask/metamask-extension/compare/v4.1.2...v4.1.3 +[4.1.2]: https://github.com/MetaMask/metamask-extension/compare/v4.1.1...v4.1.2 +[4.1.1]: https://github.com/MetaMask/metamask-extension/compare/v4.1.0...v4.1.1 +[4.1.0]: https://github.com/MetaMask/metamask-extension/compare/v4.0.0...v4.1.0 +[4.0.0]: https://github.com/MetaMask/metamask-extension/compare/v3.14.2...v4.0.0 +[3.14.2]: https://github.com/MetaMask/metamask-extension/compare/v3.14.1...v3.14.2 +[3.14.1]: https://github.com/MetaMask/metamask-extension/compare/v3.14.0...v3.14.1 +[3.14.0]: https://github.com/MetaMask/metamask-extension/compare/v3.13.8...v3.14.0 +[3.13.8]: https://github.com/MetaMask/metamask-extension/compare/v3.13.7...v3.13.8 +[3.13.7]: https://github.com/MetaMask/metamask-extension/compare/v3.13.6...v3.13.7 +[3.13.6]: https://github.com/MetaMask/metamask-extension/compare/v3.13.5...v3.13.6 +[3.13.5]: https://github.com/MetaMask/metamask-extension/compare/v3.13.4...v3.13.5 +[3.13.4]: https://github.com/MetaMask/metamask-extension/compare/v3.13.3...v3.13.4 +[3.13.3]: https://github.com/MetaMask/metamask-extension/compare/v3.13.2...v3.13.3 +[3.13.2]: https://github.com/MetaMask/metamask-extension/compare/v3.13.1...v3.13.2 +[3.13.1]: https://github.com/MetaMask/metamask-extension/compare/v3.13.0...v3.13.1 +[3.13.0]: https://github.com/MetaMask/metamask-extension/compare/v3.12.1...v3.13.0 +[3.12.1]: https://github.com/MetaMask/metamask-extension/compare/v3.12.0...v3.12.1 +[3.12.0]: https://github.com/MetaMask/metamask-extension/compare/v3.11.2...v3.12.0 +[3.11.2]: https://github.com/MetaMask/metamask-extension/compare/v3.11.1...v3.11.2 +[3.11.1]: https://github.com/MetaMask/metamask-extension/compare/v3.11.0...v3.11.1 +[3.11.0]: https://github.com/MetaMask/metamask-extension/compare/v3.10.9...v3.11.0 +[3.10.9]: https://github.com/MetaMask/metamask-extension/compare/v3.10.8...v3.10.9 +[3.10.8]: https://github.com/MetaMask/metamask-extension/compare/v3.10.7...v3.10.8 +[3.10.7]: https://github.com/MetaMask/metamask-extension/compare/v3.10.6...v3.10.7 +[3.10.6]: https://github.com/MetaMask/metamask-extension/compare/v3.10.5...v3.10.6 +[3.10.5]: https://github.com/MetaMask/metamask-extension/compare/v3.10.4...v3.10.5 +[3.10.4]: https://github.com/MetaMask/metamask-extension/compare/v3.10.3...v3.10.4 +[3.10.3]: https://github.com/MetaMask/metamask-extension/compare/v3.10.2...v3.10.3 +[3.10.2]: https://github.com/MetaMask/metamask-extension/compare/v3.10.1...v3.10.2 +[3.10.1]: https://github.com/MetaMask/metamask-extension/compare/v3.10.0...v3.10.1 +[3.10.0]: https://github.com/MetaMask/metamask-extension/compare/v3.9.13...v3.10.0 +[3.9.13]: https://github.com/MetaMask/metamask-extension/compare/v3.9.12...v3.9.13 +[3.9.12]: https://github.com/MetaMask/metamask-extension/compare/v3.9.11...v3.9.12 +[3.9.11]: https://github.com/MetaMask/metamask-extension/compare/v3.9.10...v3.9.11 +[3.9.10]: https://github.com/MetaMask/metamask-extension/compare/v3.9.9...v3.9.10 +[3.9.9]: https://github.com/MetaMask/metamask-extension/compare/v3.9.8...v3.9.9 +[3.9.8]: https://github.com/MetaMask/metamask-extension/compare/v3.9.7...v3.9.8 +[3.9.7]: https://github.com/MetaMask/metamask-extension/compare/v3.9.6...v3.9.7 +[3.9.6]: https://github.com/MetaMask/metamask-extension/compare/v3.9.5...v3.9.6 +[3.9.5]: https://github.com/MetaMask/metamask-extension/compare/v3.9.4...v3.9.5 +[3.9.4]: https://github.com/MetaMask/metamask-extension/compare/v3.9.3...v3.9.4 +[3.9.3]: https://github.com/MetaMask/metamask-extension/compare/v3.9.2...v3.9.3 +[3.9.2]: https://github.com/MetaMask/metamask-extension/compare/v3.9.1...v3.9.2 +[3.9.1]: https://github.com/MetaMask/metamask-extension/compare/v3.9.0...v3.9.1 +[3.9.0]: https://github.com/MetaMask/metamask-extension/compare/v3.8.6...v3.9.0 +[3.8.6]: https://github.com/MetaMask/metamask-extension/compare/v3.8.5...v3.8.6 +[3.8.5]: https://github.com/MetaMask/metamask-extension/compare/v3.8.4...v3.8.5 +[3.8.4]: https://github.com/MetaMask/metamask-extension/compare/v3.8.3...v3.8.4 +[3.8.3]: https://github.com/MetaMask/metamask-extension/compare/v3.8.2...v3.8.3 +[3.8.2]: https://github.com/MetaMask/metamask-extension/compare/v3.8.1...v3.8.2 +[3.8.1]: https://github.com/MetaMask/metamask-extension/compare/v3.8.0...v3.8.1 +[3.8.0]: https://github.com/MetaMask/metamask-extension/compare/v3.7.8...v3.8.0 +[3.7.8]: https://github.com/MetaMask/metamask-extension/compare/v3.7.7...v3.7.8 +[3.7.7]: https://github.com/MetaMask/metamask-extension/compare/v3.7.6...v3.7.7 +[3.7.6]: https://github.com/MetaMask/metamask-extension/compare/v3.7.5...v3.7.6 +[3.7.5]: https://github.com/MetaMask/metamask-extension/compare/v3.7.4...v3.7.5 +[3.7.4]: https://github.com/MetaMask/metamask-extension/compare/v3.7.3...v3.7.4 +[3.7.3]: https://github.com/MetaMask/metamask-extension/compare/v3.7.2...v3.7.3 +[3.7.2]: https://github.com/MetaMask/metamask-extension/compare/v3.7.0...v3.7.2 +[3.7.0]: https://github.com/MetaMask/metamask-extension/compare/v3.6.5...v3.7.0 +[3.6.5]: https://github.com/MetaMask/metamask-extension/compare/v3.6.4...v3.6.5 +[3.6.4]: https://github.com/MetaMask/metamask-extension/compare/v3.6.3...v3.6.4 +[3.6.3]: https://github.com/MetaMask/metamask-extension/compare/v3.6.2...v3.6.3 +[3.6.2]: https://github.com/MetaMask/metamask-extension/compare/v3.6.1...v3.6.2 +[3.6.1]: https://github.com/MetaMask/metamask-extension/compare/v3.6.0...v3.6.1 +[3.6.0]: https://github.com/MetaMask/metamask-extension/compare/v3.5.4...v3.6.0 +[3.5.4]: https://github.com/MetaMask/metamask-extension/compare/v3.5.3...v3.5.4 +[3.5.3]: https://github.com/MetaMask/metamask-extension/compare/v3.5.2...v3.5.3 +[3.5.2]: https://github.com/MetaMask/metamask-extension/compare/v3.5.1...v3.5.2 +[3.5.1]: https://github.com/MetaMask/metamask-extension/compare/v3.5.0...v3.5.1 +[3.5.0]: https://github.com/MetaMask/metamask-extension/compare/v3.4.0...v3.5.0 +[3.4.0]: https://github.com/MetaMask/metamask-extension/compare/v3.3.0...v3.4.0 +[3.3.0]: https://github.com/MetaMask/metamask-extension/compare/v3.2.2...v3.3.0 +[3.2.2]: https://github.com/MetaMask/metamask-extension/compare/v3.2.1...v3.2.2 +[3.2.1]: https://github.com/MetaMask/metamask-extension/compare/v3.2.0...v3.2.1 +[3.2.0]: https://github.com/MetaMask/metamask-extension/compare/v3.1.2...v3.2.0 +[3.1.2]: https://github.com/MetaMask/metamask-extension/compare/v3.1.1...v3.1.2 +[3.1.1]: https://github.com/MetaMask/metamask-extension/compare/v3.1.0...v3.1.1 +[3.1.0]: https://github.com/MetaMask/metamask-extension/compare/v3.0.1...v3.1.0 +[3.0.1]: https://github.com/MetaMask/metamask-extension/compare/v3.0.0...v3.0.1 +[3.0.0]: https://github.com/MetaMask/metamask-extension/compare/v2.14.1...v3.0.0 +[2.14.1]: https://github.com/MetaMask/metamask-extension/compare/v2.14.0...v2.14.1 +[2.14.0]: https://github.com/MetaMask/metamask-extension/compare/v2.13.11...v2.14.0 +[2.13.11]: https://github.com/MetaMask/metamask-extension/compare/v2.13.10...v2.13.11 +[2.13.10]: https://github.com/MetaMask/metamask-extension/compare/v2.13.9...v2.13.10 +[2.13.9]: https://github.com/MetaMask/metamask-extension/compare/v2.13.8...v2.13.9 +[2.13.8]: https://github.com/MetaMask/metamask-extension/compare/v2.13.7...v2.13.8 +[2.13.7]: https://github.com/MetaMask/metamask-extension/compare/v2.13.6...v2.13.7 +[2.13.6]: https://github.com/MetaMask/metamask-extension/compare/v2.13.5...v2.13.6 +[2.13.5]: https://github.com/MetaMask/metamask-extension/compare/v2.13.4...v2.13.5 +[2.13.4]: https://github.com/MetaMask/metamask-extension/compare/v2.13.3...v2.13.4 +[2.13.3]: https://github.com/MetaMask/metamask-extension/compare/v2.13.2...v2.13.3 +[2.13.2]: https://github.com/MetaMask/metamask-extension/compare/v2.13.1...v2.13.2 +[2.13.1]: https://github.com/MetaMask/metamask-extension/compare/v2.13.0...v2.13.1 +[2.13.0]: https://github.com/MetaMask/metamask-extension/compare/v2.12.1...v2.13.0 +[2.12.1]: https://github.com/MetaMask/metamask-extension/compare/v2.12.0...v2.12.1 +[2.12.0]: https://github.com/MetaMask/metamask-extension/compare/v2.11.1...v2.12.0 +[2.11.1]: https://github.com/MetaMask/metamask-extension/compare/v2.11.0...v2.11.1 +[2.11.0]: https://github.com/MetaMask/metamask-extension/compare/v2.10.2...v2.11.0 +[2.10.2]: https://github.com/MetaMask/metamask-extension/compare/v2.10.1...v2.10.2 +[2.10.1]: https://github.com/MetaMask/metamask-extension/compare/v2.10.0...v2.10.1 +[2.10.0]: https://github.com/MetaMask/metamask-extension/compare/v2.9.2...v2.10.0 +[2.9.2]: https://github.com/MetaMask/metamask-extension/compare/v2.9.1...v2.9.2 +[2.9.1]: https://github.com/MetaMask/metamask-extension/compare/v2.9.0...v2.9.1 +[2.9.0]: https://github.com/MetaMask/metamask-extension/compare/v2.8.0...v2.9.0 +[2.8.0]: https://github.com/MetaMask/metamask-extension/compare/v2.7.3...v2.8.0 +[2.7.3]: https://github.com/MetaMask/metamask-extension/compare/v2.7.2...v2.7.3 +[2.7.2]: https://github.com/MetaMask/metamask-extension/compare/v2.7.1...v2.7.2 +[2.7.1]: https://github.com/MetaMask/metamask-extension/compare/v2.7.0...v2.7.1 +[2.7.0]: https://github.com/MetaMask/metamask-extension/compare/v2.6.2...v2.7.0 +[2.6.2]: https://github.com/MetaMask/metamask-extension/compare/v2.6.1...v2.6.2 +[2.6.1]: https://github.com/MetaMask/metamask-extension/compare/v2.6.0...v2.6.1 +[2.6.0]: https://github.com/MetaMask/metamask-extension/compare/v2.5.0...v2.6.0 +[2.5.0]: https://github.com/MetaMask/metamask-extension/compare/v2.4.5...v2.5.0 +[2.4.5]: https://github.com/MetaMask/metamask-extension/compare/v2.4.4...v2.4.5 +[2.4.4]: https://github.com/MetaMask/metamask-extension/compare/v2.4.3...v2.4.4 +[2.4.3]: https://github.com/MetaMask/metamask-extension/compare/v2.4.2...v2.4.3 +[2.4.2]: https://github.com/MetaMask/metamask-extension/compare/v2.4.0...v2.4.2 +[2.4.0]: https://github.com/MetaMask/metamask-extension/compare/v2.3.1...v2.4.0 +[2.3.1]: https://github.com/MetaMask/metamask-extension/compare/v2.3.0...v2.3.1 +[2.3.0]: https://github.com/MetaMask/metamask-extension/compare/v2.2.0...v2.3.0 +[2.2.0]: https://github.com/MetaMask/metamask-extension/compare/v2.1.0...v2.2.0 +[2.1.0]: https://github.com/MetaMask/metamask-extension/compare/v2.0.0...v2.1.0 +[2.0.0]: https://github.com/MetaMask/metamask-extension/compare/v1.8.4...v2.0.0 +[1.8.4]: https://github.com/MetaMask/metamask-extension/compare/v1.8.3...v1.8.4 +[1.8.3]: https://github.com/MetaMask/metamask-extension/compare/v1.8.2...v1.8.3 +[1.8.2]: https://github.com/MetaMask/metamask-extension/compare/v1.8.1...v1.8.2 +[1.8.1]: https://github.com/MetaMask/metamask-extension/compare/v1.8.0...v1.8.1 +[1.8.0]: https://github.com/MetaMask/metamask-extension/compare/v1.7.0...v1.8.0 +[1.7.0]: https://github.com/MetaMask/metamask-extension/compare/v1.6.0...v1.7.0 +[1.6.0]: https://github.com/MetaMask/metamask-extension/compare/v1.5.1...v1.6.0 +[1.5.1]: https://github.com/MetaMask/metamask-extension/compare/v1.5.0...v1.5.1 +[1.5.0]: https://github.com/MetaMask/metamask-extension/compare/v1.4.0...v1.5.0 +[1.4.0]: https://github.com/MetaMask/metamask-extension/compare/v1.3.2...v1.4.0 +[1.3.2]: https://github.com/MetaMask/metamask-extension/compare/v1.0.0...v1.3.2 +[1.0.0]: https://github.com/MetaMask/metamask-extension/compare/v0.14.0...v1.0.0 +[0.14.0]: https://github.com/MetaMask/metamask-extension/releases/tag/v0.14.0 diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index a52aee23f..ad11dad20 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "ቋት አስመጣ" }, - "importYourExisting": { - "message": "ባለ 12 ቃል የዘር ሐረግን በመጠቀም ነባር ቋትዎን ያስመጡ" - }, "imported": { "message": "ከውጭ የመጣ", "description": "status showing that an account has been fully loaded into the keyring" @@ -892,9 +889,6 @@ "secretBackupPhraseWarning": { "message": "ማስጠንቀቂያ፡ የመጠባበቂያ ምዕራፍዎን በጭራሽ አይግለጹ። ይህን ሐረገ የያዘ ማንኛውም ሰው የእርስዎን Ether እስከወዲያኛው ሊወስደው ይችላል።" }, - "secretPhrase": { - "message": "ካዝናዎን ወደነበረበት ለመመለስ ሚስጥራዊ ባለ አስራ ሁለት ቃል ሐረግዎን ያስገቡ።" - }, "securityAndPrivacy": { "message": "ደህንነት እና ግላዊነት" }, @@ -934,9 +928,6 @@ "sendAmount": { "message": "መጠኑን ላክ" }, - "sendETH": { - "message": "ETH ላክ" - }, "sendTokens": { "message": "ተለዋጭ ስሞችን ላክ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 9a04e15d8..82a2efd8c 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "استيراد المحفظة" }, - "importYourExisting": { - "message": "قم باستيراد محفظتك الحالية باستخدام جملة بذرية مكونة من 12 كلمة" - }, "imported": { "message": "المستوردة", "description": "status showing that an account has been fully loaded into the keyring" @@ -888,9 +885,6 @@ "secretBackupPhraseWarning": { "message": "تحذير: لا تكشف مطلقاً عن عبارة الدعم الخاصة بك. يمكن لأي شخص بهذه العبارة أن يستحوذ على الأثير الخاص بك إلى الأبد." }, - "secretPhrase": { - "message": "أدخل جملتك السرية المكونة من اثني عشر كلمة هنا لاستعادة خزنتك." - }, "securityAndPrivacy": { "message": "الأمن والخصوصية" }, @@ -930,9 +924,6 @@ "sendAmount": { "message": "إرسال المبلغ" }, - "sendETH": { - "message": "إرسال عملة إيثيريوم" - }, "sendTokens": { "message": "إرسال عملات رمزية" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 26abfe956..119d630f3 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "Импортиране на портфейла" }, - "importYourExisting": { - "message": "Импортирайте съществуващия си портфейл с помощта на фраза зародиш с 12 думи" - }, "imported": { "message": "Импортирани", "description": "status showing that an account has been fully loaded into the keyring" @@ -891,9 +888,6 @@ "secretBackupPhraseWarning": { "message": "ВНИМАНИЕ: Никога не разкривайте резервната си фраза. Всеки с тази фраза може да вземе вашия етер завинаги." }, - "secretPhrase": { - "message": "Въведете своята тайна фраза от дванадесет думи тук, за да възстановите портфейла си." - }, "securityAndPrivacy": { "message": "Сигурност и поверителност" }, @@ -933,9 +927,6 @@ "sendAmount": { "message": "Изпратете сумата" }, - "sendETH": { - "message": "Изпрати ETH" - }, "sendTokens": { "message": "Изпращане на жетони" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 2ef360f19..8069ae92d 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "ওয়ালেট আমদানি করুন" }, - "importYourExisting": { - "message": "একটি 12 শব্দের সীড ফ্রেজ ব্যবহার করে আপনার বিদ্যমান ওয়ালেট আমদানি করুন" - }, "imported": { "message": "আমদানিকৃত", "description": "status showing that an account has been fully loaded into the keyring" @@ -895,9 +892,6 @@ "secretBackupPhraseWarning": { "message": "সতর্কতা: কখনও আপনার ব্যাকআপ ফ্রেজ প্রকাশ করবেন না। এই ফ্রেজ পেয়ে গেলে যে কেউ আপনার ইথার চিরকালের জন্য নিয়ে নিতে পারবেন।" }, - "secretPhrase": { - "message": "আপনার ভল্ট রিস্টোর করতে এখানে আপনার গোপন বারো শব্দের ফ্রেজ লিখুন।" - }, "securityAndPrivacy": { "message": "নিরাপত্তা এবং গোপনীয়তা" }, @@ -937,9 +931,6 @@ "sendAmount": { "message": "পাঠানো অর্থরাশি" }, - "sendETH": { - "message": "ETH পাঠান" - }, "sendTokens": { "message": "টোকেনগুলি পাঠান" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index fda999248..9e5030e14 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -492,9 +492,6 @@ "importWallet": { "message": "Importar Moneder" }, - "importYourExisting": { - "message": "Importa el teu moneder utilitzant la frase de recuperació de 12 paraules" - }, "imported": { "message": "Importats", "description": "status showing that an account has been fully loaded into the keyring" @@ -873,9 +870,6 @@ "secretBackupPhraseWarning": { "message": "ATENCIÓ: No divulguis mai la teva frase de recuperació. Qualsevol amb aquesta frase pot utilitzar el teu Ether per sempre." }, - "secretPhrase": { - "message": "Introdueix aquí la teva frase secreta de dotze paraules per a recuperar la teva caixa forta." - }, "securityAndPrivacy": { "message": "Seguretat i privacitat" }, @@ -915,9 +909,6 @@ "sendAmount": { "message": "Enviar Quantitat" }, - "sendETH": { - "message": "Envia ETH" - }, "sendTokens": { "message": "Enviar Fitxes" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 3ccfc0089..7ac69c33e 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -344,9 +344,6 @@ "searchTokens": { "message": "Hledat tokeny" }, - "secretPhrase": { - "message": "Zadejte svých 12 slov tajné fráze k obnovení trezoru." - }, "seedPhraseReq": { "message": "klíčové fráze mají 12 slov" }, @@ -356,9 +353,6 @@ "send": { "message": "Odeslat" }, - "sendETH": { - "message": "Odeslat ETH" - }, "sendTokens": { "message": "Odeslat tokeny" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 9378cbbbd..8a2d0abdf 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -498,9 +498,6 @@ "importWallet": { "message": "Importér pung" }, - "importYourExisting": { - "message": "Importér din eksisterende pung med en 12-ords seedfrase" - }, "imported": { "message": "Importeret", "description": "status showing that an account has been fully loaded into the keyring" @@ -876,9 +873,6 @@ "secretBackupPhraseWarning": { "message": "ADVARSEL: Afslør aldrig din backup-frase. Enhver med denne frase kan tage din Ether for altid." }, - "secretPhrase": { - "message": "Indtast din hemmelige tolv ord lange sætning for at gendanne din vault." - }, "securityAndPrivacy": { "message": "Sikkerhed & Privatliv" }, @@ -915,9 +909,6 @@ "sendAmount": { "message": "Send Beløb" }, - "sendETH": { - "message": "Vælg ETH" - }, "sendTokens": { "message": "Send tokens" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index f90cd388e..67ffd7a1d 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -493,9 +493,6 @@ "importWallet": { "message": "Wallet importieren" }, - "importYourExisting": { - "message": "Importieren Sie Ihre bestehende Wallet mit einem 12-Wort-Seed-Schlüssel" - }, "imported": { "message": "Importiert", "description": "status showing that an account has been fully loaded into the keyring" @@ -864,9 +861,6 @@ "secretBackupPhraseWarning": { "message": "WARNUNG: Legen Sie niemals Ihre Sicherungsphrase offen. Mit dieser Phrase kann sich jeder Ihr Ether für immer aneignen." }, - "secretPhrase": { - "message": "Gib die 12 Wörter deiner geheimem Wörterfolge ein um deinen Vault wiederherzustellen." - }, "securityAndPrivacy": { "message": "Sicherheit & Datenschutz" }, @@ -906,9 +900,6 @@ "sendAmount": { "message": "Betrag senden" }, - "sendETH": { - "message": "ETH senden" - }, "sendTokens": { "message": "Token senden" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 95df989d9..48bac1c13 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -502,9 +502,6 @@ "importWallet": { "message": "Εισαγωγή Πορτοφολιού" }, - "importYourExisting": { - "message": "Εισαγάγετε το υπάρχον πορτοφόλι σας χρησιμοποιώντας μια φράση φύτρου 12 λέξεων" - }, "imported": { "message": "Έγινε εισαγωγή", "description": "status showing that an account has been fully loaded into the keyring" @@ -892,9 +889,6 @@ "secretBackupPhraseWarning": { "message": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Ποτέ μην αποκαλύπτετε την εφεδρική φράση. Όποιος έχει αυτή τη φράση μπορεί να πάρει τα Ether σας για πάντα." }, - "secretPhrase": { - "message": "Εισαγάγετε εδώ τη μυστική φράση δώδεκα λέξεων για να επαναφέρετε το χρηματοκιβώτιό σας." - }, "securityAndPrivacy": { "message": "Ασφάλεια και Απόρρητο" }, @@ -934,9 +928,6 @@ "sendAmount": { "message": "Αποστολή Ποσού" }, - "sendETH": { - "message": "Στείλτε ETH" - }, "sendTokens": { "message": "Στείλτε Tokens" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 70e25f644..b9501b008 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -869,7 +869,7 @@ "message": "Import wallet" }, "importYourExisting": { - "message": "Import your existing wallet using a 12 word seed phrase" + "message": "Import your existing wallet using a seed phrase" }, "imported": { "message": "Imported", @@ -1060,7 +1060,7 @@ "message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem." }, "mismatchedChain": { - "message": "This network details for this Chain ID do not match our records. We recommend that you $1 before proceeding.", + "message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.", "description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key" }, "mismatchedChainLinkText": { @@ -1451,7 +1451,7 @@ "message": "WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever." }, "secretPhrase": { - "message": "Enter your secret twelve word phrase here to restore your vault." + "message": "Enter your secret phrase here to restore your vault." }, "securityAndPrivacy": { "message": "Security & Privacy" @@ -1507,9 +1507,6 @@ "sendAmount": { "message": "Send Amount" }, - "sendETH": { - "message": "Send ETH" - }, "sendSpecifiedTokens": { "message": "Send $1", "description": "Symbol of the specified token" @@ -2218,6 +2215,9 @@ "walletSeed": { "message": "Seed phrase" }, + "walletSeedRestore": { + "message": "Wallet Seed" + }, "web3ShimUsageNotification": { "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", "description": "$1 is a clickable link." diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 4a160692e..a171d476b 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -796,9 +796,6 @@ "importWallet": { "message": "Importar Monedero" }, - "importYourExisting": { - "message": "Importa tu monedero existente usando la frase semilla de 12 palabras" - }, "imported": { "message": "Importado", "description": "status showing that an account has been fully loaded into the keyring" @@ -1355,9 +1352,6 @@ "secretBackupPhraseWarning": { "message": "ADVERTENCIA: Nunca revele su frase de respaldo. Cualquiera con esta frase puede tomar su Ether para siempre." }, - "secretPhrase": { - "message": "Ingresa tu frase de doce (12) palabras para restaurar tu bóveda." - }, "securityAndPrivacy": { "message": "Seguridad y Privacidad" }, @@ -1409,9 +1403,6 @@ "sendAmount": { "message": "Enviar cantidad" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendSpecifiedTokens": { "message": "Enviar $1", "description": "Symbol of the specified token" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index f7b9d815f..c7340c869 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -796,9 +796,6 @@ "importWallet": { "message": "Importar billetera" }, - "importYourExisting": { - "message": "Importa tu billetera existente con una frase semilla de 12 palabras" - }, "imported": { "message": "Importado", "description": "status showing that an account has been fully loaded into the keyring" @@ -1355,9 +1352,6 @@ "secretBackupPhraseWarning": { "message": "ADVERTENCIA: Nunca reveles tu frase de respaldo. Cualquier persona que tenga acceso a esta frase puede llevarse tus Ether permanentemente." }, - "secretPhrase": { - "message": "Ingresa tu frase secreta de doce palabras para restaurar tu almacén." - }, "securityAndPrivacy": { "message": "Seguridad y privacidad" }, @@ -1409,9 +1403,6 @@ "sendAmount": { "message": "Enviar monto" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendSpecifiedTokens": { "message": "Enviar $1", "description": "Symbol of the specified token" diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index e2cfe7aa3..a005f48e5 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "Importige rahakott" }, - "importYourExisting": { - "message": "Importige 12-sõnalise seemnefraasi abil olemasolev rahakott" - }, "imported": { "message": "Imporditud", "description": "status showing that an account has been fully loaded into the keyring" @@ -885,9 +882,6 @@ "secretBackupPhraseWarning": { "message": "HOIATUS! Ärge avaldage kunagi oma varundusfraasi. Selle fraasiga on võimalik teie eeter igaveseks ära võtta." }, - "secretPhrase": { - "message": "Sisestage hoidla taastamiseks oma salajane 12-sõnaline fraas." - }, "securityAndPrivacy": { "message": "Turvalisus ja privaatsus" }, @@ -927,9 +921,6 @@ "sendAmount": { "message": "Saatke kogus" }, - "sendETH": { - "message": "Saada ETH" - }, "sendTokens": { "message": "Saada lube" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index b8bffe956..a6ae259fe 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "وارد سازی کیف" }, - "importYourExisting": { - "message": "کیف موجود تان را با استفاده از عبارت رمزیاب 12 کلمه یی وارد کنید" - }, "imported": { "message": "وارد شده", "description": "status showing that an account has been fully loaded into the keyring" @@ -895,9 +892,6 @@ "secretBackupPhraseWarning": { "message": "هشدار: هرگز عبارت پشتیبان تان را به کسی فاش نسازید. هرکسیکه این عبارت را داشته باشد ایتر شما را برای همیشه خواهد گرفت." }, - "secretPhrase": { - "message": "جهت بازیابی خزانه تان عبارت دوازده کلمه یی تان را اینجا وارد کنید." - }, "securityAndPrivacy": { "message": "امنیت و حریم خصوصی" }, @@ -937,9 +931,6 @@ "sendAmount": { "message": "ارسال مبلغ" }, - "sendETH": { - "message": "ارسال ETH" - }, "sendTokens": { "message": "رمزیاب ها را ارسال کنید" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index fa5bd1125..429f23fc0 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "Tuo kukkaro" }, - "importYourExisting": { - "message": "Tuo nykyinen lompakkosi käyttäen 12 sanan \n siemenlausetta" - }, "imported": { "message": "Tuotu", "description": "status showing that an account has been fully loaded into the keyring" @@ -892,9 +889,6 @@ "secretBackupPhraseWarning": { "message": "VAROITUS: älä koskaan kerro varmuuskopiolausettasi kenellekään. Kuka tahansa tämän lauseen omaava voi napata Etherisi pysyvästi." }, - "secretPhrase": { - "message": "Palauta holvisi syöttämällä tähän salainen kahdentoista sanan tekstisi." - }, "securityAndPrivacy": { "message": "Turva & yksityisyys" }, @@ -934,9 +928,6 @@ "sendAmount": { "message": "Lähetä summa" }, - "sendETH": { - "message": "Lähetä ETH:iä" - }, "sendTokens": { "message": "Lähetä tietueita" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 473331a30..24ee12687 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -462,9 +462,6 @@ "importWallet": { "message": "Mag-import ng Wallet" }, - "importYourExisting": { - "message": "I-import ang kasalukuyan mong wallet gamit ang isang seed phrase na may 12 salita" - }, "imported": { "message": "Na-import", "description": "status showing that an account has been fully loaded into the keyring" @@ -807,9 +804,6 @@ "secretBackupPhraseWarning": { "message": "BABALA: Huwag ibunyag ang iyong backup phrase. Mananakaw ng kahit sinong may ganitong parirala ang iyong Ether at hindi na ito maibabalik." }, - "secretPhrase": { - "message": "Ilagay ang iyong lihim na pariralang may labindalawang salita para ma-restore ang iyong vault." - }, "securityAndPrivacy": { "message": "Seguridad at Privacy" }, @@ -849,9 +843,6 @@ "sendAmount": { "message": "Magpadala ng Halaga" }, - "sendETH": { - "message": "Magpadala ng ETH" - }, "sendTokens": { "message": "Magpadala ng Mga Token" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 39d44fb72..23808a1b5 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -496,9 +496,6 @@ "importWallet": { "message": "Importer le portefeuille" }, - "importYourExisting": { - "message": "Importez votre portefeuille existant à l'aide d'une phrase mnémotechnique de 12 mots" - }, "imported": { "message": "Importé", "description": "status showing that an account has been fully loaded into the keyring" @@ -877,9 +874,6 @@ "secretBackupPhraseWarning": { "message": "AVERTISSEMENT : ne révélez jamais votre phrase de sauvegarde. N'importe qui avec cette phrase peut voler votre Ether pour toujours." }, - "secretPhrase": { - "message": "Entrez vos 12 mots secrets de votre phrase Seed pour restaurer votre coffre." - }, "securityAndPrivacy": { "message": "Sécurité et confidentialité" }, @@ -919,9 +913,6 @@ "sendAmount": { "message": "Envoyer le montant" }, - "sendETH": { - "message": "Envoyer des ETH" - }, "sendTokens": { "message": "Envoyer des jetons" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 9b426ea16..319424a46 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "ייבא ארנק" }, - "importYourExisting": { - "message": "יבא/י את הארנק הקיים שלך באמצעות seed phrase בן 12 מילים" - }, "imported": { "message": "מיובאות", "description": "status showing that an account has been fully loaded into the keyring" @@ -889,9 +886,6 @@ "secretBackupPhraseWarning": { "message": "אזהרה: לעולם אין לחשוף את צירוף הגיבוי שלך. כל מי שברשותו צירוף זה יכול לקחת את האת'ר שלך לצמיתות." }, - "secretPhrase": { - "message": "הזנ/י את הצירוף הסודי שלך של שתים-עשרה המילים כאן כדי לשחזר את הכספת שלך." - }, "securityAndPrivacy": { "message": "אבטחה ופרטיות" }, @@ -931,9 +925,6 @@ "sendAmount": { "message": "שלח סכום" }, - "sendETH": { - "message": "שלח/י ETH" - }, "sendTokens": { "message": "שלח טוקנים" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9e8d76544..24411537c 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "वॉलेट आयात करें" }, - "importYourExisting": { - "message": "12 शब्द के सीडफ्रेज़ का उपयोग करके अपने मौजूदा वॉलेट को आयात करें" - }, "imported": { "message": "आयातित", "description": "status showing that an account has been fully loaded into the keyring" @@ -1346,9 +1343,6 @@ "secretBackupPhraseWarning": { "message": "चेतावनी: कभी भी अपने बैकअप वाक्यांश का खुलासा न करें। इस वाक्यांश के साथ कोई भी आपके Ether को हमेशा के लिए ले सकता है।" }, - "secretPhrase": { - "message": "अपनी तिजोरी को पुनर्स्थापित करने के लिए अपने गुप्त बारह शब्द वाक्यांश को यहाँ दर्ज करें।" - }, "securityAndPrivacy": { "message": "सुरक्षा और गोपनीयता" }, @@ -1400,9 +1394,6 @@ "sendAmount": { "message": "राशि भेजें" }, - "sendETH": { - "message": "ETH भेजें" - }, "sendSpecifiedTokens": { "message": "$1 भेजें", "description": "Symbol of the specified token" diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 0dac2b285..9963ac5ec 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -324,9 +324,6 @@ "search": { "message": "खोज करें" }, - "secretPhrase": { - "message": "अपनी गुप्त बारह शब्द वाक्यांश यहाँ अपनी तिजोरी को पुनर्स्थापित करने के लिए दर्ज करें।" - }, "seedPhraseReq": { "message": "बीज वाक्यांश 12 शब्द लंबा हैं" }, @@ -336,9 +333,6 @@ "send": { "message": "भेजें" }, - "sendETH": { - "message": "भेजें ETH" - }, "sendTokens": { "message": "भेजें टोकन" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 17c35b6b7..91f88331a 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "Uvezi novčanik" }, - "importYourExisting": { - "message": "Uvezite svoj postojeći novčanik početnom rečenicom od 12 riječi" - }, "imported": { "message": "Uvezeno", "description": "status showing that an account has been fully loaded into the keyring" @@ -888,9 +885,6 @@ "secretBackupPhraseWarning": { "message": "UPOZORENJE: nikada ne otkrivajte svoju alternativnu rečenicu. Bilo tko ovom rečenicom može zauvijek preuzeti vaš Ether." }, - "secretPhrase": { - "message": "Ovdje upišite svoju tajnu rečenicu od 12 riječi kako biste obnovili svoj sef." - }, "securityAndPrivacy": { "message": "Sigurnost i privatnost" }, @@ -930,9 +924,6 @@ "sendAmount": { "message": "Odaberi iznos" }, - "sendETH": { - "message": "Pošalji ETH" - }, "sendTokens": { "message": "Pošalji tokene" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 40d8d73bb..0c427dbb6 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -561,9 +561,6 @@ "searchTokens": { "message": "Rechèch Tokens" }, - "secretPhrase": { - "message": "Antre fraz sekrè douz mo ou a pou w restore kòf ou a." - }, "seedPhraseReq": { "message": "Seed fraz yo se 12 long mo" }, @@ -585,9 +582,6 @@ "send": { "message": "Voye" }, - "sendETH": { - "message": "Voye ETH" - }, "sendTokens": { "message": "Voye Tokens" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index ce8df8e5d..6cd093e4d 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "Pénztárca importálása" }, - "importYourExisting": { - "message": "Importálja meglévő pénztárcáját a 12 szóból álló seed mondat segítségével" - }, "imported": { "message": "Importált", "description": "status showing that an account has been fully loaded into the keyring" @@ -888,9 +885,6 @@ "secretBackupPhraseWarning": { "message": "FIGYELEM: Senkise se adja meg a biztonsági szakaszát. Ennek tulajdonosa örökre elviheti Ether-jeit." }, - "secretPhrase": { - "message": "Tárolód helyreállításához írd be titkos tizenkét szavas szókapcsolatodat ide." - }, "securityAndPrivacy": { "message": "Biztonság és adatvédelem" }, @@ -930,9 +924,6 @@ "sendAmount": { "message": "Összeg küldése" }, - "sendETH": { - "message": "ETH küldése" - }, "sendTokens": { "message": "Token küldése" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 910bfcaff..aa23ddabe 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "Impor dompet" }, - "importYourExisting": { - "message": "Impor dompet Anda yang ada menggunakan frasa pemulihan 12 kata" - }, "imported": { "message": "Diimpor", "description": "status showing that an account has been fully loaded into the keyring" @@ -1346,9 +1343,6 @@ "secretBackupPhraseWarning": { "message": "PERINGATAN: Jangan pernah ungkapkan frasa cadangan Anda. Siapa pun yang memiliki frasa ini dapat mengambil Ether Anda selamanya." }, - "secretPhrase": { - "message": "Masukkan frasa kata dua belas rahasia Anda di sini untuk memulihkan vault Anda." - }, "securityAndPrivacy": { "message": "Keamanan & Privasi" }, @@ -1400,9 +1394,6 @@ "sendAmount": { "message": "Kirim Jumlah" }, - "sendETH": { - "message": "Kirim ETH" - }, "sendSpecifiedTokens": { "message": "Kirim $1", "description": "Symbol of the specified token" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 6f591ddd9..a15720e79 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -799,9 +799,6 @@ "importWallet": { "message": "Importa Portafoglio" }, - "importYourExisting": { - "message": "Importa il tuo portafoglio esistente usando la tua frase seed a 12 parole" - }, "imported": { "message": "Importato", "description": "status showing that an account has been fully loaded into the keyring" @@ -1361,9 +1358,6 @@ "secretBackupPhraseWarning": { "message": "ATTENZIONE: Non dire mai a nessuno questa frase di backup. Chiunque con questa frase può rubare i tuoi Ether per sempre." }, - "secretPhrase": { - "message": "Inserisci la tua frase segreta di dodici parole per ripristinare la cassaforte." - }, "securityAndPrivacy": { "message": "Sicurezza & Privacy" }, @@ -1415,9 +1409,6 @@ "sendAmount": { "message": "Invia Importo" }, - "sendETH": { - "message": "Invia ETH" - }, "sendSpecifiedTokens": { "message": "Invia $1", "description": "Symbol of the specified token" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index a29480b24..e6aacbb4f 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -796,9 +796,6 @@ "importWallet": { "message": "ウォレットのインポート" }, - "importYourExisting": { - "message": "12単語のシードフレーズを使用して既存のウォレットをインポートします" - }, "imported": { "message": "インポート済", "description": "status showing that an account has been fully loaded into the keyring" @@ -1355,9 +1352,6 @@ "secretBackupPhraseWarning": { "message": "警告:シードフレーズは絶対に公開しないでください。シードフレーズを使うと、誰でもアカウントからETHを盗み出せます。" }, - "secretPhrase": { - "message": "アカウント情報を復元するには、12単語で構成されたシードフレーズを入力してください。" - }, "securityAndPrivacy": { "message": "セキュリティとプライバシー" }, @@ -1409,9 +1403,6 @@ "sendAmount": { "message": "送金額" }, - "sendETH": { - "message": "ETHの送金" - }, "sendSpecifiedTokens": { "message": "$1 を送る", "description": "Symbol of the specified token" diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index e07994172..f895c1f9a 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "ವ್ಯಾಲೆಟ್ ಅನ್ನು ಆಮದು ಮಾಡಿ" }, - "importYourExisting": { - "message": "12 ಪದದ ಸೀಡ್ ಫ್ರೇಸ್ ಅನ್ನು ಬಳಸಿಕೊಂಡು ನಿಮ್ಮ ಅಸ್ತಿತ್ವದಲ್ಲಿರುವ ವ್ಯಾಲೆಟ್ ಅನ್ನು ಆಮದು ಮಾಡಿ" - }, "imported": { "message": "ಆಮದುಮಾಡಲಾಗಿದೆ", "description": "status showing that an account has been fully loaded into the keyring" @@ -895,9 +892,6 @@ "secretBackupPhraseWarning": { "message": "ಎಚ್ಚರಿಕೆ: ನಿಮ್ಮ ಬ್ಯಾಕಪ್ ಫ್ರೇಸ್ ಅನ್ನು ಎಂದಿಗೂ ಬಹಿರಗಪಡಿಸಬೇಡಿ. ಈ ಫ್ರೇಸ್ ಅನ್ನು ಹೊಂದಿರುವ ಯಾರಾದರೂ ನಿಮ್ಮ ಎಥರ್ ಅನ್ನು ಶಾಶ್ವತವಾಗಿ ತೆಗೆದುಕೊಳ್ಳಬಹುದು." }, - "secretPhrase": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಮರುಸ್ಥಾಪಿಸಲು ನಿಮ್ಮ ರಹಸ್ಯ ಹನ್ನೆರಡು ಪದದ ಫ್ರೇಸ್ ಅನ್ನು ಇಲ್ಲಿ ನಮೂದಿಸಿ." - }, "securityAndPrivacy": { "message": "ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆ" }, @@ -937,9 +931,6 @@ "sendAmount": { "message": "ಮೊತ್ತವನ್ನು ಕಳುಹಿಸಿ" }, - "sendETH": { - "message": "ETH ಕಳುಹಿಸಿ" - }, "sendTokens": { "message": "ಟೋಕನ್‌ಗಳನ್ನು ಕಳುಹಿಸಿ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 59d1fcc42..efd307b2e 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "지갑 가져오기" }, - "importYourExisting": { - "message": "12단어 시드 구문을 사용하여 지갑 가져오기" - }, "imported": { "message": "가져옴", "description": "status showing that an account has been fully loaded into the keyring" @@ -1343,9 +1340,6 @@ "secretBackupPhraseWarning": { "message": "경고: 백업 구문은 절대로 공개하지 마세요. 이 구문이 있는 사람은 귀하의 Ether를 영원히 소유할 수 있습니다." }, - "secretPhrase": { - "message": "금고를 복구하려면 비밀 12단어 구문을 여기에 입력하세요." - }, "securityAndPrivacy": { "message": "보안 및 개인정보 보호" }, @@ -1397,9 +1391,6 @@ "sendAmount": { "message": "금액 보내기" }, - "sendETH": { - "message": "ETH 보내기" - }, "sendSpecifiedTokens": { "message": "$1 보내기", "description": "Symbol of the specified token" diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index ec9137030..09183e9a1 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "Importuoti slaptažodinę" }, - "importYourExisting": { - "message": "Importuoti turimą piniginę naudojant 12 žodžių atkūrimo frazę" - }, "imported": { "message": "Importuota", "description": "status showing that an account has been fully loaded into the keyring" @@ -895,9 +892,6 @@ "secretBackupPhraseWarning": { "message": "ĮSPĖJIMAS. Niekada neatskleiskite savo atsarginės frazės. Bet kas, žinantis šią frazę, gali visiems laikams pasiimti jūsų eterius." }, - "secretPhrase": { - "message": "Savo saugyklai atkurti įveskite slaptą dvylikos žodžių frazę." - }, "securityAndPrivacy": { "message": "Sauga ir privatumas" }, @@ -937,9 +931,6 @@ "sendAmount": { "message": "Siųsti sumą" }, - "sendETH": { - "message": "Siųsti ETH" - }, "sendTokens": { "message": "Siųsti žetonus" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index fd1b79205..dc02d1c31 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -501,9 +501,6 @@ "importWallet": { "message": "Importēt maku" }, - "importYourExisting": { - "message": "Importējiet esošo maku, izmantojot 12 vārdu atkopšanas frāzi" - }, "imported": { "message": "Importēts", "description": "status showing that an account has been fully loaded into the keyring" @@ -891,9 +888,6 @@ "secretBackupPhraseWarning": { "message": "BRĪDINĀJUMS! Nekādā gadījumā neizpaudiet savu rezerves frāzi. Ikviens, kam pieejama šī frāze, var uz visiem laikiem pārņemt jūsu Ether." }, - "secretPhrase": { - "message": "Ievadiet šeit slepeno divpadsmit vārdu frāzi, lai atjaunotu savu seifu." - }, "securityAndPrivacy": { "message": "Drošība un konfidencialitāte" }, @@ -933,9 +927,6 @@ "sendAmount": { "message": "Nosūtītā summa" }, - "sendETH": { - "message": "Sūtīt ETH" - }, "sendTokens": { "message": "Nosūtīt marķierus" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 355a2ca28..3bbd5a019 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -491,9 +491,6 @@ "importWallet": { "message": "Import Dompet" }, - "importYourExisting": { - "message": "Import dompet sedia ada anda menggunakan frasa benih 12 perkataan" - }, "imported": { "message": "Diimport", "description": "status showing that an account has been fully loaded into the keyring" @@ -872,9 +869,6 @@ "secretBackupPhraseWarning": { "message": "AMARAN: Jangan sesekali dedahkan frasa sandaran anda. Sesiapa yang memperoleh frasa ini boleh mengambil Ether anda selama-lamanya." }, - "secretPhrase": { - "message": "Masukkan ungkapan rahsia dua belas perkataan di sini untuk memulihkan kekubah anda." - }, "securityAndPrivacy": { "message": "Keselamatan & Privasi" }, @@ -914,9 +908,6 @@ "sendAmount": { "message": "Hantar Amaun" }, - "sendETH": { - "message": "Hantar ETH" - }, "sendTokens": { "message": "Hantar Token" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 1c20ec2f5..f25182de5 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -311,9 +311,6 @@ "search": { "message": "Zoeken" }, - "secretPhrase": { - "message": "Voer hier je geheime twaalfwoordfrase in om je kluis te herstellen." - }, "seedPhraseReq": { "message": "Back-up woorden zijn 12 woorden lang" }, @@ -323,9 +320,6 @@ "send": { "message": "Sturen" }, - "sendETH": { - "message": "Verzend ETH" - }, "sendTokens": { "message": "Stuur tokens" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 8489dff4f..5dd774f1a 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -492,9 +492,6 @@ "importWallet": { "message": "Importér lommebok " }, - "importYourExisting": { - "message": "Importer din eksisterende lommebok ved å bruk en tolvords seed-frase." - }, "imported": { "message": "Importert", "description": "status showing that an account has been fully loaded into the keyring" @@ -879,9 +876,6 @@ "secretBackupPhraseWarning": { "message": "ADVARSEL: Du må aldri avsløre gjenopprettingsfrasen din. Alle som har denne frasen kan ta fra deg Etheren din for alltid." }, - "secretPhrase": { - "message": "Skriv inn den tolv ord lange frasen her for å gjenopprette hvelvet ditt. " - }, "securityAndPrivacy": { "message": "Sikkerhet og personvern" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 4a4c717a8..52817a88b 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "Importuj portfel" }, - "importYourExisting": { - "message": "Zaimportuj istniejący portfel, wprowadzając 12 słów frazy seed" - }, "imported": { "message": "Zaimportowane", "description": "status showing that an account has been fully loaded into the keyring" @@ -889,9 +886,6 @@ "secretBackupPhraseWarning": { "message": "OSTRZEŻENIE: Nigdy nie ujawniaj swojej frazy zapasowej. Każdy, kto pozna tę frazę, może bezpowrotnie odebrać Ci kryptowalutę Ether." }, - "secretPhrase": { - "message": "Żeby otworzyć schowek, wpisz tutaj swoją frazę dwunastu słów." - }, "securityAndPrivacy": { "message": "Bezpieczeństwo i prywatność" }, @@ -931,9 +925,6 @@ "sendAmount": { "message": "Wyślij kwotę" }, - "sendETH": { - "message": "Wyślij ETH" - }, "sendTokens": { "message": "Wyślij tokeny" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index ca1cca50f..b21e04ef8 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -321,9 +321,6 @@ "search": { "message": "Procurar" }, - "secretPhrase": { - "message": "Introduza a sua frase secreta de 12 palavras para recuperar o seu ." - }, "seedPhraseReq": { "message": "seed phrases are 12 words long" }, @@ -333,9 +330,6 @@ "send": { "message": "Enviar" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendTokens": { "message": "Enviar Tokens" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 5aac3e78b..6f6bba07b 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -499,9 +499,6 @@ "importWallet": { "message": "Importar Carteira" }, - "importYourExisting": { - "message": "Importe sua carteira existente usando uma frase-semente de 12 palavras" - }, "imported": { "message": "Importado", "description": "status showing that an account has been fully loaded into the keyring" @@ -883,9 +880,6 @@ "secretBackupPhraseWarning": { "message": "ATENÇÃO: Nunca revele sua frase de backup a ninguém. Qualquer pessoa com essa frase pode obter seu Ether para sempre." }, - "secretPhrase": { - "message": "Digite sua frase secreta de doze palavras aqui para restaurar seu cofre." - }, "securityAndPrivacy": { "message": "Segurança & Privacidade" }, @@ -925,9 +919,6 @@ "sendAmount": { "message": "Enviar Quantia" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendTokens": { "message": "Enviar Tokens" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index d29f98239..3c475cd25 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -495,9 +495,6 @@ "importWallet": { "message": "Importați portofel" }, - "importYourExisting": { - "message": "Importați portofelul existent folosind o frază seed de 12 cuvinte" - }, "imported": { "message": "Importate", "description": "status showing that an account has been fully loaded into the keyring" @@ -882,9 +879,6 @@ "secretBackupPhraseWarning": { "message": "ATENȚIE: Nu dezvăluiți niciodată expresia dvs. de rezervă. Oricine are această expresie vă poate lua Ether-ul pentru totdeauna." }, - "secretPhrase": { - "message": "Introduceți aici expresia dvs. secretă din 12 cuvinte pentru a restabili seiful." - }, "securityAndPrivacy": { "message": "Securitate și confidențialitate" }, @@ -924,9 +918,6 @@ "sendAmount": { "message": "Suma trimisă" }, - "sendETH": { - "message": "Trimitere ETH" - }, "sendTokens": { "message": "Trimiteți indicative" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 6fb75c74a..c1de60e0c 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "Импортировать кошелек" }, - "importYourExisting": { - "message": "Импортируйте существующий кошелек, используя начальную фразу из 12 слов" - }, "imported": { "message": "Импортированный", "description": "status showing that an account has been fully loaded into the keyring" @@ -1346,9 +1343,6 @@ "secretBackupPhraseWarning": { "message": "ПРЕДУПРЕЖДЕНИЕ: Никогда не разглашайте резервную фразу. Любой, у кого есть эта фраза, может забрать ваш Ether навсегда." }, - "secretPhrase": { - "message": "Введите здесь свою секретную фразу из двенадцати слов, чтобы восстановить свой сейф." - }, "securityAndPrivacy": { "message": "Безопасность и конфиденциальность" }, @@ -1400,9 +1394,6 @@ "sendAmount": { "message": "Отправить сумму" }, - "sendETH": { - "message": "Отправить ETH" - }, "sendSpecifiedTokens": { "message": "Отправить $1", "description": "Symbol of the specified token" diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 3124751fb..7769ddd3b 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -492,9 +492,6 @@ "importWallet": { "message": "Importovať Peňaženku" }, - "importYourExisting": { - "message": "Importujte svoju existujúcu peňaženku pomocou 12-slovnej seed frázy" - }, "imported": { "message": "Importováno", "description": "status showing that an account has been fully loaded into the keyring" @@ -858,9 +855,6 @@ "secretBackupPhraseWarning": { "message": "UPOZORNENIE: Nikdy nezverejňujte svoju backup frázu. Každý, kto má túto frázu, môže navždy vziať váš Ether." }, - "secretPhrase": { - "message": "Zadejte svých 12 slov tajné fráze k obnovení trezoru." - }, "securityAndPrivacy": { "message": "Bezpečnosť a súkromie" }, @@ -900,9 +894,6 @@ "sendAmount": { "message": "Poslať sumu" }, - "sendETH": { - "message": "Odeslat ETH" - }, "sendTokens": { "message": "Odeslat tokeny" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index f2fc5b566..8392f0a64 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -496,9 +496,6 @@ "importWallet": { "message": "Uvozi denarnico" }, - "importYourExisting": { - "message": "Uvozite svojo obstoječo denarnico s pomočjo 12-besednega gesla seed phrase" - }, "imported": { "message": "Uvoženo", "description": "status showing that an account has been fully loaded into the keyring" @@ -877,9 +874,6 @@ "secretBackupPhraseWarning": { "message": "OPOZORILO: Nikoli nikomur ne razkrijte varnostne kopije. Kdorkoli lahko tem geslom vedno prevzame vaš Ether." }, - "secretPhrase": { - "message": "Vnesite vaših dvanajst besed za obnovitev vaših računov." - }, "securityAndPrivacy": { "message": "Varnost in zasebnost" }, @@ -919,9 +913,6 @@ "sendAmount": { "message": "Pošlji znesek" }, - "sendETH": { - "message": "Pošlji ETH" - }, "sendTokens": { "message": "Pošlji žetone" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index fcf6cf06e..b094ddd2d 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -502,9 +502,6 @@ "importWallet": { "message": "Uvezite novčanik" }, - "importYourExisting": { - "message": "Uvezite vaš postojeći novčanik koristeći seed frazu sa 12 reči" - }, "imported": { "message": "Увезени", "description": "status showing that an account has been fully loaded into the keyring" @@ -886,9 +883,6 @@ "secretBackupPhraseWarning": { "message": "UPOZORENJE: Nikada ne otkrivajte svoju rezervnu frazu. Svako sa ovom frazom može zauvek da Vam uzme Vaš Ether." }, - "secretPhrase": { - "message": "Unesite ovde svoj tajni izraz od dvanaest reči kako biste povratili svoj trezor." - }, "securityAndPrivacy": { "message": "Bezbednost i privatnost" }, @@ -928,9 +922,6 @@ "sendAmount": { "message": "Pošaljite iznos" }, - "sendETH": { - "message": "Pošalji ETH" - }, "sendTokens": { "message": "Pošalji tokene" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index f961555dc..c7af7fdde 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -495,9 +495,6 @@ "importWallet": { "message": "Importera plånbok" }, - "importYourExisting": { - "message": "Importera din existerande plånbok med hjälp av en 12 ord lång seedfras" - }, "imported": { "message": "Importerade", "description": "status showing that an account has been fully loaded into the keyring" @@ -879,9 +876,6 @@ "secretBackupPhraseWarning": { "message": "VARNING: avslöja aldrig din backup-fras. Någon som känner till denna fras kan ta dina Ether för alltid." }, - "secretPhrase": { - "message": "Ange din tolv ord långa hemliga fras här för att återställa ditt valv." - }, "securityAndPrivacy": { "message": "Säkerhet och integritet" }, @@ -921,9 +915,6 @@ "sendAmount": { "message": "Skicka belopp" }, - "sendETH": { - "message": "Skicka ETH" - }, "sendTokens": { "message": "Skicka tokens" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 3176cc28d..a4451aee5 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -492,9 +492,6 @@ "importWallet": { "message": "Hamisha Waleti" }, - "importYourExisting": { - "message": "Hamisha waleti iliyopo kwa kutumia kirai kianzio cha maneno 12" - }, "imported": { "message": "Zilizoingizwa", "description": "status showing that an account has been fully loaded into the keyring" @@ -873,9 +870,6 @@ "secretBackupPhraseWarning": { "message": "ONYO: Kamwe usiweke wazi kirai chako cha hifadhi mbadala. Mtu yeyote mwenye kirai hiki anaweza kuchukua Ether yako daima." }, - "secretPhrase": { - "message": "Ingiza hapa kirai chako cha siri cha maneno kumi na mawili ili urejeshe vault yako." - }, "securityAndPrivacy": { "message": "Ulinzi na Faragha" }, @@ -915,9 +909,6 @@ "sendAmount": { "message": "Tuma Kiasi" }, - "sendETH": { - "message": "Tuma ETH" - }, "sendTokens": { "message": "Tuma Vianzio" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 09b0b87bb..92e063943 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -426,9 +426,6 @@ "searchTokens": { "message": "தேடல் டோக்கன்ஸ்" }, - "secretPhrase": { - "message": "உங்கள் பெட்டகத்தை மீட்டெடுப்பதற்காக இங்கே உங்கள் ரகசிய பன்னிரண்டு வார்த்தை சொற்றொடரை உள்ளிடவும்." - }, "seedPhraseReq": { "message": "விதை வாக்கியங்கள் 12 வார்த்தைகள் நீண்டவை" }, @@ -438,9 +435,6 @@ "send": { "message": "அனுப்பு" }, - "sendETH": { - "message": "ETH ஐ அனுப்பு" - }, "sendTokens": { "message": "டோக்கன்களை அனுப்பவும்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index b0df95826..aba919b3f 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -420,9 +420,6 @@ "search": { "message": "ค้นหา" }, - "secretPhrase": { - "message": "ป้อนกลุ่มคำสิบสองคำเพื่อกู้คืนตู้เซฟของคุณ" - }, "seedPhraseReq": { "message": "กลุ่มคำชีดมีความยาว 12 คำ" }, @@ -441,9 +438,6 @@ "sendAmount": { "message": "ส่งจำนวนเงินนี้" }, - "sendETH": { - "message": "ส่งอีเธอร์" - }, "sendTokens": { "message": "ส่งโทเค็น" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 22af3556c..b53205cb8 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "I-import ang wallet" }, - "importYourExisting": { - "message": "I-import ang iyong kasalukuyang wallet gamit ang 12 salita na seed phrase" - }, "imported": { "message": "Na-import", "description": "status showing that an account has been fully loaded into the keyring" @@ -1343,9 +1340,6 @@ "secretBackupPhraseWarning": { "message": "BABALA: Huwag kailanman ipaalam ang iyong phrase sa pag-back up. Ang sinumang may phrase na ito ay maaaring angkinin ang iyong Ether." }, - "secretPhrase": { - "message": "Ilagay ang iyong labindalawang lihim na phrase dito para ma-restore ang iyong vault." - }, "securityAndPrivacy": { "message": "Seguridad at Pagkapribado" }, @@ -1397,9 +1391,6 @@ "sendAmount": { "message": "Halaga ng Ipapadala" }, - "sendETH": { - "message": "Magpadala ng ETH" - }, "sendSpecifiedTokens": { "message": "Magpadala ng $1", "description": "Symbol of the specified token" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 4b71110d0..964f8bb1e 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -366,9 +366,6 @@ "searchTokens": { "message": "Jeton ara" }, - "secretPhrase": { - "message": "Kasanızı geri getirmek için gizli 12 kelimelik ifadenizi giriniz." - }, "seedPhraseReq": { "message": "Kaynak ifadeleri 12 kelimedir." }, @@ -378,9 +375,6 @@ "send": { "message": "Gönder" }, - "sendETH": { - "message": "ETH Gönder" - }, "sendTokens": { "message": "Jeton Gönder" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 8f499bc2f..789e3c64c 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -505,9 +505,6 @@ "importWallet": { "message": "Імпортувати гаманець" }, - "importYourExisting": { - "message": "Імпортуйте ваш гаманець, що існує, використовуючи початкову фразу з 12 слів" - }, "imported": { "message": "Імпортовано", "description": "status showing that an account has been fully loaded into the keyring" @@ -895,9 +892,6 @@ "secretBackupPhraseWarning": { "message": "ЗАСТЕРЕЖЕННЯ: Ніколи не розголошуйте вашу резервну фразу. Будь-хто з цією фразою зможе забрати ваш Ether назавжди." }, - "secretPhrase": { - "message": "Введіть секретну фразу з дванадцяти слів, щоб відновити своє сховище." - }, "securityAndPrivacy": { "message": "Безпека й конфіденційність" }, @@ -937,9 +931,6 @@ "sendAmount": { "message": "Надіслати суму" }, - "sendETH": { - "message": "Надіслати ETH" - }, "sendTokens": { "message": "Надіслати токени" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index e35becf7f..81c5efc03 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -787,9 +787,6 @@ "importWallet": { "message": "Nhập ví" }, - "importYourExisting": { - "message": "Nhập ví hiện có của bạn bằng cụm mật khẩu gốc gồm 12 từ" - }, "imported": { "message": "Đã nhập", "description": "status showing that an account has been fully loaded into the keyring" @@ -1346,9 +1343,6 @@ "secretBackupPhraseWarning": { "message": "CẢNH BÁO: Tuyệt đối không để lộ cụm mật khẩu sao lưu của bạn. Bất kỳ ai có cụm mật khẩu này cũng có thể lấy Ether của bạn vĩnh viễn." }, - "secretPhrase": { - "message": "Nhập cụm mật khẩu bí mật gồm 12 từ vào đây để khôi phục két của bạn." - }, "securityAndPrivacy": { "message": "Bảo mật và quyền riêng tư" }, @@ -1400,9 +1394,6 @@ "sendAmount": { "message": "Gửi khoản tiền" }, - "sendETH": { - "message": "Gửi ETH" - }, "sendSpecifiedTokens": { "message": "Gửi $1", "description": "Symbol of the specified token" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9ca2dbe32..4c7671d7f 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -796,9 +796,6 @@ "importWallet": { "message": "导入钱包" }, - "importYourExisting": { - "message": "使用 12 个单词的账户助记词导入您现有的钱包账户。" - }, "imported": { "message": "已导入", "description": "status showing that an account has been fully loaded into the keyring" @@ -1355,9 +1352,6 @@ "secretBackupPhraseWarning": { "message": "警告:切勿向他人透露您的账户助记词。任何人一旦持有该账户助记词,即可控制您的 Ether。" }, - "secretPhrase": { - "message": "输入 12 个单词组成的账户助记词恢复您的账户。" - }, "securityAndPrivacy": { "message": "安全与隐私" }, @@ -1409,9 +1403,6 @@ "sendAmount": { "message": "发送数额" }, - "sendETH": { - "message": "发送 ETH" - }, "sendSpecifiedTokens": { "message": "发送 $1", "description": "Symbol of the specified token" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index d724a8647..2c8dba349 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -514,9 +514,6 @@ "importWallet": { "message": "匯入錢包" }, - "importYourExisting": { - "message": "使用 12 字的助記詞來匯入你現有的錢包" - }, "imported": { "message": "已匯入私鑰", "description": "status showing that an account has been fully loaded into the keyring" @@ -889,9 +886,6 @@ "secretBackupPhraseWarning": { "message": "警告: 絕對不要洩漏您的助憶詞。任何人只要得知助憶詞代表他可以竊取您所有的以太幣和代幣。" }, - "secretPhrase": { - "message": "輸入您的12個助憶詞以回復金庫" - }, "securityAndPrivacy": { "message": "安全&隱私" }, @@ -925,9 +919,6 @@ "sendAmount": { "message": "發送數量" }, - "sendETH": { - "message": "發送以太幣" - }, "sendTokens": { "message": "發送代幣" }, diff --git a/app/background.html b/app/background.html new file mode 100644 index 000000000..290576939 --- /dev/null +++ b/app/background.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/manifest/_base.json b/app/manifest/_base.json index ab4d0d4ec..7f89a7abf 100644 --- a/app/manifest/_base.json +++ b/app/manifest/_base.json @@ -1,14 +1,7 @@ { "author": "https://metamask.io", "background": { - "scripts": [ - "globalthis.js", - "initSentry.js", - "lockdown.js", - "runLockdown.js", - "bg-libs.js", - "background.js" - ], + "page": "background.html", "persistent": true }, "browser_action": { diff --git a/test/unit/app/account-import-strategies.test.js b/app/scripts/account-import-strategies/account-import-strategies.test.js similarity index 97% rename from test/unit/app/account-import-strategies.test.js rename to app/scripts/account-import-strategies/account-import-strategies.test.js index 5dc706b90..bc2f53ae7 100644 --- a/test/unit/app/account-import-strategies.test.js +++ b/app/scripts/account-import-strategies/account-import-strategies.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; import ethUtil from 'ethereumjs-util'; -import accountImporter from '../../../app/scripts/account-import-strategies'; +import accountImporter from '.'; describe('Account Import Strategies', function () { const privkey = diff --git a/app/scripts/background.js b/app/scripts/background.js index 44bcab288..ae46819c1 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,12 +1,6 @@ /** * @file The entry point for the web extension singleton process. */ -// these need to run before anything else -/* eslint-disable import/first,import/order */ -import setupFetchDebugging from './lib/setupFetchDebugging'; -/* eslint-enable import/order */ - -setupFetchDebugging(); // polyfills import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; @@ -70,21 +64,7 @@ if (inTest || process.env.METAMASK_DEBUG) { initialize().catch(log.error); /** - * An object representing a transaction, in whatever state it is in. - * @typedef TransactionMeta - * - * @property {number} id - An internally unique tx identifier. - * @property {number} time - Time the tx was first suggested, in unix epoch time (ms). - * @property {string} status - The current transaction status (unapproved, signed, submitted, dropped, failed, rejected), as defined in `tx-state-manager.js`. - * @property {string} metamaskNetworkId - The transaction's network ID, used for EIP-155 compliance. - * @property {boolean} loadingDefaults - TODO: Document - * @property {Object} txParams - The tx params as passed to the network provider. - * @property {Object[]} history - A history of mutations to this TransactionMeta object. - * @property {string} origin - A string representing the interface that suggested the transaction. - * @property {Object} nonceDetails - A metadata object containing information used to derive the suggested nonce, useful for debugging nonce issues. - * @property {string} rawTx - A hex string of the final signed transaction, ready to submit to the network. - * @property {string} hash - A hex string of the transaction hash, used to identify the transaction on the network. - * @property {number} submittedTime - The time the transaction was submitted to the network, in Unix epoch time (ms). + * @typedef {import('../../shared/constants/transaction').TransactionMeta} TransactionMeta */ /** diff --git a/test/unit/app/controllers/cached-balances.test.js b/app/scripts/controllers/cached-balances.test.js similarity index 95% rename from test/unit/app/controllers/cached-balances.test.js rename to app/scripts/controllers/cached-balances.test.js index cf2826024..94e86b41a 100644 --- a/test/unit/app/controllers/cached-balances.test.js +++ b/app/scripts/controllers/cached-balances.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import sinon from 'sinon'; -import CachedBalancesController from '../../../../app/scripts/controllers/cached-balances'; -import { KOVAN_CHAIN_ID } from '../../../../shared/constants/network'; +import { KOVAN_CHAIN_ID } from '../../../shared/constants/network'; +import CachedBalancesController from './cached-balances'; describe('CachedBalancesController', function () { describe('updateCachedBalances', function () { diff --git a/test/unit/app/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js similarity index 96% rename from test/unit/app/controllers/detect-tokens.test.js rename to app/scripts/controllers/detect-tokens.test.js index 458183d8b..c472c7977 100644 --- a/test/unit/app/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -4,10 +4,10 @@ import { ObservableStore } from '@metamask/obs-store'; import contracts from '@metamask/contract-metadata'; import BigNumber from 'bignumber.js'; -import DetectTokensController from '../../../../app/scripts/controllers/detect-tokens'; -import NetworkController from '../../../../app/scripts/controllers/network/network'; -import PreferencesController from '../../../../app/scripts/controllers/preferences'; -import { MAINNET, ROPSTEN } from '../../../../shared/constants/network'; +import { MAINNET, ROPSTEN } from '../../../shared/constants/network'; +import DetectTokensController from './detect-tokens'; +import NetworkController from './network'; +import PreferencesController from './preferences'; describe('DetectTokensController', function () { const sandbox = sinon.createSandbox(); diff --git a/test/unit/app/controllers/ens-controller.test.js b/app/scripts/controllers/ens/index.test.js similarity index 98% rename from test/unit/app/controllers/ens-controller.test.js rename to app/scripts/controllers/ens/index.test.js index d21a723e1..6e1a4c047 100644 --- a/test/unit/app/controllers/ens-controller.test.js +++ b/app/scripts/controllers/ens/index.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; import sinon from 'sinon'; -import EnsController from '../../../../app/scripts/controllers/ens'; +import EnsController from '.'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_X_ERROR_ADDRESS = '0x'; diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index b6227d984..7aa621898 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -1,32 +1,48 @@ import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import BN from 'bn.js'; -import createId from '../lib/random-id'; +import createId from '../../../shared/modules/random-id'; import { bnToHex } from '../lib/util'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; import { CHAIN_ID_TO_NETWORK_ID_MAP, CHAIN_ID_TO_TYPE_MAP, - GOERLI, GOERLI_CHAIN_ID, - KOVAN, KOVAN_CHAIN_ID, - MAINNET, MAINNET_CHAIN_ID, - RINKEBY, RINKEBY_CHAIN_ID, - ROPSTEN, ROPSTEN_CHAIN_ID, } from '../../../shared/constants/network'; -import { NETWORK_EVENTS } from './network'; const fetchWithTimeout = getFetchWithTimeout(30000); +/** + * @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta + */ + +/** + * A transaction object in the format returned by the Etherscan API. + * + * Note that this is not an exhaustive type definiton; only the properties we use are defined + * + * @typedef {Object} EtherscanTransaction + * @property {string} blockNumber - The number of the block this transaction was found in, in decimal + * @property {string} from - The hex-prefixed address of the sender + * @property {string} gas - The gas limit, in decimal WEI + * @property {string} gasPrice - The gas price, in decimal WEI + * @property {string} hash - The hex-prefixed transaction hash + * @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed) + * @property {string} nonce - The transaction nonce, in decimal + * @property {string} timeStamp - The timestamp for the transaction, in seconds + * @property {string} to - The hex-prefixed address of the recipient + * @property {string} value - The amount of ETH sent in this transaction, in decimal WEI + */ + /** * This controller is responsible for retrieving incoming transactions. Etherscan is polled once every block to check * for new incoming transactions for the current selected account on the current network @@ -44,35 +60,37 @@ const etherscanSupportedNetworks = [ export default class IncomingTransactionsController { constructor(opts = {}) { - const { blockTracker, networkController, preferencesController } = opts; + const { + blockTracker, + onNetworkDidChange, + getCurrentChainId, + preferencesController, + } = opts; this.blockTracker = blockTracker; - this.networkController = networkController; + this.getCurrentChainId = getCurrentChainId; this.preferencesController = preferencesController; this._onLatestBlock = async (newBlockNumberHex) => { const selectedAddress = this.preferencesController.getSelectedAddress(); const newBlockNumberDec = parseInt(newBlockNumberHex, 16); - await this._update({ - address: selectedAddress, - newBlockNumberDec, - }); + await this._update(selectedAddress, newBlockNumberDec); }; const initState = { incomingTransactions: {}, - incomingTxLastFetchedBlocksByNetwork: { - [GOERLI]: null, - [KOVAN]: null, - [MAINNET]: null, - [RINKEBY]: null, - [ROPSTEN]: null, + incomingTxLastFetchedBlockByChainId: { + [GOERLI_CHAIN_ID]: null, + [KOVAN_CHAIN_ID]: null, + [MAINNET_CHAIN_ID]: null, + [RINKEBY_CHAIN_ID]: null, + [ROPSTEN_CHAIN_ID]: null, }, ...opts.initState, }; this.store = new ObservableStore(initState); this.preferencesController.store.subscribe( - pairwise((prevState, currState) => { + previousValueComparator((prevState, currState) => { const { featureFlags: { showIncomingTransactions: prevShowIncomingTransactions, @@ -94,29 +112,24 @@ export default class IncomingTransactionsController { } this.start(); - }), + }, this.preferencesController.store.getState()), ); this.preferencesController.store.subscribe( - pairwise(async (prevState, currState) => { + previousValueComparator(async (prevState, currState) => { const { selectedAddress: prevSelectedAddress } = prevState; const { selectedAddress: currSelectedAddress } = currState; if (currSelectedAddress === prevSelectedAddress) { return; } - - await this._update({ - address: currSelectedAddress, - }); - }), + await this._update(currSelectedAddress); + }, this.preferencesController.store.getState()), ); - this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => { + onNetworkDidChange(async () => { const address = this.preferencesController.getSelectedAddress(); - await this._update({ - address, - }); + await this._update(address); }); } @@ -136,85 +149,79 @@ export default class IncomingTransactionsController { this.blockTracker.removeListener('latest', this._onLatestBlock); } - async _update({ address, newBlockNumberDec } = {}) { - const chainId = this.networkController.getCurrentChainId(); - if (!etherscanSupportedNetworks.includes(chainId)) { + /** + * Determines the correct block number to begin looking for new transactions + * from, fetches the transactions and then saves them and the next block + * number to begin fetching from in state. Block numbers and transactions are + * stored per chainId. + * @private + * @param {string} address - address to lookup transactions for + * @param {number} [newBlockNumberDec] - block number to begin fetching from + * @returns {void} + */ + async _update(address, newBlockNumberDec) { + const chainId = this.getCurrentChainId(); + if (!etherscanSupportedNetworks.includes(chainId) || !address) { return; } try { - const dataForUpdate = await this._getDataForUpdate({ + const currentState = this.store.getState(); + const currentBlock = parseInt(this.blockTracker.getCurrentBlock(), 16); + + const mostRecentlyFetchedBlock = + currentState.incomingTxLastFetchedBlockByChainId[chainId]; + const blockToFetchFrom = + mostRecentlyFetchedBlock ?? newBlockNumberDec ?? currentBlock; + + const newIncomingTxs = await this._getNewIncomingTransactions( address, + blockToFetchFrom, chainId, - newBlockNumberDec, + ); + + let newMostRecentlyFetchedBlock = blockToFetchFrom; + + newIncomingTxs.forEach((tx) => { + if ( + tx.blockNumber && + parseInt(newMostRecentlyFetchedBlock, 10) < + parseInt(tx.blockNumber, 10) + ) { + newMostRecentlyFetchedBlock = parseInt(tx.blockNumber, 10); + } + }); + + this.store.updateState({ + incomingTxLastFetchedBlockByChainId: { + ...currentState.incomingTxLastFetchedBlockByChainId, + [chainId]: newMostRecentlyFetchedBlock + 1, + }, + incomingTransactions: newIncomingTxs.reduce( + (transactions, tx) => { + transactions[tx.hash] = tx; + return transactions; + }, + { + ...currentState.incomingTransactions, + }, + ), }); - this._updateStateWithNewTxData(dataForUpdate); } catch (err) { log.error(err); } } - async _getDataForUpdate({ address, chainId, newBlockNumberDec } = {}) { - const { - incomingTransactions: currentIncomingTxs, - incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork, - } = this.store.getState(); - - const lastFetchBlockByCurrentNetwork = - currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]]; - let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec; - if (blockToFetchFrom === undefined) { - blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16); - } - - const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll( - address, - blockToFetchFrom, - chainId, - ); - - return { - latestIncomingTxBlockNumber, - newTxs, - currentIncomingTxs, - currentBlocksByNetwork, - fetchedBlockNumber: blockToFetchFrom, - chainId, - }; - } - - _updateStateWithNewTxData({ - latestIncomingTxBlockNumber, - newTxs, - currentIncomingTxs, - currentBlocksByNetwork, - fetchedBlockNumber, - chainId, - }) { - const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber - ? parseInt(latestIncomingTxBlockNumber, 10) + 1 - : fetchedBlockNumber + 1; - const newIncomingTransactions = { - ...currentIncomingTxs, - }; - newTxs.forEach((tx) => { - newIncomingTransactions[tx.hash] = tx; - }); - - this.store.updateState({ - incomingTxLastFetchedBlocksByNetwork: { - ...currentBlocksByNetwork, - [CHAIN_ID_TO_TYPE_MAP[chainId]]: newLatestBlockHashByNetwork, - }, - incomingTransactions: newIncomingTransactions, - }); - } - - async _fetchAll(address, fromBlock, chainId) { - const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId); - return this._processTxFetchResponse(fetchedTxResponse); - } - - async _fetchTxs(address, fromBlock, chainId) { + /** + * fetches transactions for the given address and chain, via etherscan, then + * processes the data into the necessary shape for usage in this controller. + * + * @private + * @param {string} [address] - Address to fetch transactions for + * @param {number} [fromBlock] - Block to look for transactions at + * @param {string} [chainId] - The chainId for the current network + * @returns {TransactionMeta[]} + */ + async _getNewIncomingTransactions(address, fromBlock, chainId) { const etherscanSubdomain = chainId === MAINNET_CHAIN_ID ? 'api' @@ -227,16 +234,8 @@ export default class IncomingTransactionsController { url += `&startBlock=${parseInt(fromBlock, 10)}`; } const response = await fetchWithTimeout(url); - const parsedResponse = await response.json(); - - return { - ...parsedResponse, - address, - chainId, - }; - } - - _processTxFetchResponse({ status, result = [], address, chainId }) { + const { status, result } = await response.json(); + let newIncomingTxs = []; if (status === '1' && Array.isArray(result) && result.length > 0) { const remoteTxList = {}; const remoteTxs = []; @@ -247,70 +246,70 @@ export default class IncomingTransactionsController { } }); - const incomingTxs = remoteTxs.filter( + newIncomingTxs = remoteTxs.filter( (tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(), ); - incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)); - - let latestIncomingTxBlockNumber = null; - incomingTxs.forEach((tx) => { - if ( - tx.blockNumber && - (!latestIncomingTxBlockNumber || - parseInt(latestIncomingTxBlockNumber, 10) < - parseInt(tx.blockNumber, 10)) - ) { - latestIncomingTxBlockNumber = tx.blockNumber; - } - }); - return { - latestIncomingTxBlockNumber, - txs: incomingTxs, - }; + newIncomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)); } - return { - latestIncomingTxBlockNumber: null, - txs: [], - }; + return newIncomingTxs; } - _normalizeTxFromEtherscan(txMeta, chainId) { - const time = parseInt(txMeta.timeStamp, 10) * 1000; + /** + * Transmutes a EtherscanTransaction into a TransactionMeta + * @param {EtherscanTransaction} etherscanTransaction - the transaction to normalize + * @param {string} chainId - The chainId of the current network + * @returns {TransactionMeta} + */ + _normalizeTxFromEtherscan(etherscanTransaction, chainId) { + const time = parseInt(etherscanTransaction.timeStamp, 10) * 1000; const status = - txMeta.isError === '0' + etherscanTransaction.isError === '0' ? TRANSACTION_STATUSES.CONFIRMED : TRANSACTION_STATUSES.FAILED; return { - blockNumber: txMeta.blockNumber, + blockNumber: etherscanTransaction.blockNumber, id: createId(), chainId, metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId], status, time, txParams: { - from: txMeta.from, - gas: bnToHex(new BN(txMeta.gas)), - gasPrice: bnToHex(new BN(txMeta.gasPrice)), - nonce: bnToHex(new BN(txMeta.nonce)), - to: txMeta.to, - value: bnToHex(new BN(txMeta.value)), + from: etherscanTransaction.from, + gas: bnToHex(new BN(etherscanTransaction.gas)), + gasPrice: bnToHex(new BN(etherscanTransaction.gasPrice)), + nonce: bnToHex(new BN(etherscanTransaction.nonce)), + to: etherscanTransaction.to, + value: bnToHex(new BN(etherscanTransaction.value)), }, - hash: txMeta.hash, - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + hash: etherscanTransaction.hash, + type: TRANSACTION_TYPES.INCOMING, }; } } -function pairwise(fn) { +/** + * Returns a function with arity 1 that caches the argument that the function + * is called with and invokes the comparator with both the cached, previous, + * value and the current value. If specified, the initialValue will be passed + * in as the previous value on the first invocation of the returned method. + * @template A + * @params {A=} type of compared value + * @param {(prevValue: A, nextValue: A) => void} comparator - method to compare + * previous and next values. + * @param {A} [initialValue] - initial value to supply to prevValue + * on first call of the method. + * @returns {void} + */ +function previousValueComparator(comparator, initialValue) { let first = true; let cache; return (value) => { try { if (first) { first = false; - return fn(value, value); + return comparator(initialValue ?? value, value); } - return fn(cache, value); + return comparator(cache, value); } finally { cache = value; } diff --git a/test/unit/app/controllers/incoming-transactions.test.js b/app/scripts/controllers/incoming-transactions.test.js similarity index 60% rename from test/unit/app/controllers/incoming-transactions.test.js rename to app/scripts/controllers/incoming-transactions.test.js index d476d18cb..74aaad78c 100644 --- a/test/unit/app/controllers/incoming-transactions.test.js +++ b/app/scripts/controllers/incoming-transactions.test.js @@ -4,66 +4,67 @@ import proxyquire from 'proxyquire'; import nock from 'nock'; import { cloneDeep } from 'lodash'; -import waitUntilCalled from '../../../lib/wait-until-called'; +import waitUntilCalled from '../../../test/lib/wait-until-called'; import { - GOERLI, - KOVAN, - MAINNET, + CHAIN_ID_TO_TYPE_MAP, + GOERLI_CHAIN_ID, + KOVAN_CHAIN_ID, MAINNET_CHAIN_ID, - RINKEBY, - ROPSTEN, + RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, ROPSTEN_NETWORK_ID, -} from '../../../../shared/constants/network'; + ROPSTEN, +} from '../../../shared/constants/network'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_STATUSES, -} from '../../../../shared/constants/transaction'; -import { NETWORK_EVENTS } from '../../../../app/scripts/controllers/network'; +} from '../../../shared/constants/transaction'; -const IncomingTransactionsController = proxyquire( - '../../../../app/scripts/controllers/incoming-transactions', - { - '../lib/random-id': { default: () => 54321 }, - }, -).default; +const IncomingTransactionsController = proxyquire('./incoming-transactions', { + '../../../shared/modules/random-id': { default: () => 54321 }, +}).default; const FAKE_CHAIN_ID = '0x1338'; const MOCK_SELECTED_ADDRESS = '0x0101'; const SET_STATE_TIMEOUT = 10; +const EXISTING_INCOMING_TX = { id: 777, hash: '0x123456' }; +const PREPOPULATED_INCOMING_TXS_BY_HASH = { + [EXISTING_INCOMING_TX.hash]: EXISTING_INCOMING_TX, +}; +const PREPOPULATED_BLOCKS_BY_NETWORK = { + [GOERLI_CHAIN_ID]: 1, + [KOVAN_CHAIN_ID]: 2, + [MAINNET_CHAIN_ID]: 3, + [RINKEBY_CHAIN_ID]: 5, + [ROPSTEN_CHAIN_ID]: 4, +}; +const EMPTY_BLOCKS_BY_NETWORK = { + [GOERLI_CHAIN_ID]: null, + [KOVAN_CHAIN_ID]: null, + [MAINNET_CHAIN_ID]: null, + [RINKEBY_CHAIN_ID]: null, + [ROPSTEN_CHAIN_ID]: null, +}; + function getEmptyInitState() { return { incomingTransactions: {}, - incomingTxLastFetchedBlocksByNetwork: { - [GOERLI]: null, - [KOVAN]: null, - [MAINNET]: null, - [RINKEBY]: null, - [ROPSTEN]: null, - }, + incomingTxLastFetchedBlockByChainId: EMPTY_BLOCKS_BY_NETWORK, }; } function getNonEmptyInitState() { return { - incomingTransactions: { - '0x123456': { id: 777 }, - }, - incomingTxLastFetchedBlocksByNetwork: { - [GOERLI]: 1, - [KOVAN]: 2, - [MAINNET]: 3, - [RINKEBY]: 5, - [ROPSTEN]: 4, - }, + incomingTransactions: PREPOPULATED_INCOMING_TXS_BY_HASH, + incomingTxLastFetchedBlockByChainId: PREPOPULATED_BLOCKS_BY_NETWORK, }; } -function getMockNetworkController(chainId = FAKE_CHAIN_ID) { +function getMockNetworkControllerMethods(chainId = FAKE_CHAIN_ID) { return { getCurrentChainId: () => chainId, - on: sinon.spy(), + onNetworkDidChange: sinon.spy(), }; } @@ -93,21 +94,9 @@ function getMockBlockTracker() { } /** - * A transaction object in the format returned by the Etherscan API. - * - * Note that this is not an exhaustive type definiton; only the properties we use are defined - * - * @typedef {Object} EtherscanTransaction - * @property {string} blockNumber - The number of the block this transaction was found in, in decimal - * @property {string} from - The hex-prefixed address of the sender - * @property {string} gas - The gas limit, in decimal WEI - * @property {string} gasPrice - The gas price, in decimal WEI - * @property {string} hash - The hex-prefixed transaction hash - * @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed) - * @property {string} nonce - The transaction nonce, in decimal - * @property {string} timeStamp - The timestamp for the transaction, in seconds - * @property {string} to - The hex-prefixed address of the recipient - * @property {string} value - The amount of ETH sent in this transaction, in decimal WEI + * @typedef {import( + * '../../../../app/scripts/controllers/incoming-transactions' + * ).EtherscanTransaction} EtherscanTransaction */ /** @@ -136,6 +125,25 @@ const getFakeEtherscanTransaction = ( }; }; +function nockEtherscanApiForAllChains(mockResponse) { + for (const chainId of [ + GOERLI_CHAIN_ID, + KOVAN_CHAIN_ID, + MAINNET_CHAIN_ID, + RINKEBY_CHAIN_ID, + ROPSTEN_CHAIN_ID, + 'undefined', + ]) { + nock( + `https://api${ + chainId === MAINNET_CHAIN_ID ? '' : `-${CHAIN_ID_TO_TYPE_MAP[chainId]}` + }.etherscan.io`, + ) + .get(/api.+/u) + .reply(200, JSON.stringify(mockResponse)); + } +} + describe('IncomingTransactionsController', function () { afterEach(function () { sinon.restore(); @@ -144,37 +152,32 @@ describe('IncomingTransactionsController', function () { describe('constructor', function () { it('should set up correct store, listeners and properties in the constructor', function () { + const mockedNetworkMethods = getMockNetworkControllerMethods(); const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(), + ...mockedNetworkMethods, preferencesController: getMockPreferencesController(), initState: {}, }, ); sinon.spy(incomingTransactionsController, '_update'); - assert.deepEqual( + assert.deepStrictEqual( incomingTransactionsController.store.getState(), getEmptyInitState(), ); - assert(incomingTransactionsController.networkController.on.calledOnce); - assert.equal( - incomingTransactionsController.networkController.on.getCall(0).args[0], - NETWORK_EVENTS.NETWORK_DID_CHANGE, - ); - const networkControllerListenerCallback = incomingTransactionsController.networkController.on.getCall( + assert(mockedNetworkMethods.onNetworkDidChange.calledOnce); + const networkControllerListenerCallback = mockedNetworkMethods.onNetworkDidChange.getCall( 0, - ).args[1]; - assert.equal(incomingTransactionsController._update.callCount, 0); + ).args[0]; + assert.strictEqual(incomingTransactionsController._update.callCount, 0); networkControllerListenerCallback('testNetworkType'); - assert.equal(incomingTransactionsController._update.callCount, 1); - assert.deepEqual( + assert.strictEqual(incomingTransactionsController._update.callCount, 1); + assert.deepStrictEqual( incomingTransactionsController._update.getCall(0).args[0], - { - address: '0x0101', - }, + '0x0101', ); incomingTransactionsController._update.resetHistory(); @@ -184,13 +187,13 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(), + ...getMockNetworkControllerMethods(), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - assert.deepEqual( + assert.deepStrictEqual( incomingTransactionsController.store.getState(), getNonEmptyInitState(), ); @@ -202,7 +205,7 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(), + ...getMockNetworkControllerMethods(), preferencesController: getMockPreferencesController(), initState: {}, }, @@ -213,7 +216,7 @@ describe('IncomingTransactionsController', function () { assert( incomingTransactionsController.blockTracker.addListener.calledOnce, ); - assert.equal( + assert.strictEqual( incomingTransactionsController.blockTracker.addListener.getCall(0) .args[0], 'latest', @@ -224,13 +227,13 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); const startBlock = getNonEmptyInitState() - .incomingTxLastFetchedBlocksByNetwork[ROPSTEN]; + .incomingTxLastFetchedBlockByChainId[ROPSTEN_CHAIN_ID]; nock('https://api-ropsten.etherscan.io') .get( `/api?module=account&action=txlist&address=${MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`, @@ -276,7 +279,7 @@ describe('IncomingTransactionsController', function () { chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.CONFIRMED, time: 16000000000000000, - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + type: TRANSACTION_TYPES.INCOMING, txParams: { from: '0xfake', gas: '0x0', @@ -287,9 +290,9 @@ describe('IncomingTransactionsController', function () { }, }, }, - incomingTxLastFetchedBlocksByNetwork: { - ...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork, - [ROPSTEN]: 11, + incomingTxLastFetchedBlockByChainId: { + ...getNonEmptyInitState().incomingTxLastFetchedBlockByChainId, + [ROPSTEN_CHAIN_ID]: 11, }, }, 'State should have been updated after first block was received', @@ -300,34 +303,17 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(), + ...getMockNetworkControllerMethods(), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction()], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction()], + }); + const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -365,7 +351,7 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(), + ...getMockNetworkControllerMethods(), preferencesController: getMockPreferencesController({ showIncomingTransactions: false, }), @@ -373,28 +359,10 @@ describe('IncomingTransactionsController', function () { }, ); // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction()], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction()], + }); const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -432,34 +400,16 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction()], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction()], + }); const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -495,34 +445,16 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction()], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction()], + }); const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -560,14 +492,14 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); const NEW_MOCK_SELECTED_ADDRESS = `${MOCK_SELECTED_ADDRESS}9`; const startBlock = getNonEmptyInitState() - .incomingTxLastFetchedBlocksByNetwork[ROPSTEN]; + .incomingTxLastFetchedBlockByChainId[ROPSTEN_CHAIN_ID]; nock('https://api-ropsten.etherscan.io') .get( `/api?module=account&action=txlist&address=${NEW_MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`, @@ -620,7 +552,7 @@ describe('IncomingTransactionsController', function () { chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.CONFIRMED, time: 16000000000000000, - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + type: TRANSACTION_TYPES.INCOMING, txParams: { from: '0xfake', gas: '0x0', @@ -631,9 +563,9 @@ describe('IncomingTransactionsController', function () { }, }, }, - incomingTxLastFetchedBlocksByNetwork: { - ...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork, - [ROPSTEN]: 11, + incomingTxLastFetchedBlockByChainId: { + ...getNonEmptyInitState().incomingTxLastFetchedBlockByChainId, + [ROPSTEN_CHAIN_ID]: 11, }, }, 'State should have been updated after first block was received', @@ -644,35 +576,17 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: { ...getMockBlockTracker() }, - networkController: getMockNetworkController(), + ...getMockNetworkControllerMethods(), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); const NEW_MOCK_SELECTED_ADDRESS = `${MOCK_SELECTED_ADDRESS}9`; // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)], + }); const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -714,16 +628,19 @@ describe('IncomingTransactionsController', function () { }); it('should update when switching to a supported network', async function () { + const mockedNetworkMethods = getMockNetworkControllerMethods( + ROPSTEN_CHAIN_ID, + ); const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...mockedNetworkMethods, preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); const startBlock = getNonEmptyInitState() - .incomingTxLastFetchedBlocksByNetwork[ROPSTEN]; + .incomingTxLastFetchedBlockByChainId[ROPSTEN_CHAIN_ID]; nock('https://api-ropsten.etherscan.io') .get( `/api?module=account&action=txlist&address=${MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`, @@ -744,13 +661,9 @@ describe('IncomingTransactionsController', function () { incomingTransactionsController.store, ); - const subscription = incomingTransactionsController.networkController.on.getCall( - 0, - ).args[1]; - incomingTransactionsController.networkController = getMockNetworkController( - ROPSTEN_CHAIN_ID, - ); - await subscription(ROPSTEN); + const subscription = mockedNetworkMethods.onNetworkDidChange.getCall(0) + .args[0]; + await subscription(ROPSTEN_CHAIN_ID); await updateStateCalled(); const actualState = incomingTransactionsController.store.getState(); @@ -775,7 +688,7 @@ describe('IncomingTransactionsController', function () { chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.CONFIRMED, time: 16000000000000000, - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + type: TRANSACTION_TYPES.INCOMING, txParams: { from: '0xfake', gas: '0x0', @@ -786,9 +699,9 @@ describe('IncomingTransactionsController', function () { }, }, }, - incomingTxLastFetchedBlocksByNetwork: { - ...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork, - [ROPSTEN]: 11, + incomingTxLastFetchedBlockByChainId: { + ...getNonEmptyInitState().incomingTxLastFetchedBlockByChainId, + [ROPSTEN_CHAIN_ID]: 11, }, }, 'State should have been updated after first block was received', @@ -796,38 +709,22 @@ describe('IncomingTransactionsController', function () { }); it('should not update when switching to an unsupported network', async function () { - const networkController = getMockNetworkController(ROPSTEN_CHAIN_ID); + const mockedNetworkMethods = getMockNetworkControllerMethods( + ROPSTEN_CHAIN_ID, + ); const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController, + ...mockedNetworkMethods, preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); // reply with a valid request for any supported network, so that this test has every opportunity to fail - for (const network of [ - GOERLI, - KOVAN, - MAINNET, - RINKEBY, - ROPSTEN, - 'undefined', - ]) { - nock( - `https://api${ - network === MAINNET ? '' : `-${network.toLowerCase()}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply( - 200, - JSON.stringify({ - status: '1', - result: [getFakeEtherscanTransaction()], - }), - ); - } + nockEtherscanApiForAllChains({ + status: '1', + result: [getFakeEtherscanTransaction()], + }); const updateStateStub = sinon.stub( incomingTransactionsController.store, 'updateState', @@ -845,11 +742,10 @@ describe('IncomingTransactionsController', function () { incomingTransactionsController.store, ); - const subscription = incomingTransactionsController.networkController.on.getCall( - 0, - ).args[1]; + const subscription = mockedNetworkMethods.onNetworkDidChange.getCall(0) + .args[0]; - networkController.getCurrentChainId = () => FAKE_CHAIN_ID; + incomingTransactionsController.getCurrentChainId = () => FAKE_CHAIN_ID; await subscription(); try { @@ -867,186 +763,200 @@ describe('IncomingTransactionsController', function () { }); }); - describe('_getDataForUpdate', function () { - it('should call fetchAll with the correct params when passed a new block number and the current network has no stored block', async function () { - const incomingTransactionsController = new IncomingTransactionsController( - { - blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), - preferencesController: getMockPreferencesController(), - initState: getEmptyInitState(), - }, - ); - incomingTransactionsController._fetchAll = sinon.stub().returns({}); + describe('_update', function () { + describe('when state is empty (initialized)', function () { + it('should use provided block number and update the latest block seen', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + blockTracker: getMockBlockTracker(), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), + preferencesController: getMockPreferencesController(), + initState: getEmptyInitState(), + getCurrentChainId: () => ROPSTEN_CHAIN_ID, + }, + ); + sinon.spy(incomingTransactionsController.store, 'updateState'); - await incomingTransactionsController._getDataForUpdate({ - address: 'fakeAddress', - chainId: ROPSTEN_CHAIN_ID, - newBlockNumberDec: 999, - }); + incomingTransactionsController._getNewIncomingTransactions = sinon + .stub() + .returns([]); - assert(incomingTransactionsController._fetchAll.calledOnce); + await incomingTransactionsController._update('fakeAddress', 999); + assert( + incomingTransactionsController._getNewIncomingTransactions.calledOnce, + ); + assert.deepStrictEqual( + incomingTransactionsController._getNewIncomingTransactions.getCall(0) + .args, + ['fakeAddress', 999, ROPSTEN_CHAIN_ID], + ); + assert.deepStrictEqual( + incomingTransactionsController.store.updateState.getCall(0).args[0], + { + incomingTxLastFetchedBlockByChainId: { + ...EMPTY_BLOCKS_BY_NETWORK, + [ROPSTEN_CHAIN_ID]: 1000, + }, + incomingTransactions: {}, + }, + ); + }); - assert.deepEqual( - incomingTransactionsController._fetchAll.getCall(0).args, - ['fakeAddress', 999, ROPSTEN_CHAIN_ID], - ); - }); + it('should update the last fetched block for network to highest block seen in incoming txs', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + blockTracker: getMockBlockTracker(), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), + preferencesController: getMockPreferencesController(), + initState: getEmptyInitState(), + getCurrentChainId: () => ROPSTEN_CHAIN_ID, + }, + ); - it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async function () { - const incomingTransactionsController = new IncomingTransactionsController( - { - blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), - preferencesController: getMockPreferencesController(), - initState: getNonEmptyInitState(), - }, - ); - incomingTransactionsController._fetchAll = sinon.stub().returns({}); + const NEW_TRANSACTION_ONE = { + id: 555, + hash: '0xfff', + blockNumber: 444, + }; + const NEW_TRANSACTION_TWO = { + id: 556, + hash: '0xffa', + blockNumber: 443, + }; + + sinon.spy(incomingTransactionsController.store, 'updateState'); + + incomingTransactionsController._getNewIncomingTransactions = sinon + .stub() + .returns([NEW_TRANSACTION_ONE, NEW_TRANSACTION_TWO]); + await incomingTransactionsController._update('fakeAddress', 10); + + assert(incomingTransactionsController.store.updateState.calledOnce); + + assert.deepStrictEqual( + incomingTransactionsController._getNewIncomingTransactions.getCall(0) + .args, + ['fakeAddress', 10, ROPSTEN_CHAIN_ID], + ); - await incomingTransactionsController._getDataForUpdate({ - address: 'fakeAddress', - chainId: ROPSTEN_CHAIN_ID, - newBlockNumberDec: 999, + assert.deepStrictEqual( + incomingTransactionsController.store.updateState.getCall(0).args[0], + { + incomingTxLastFetchedBlockByChainId: { + ...EMPTY_BLOCKS_BY_NETWORK, + [ROPSTEN_CHAIN_ID]: 445, + }, + incomingTransactions: { + [NEW_TRANSACTION_ONE.hash]: NEW_TRANSACTION_ONE, + [NEW_TRANSACTION_TWO.hash]: NEW_TRANSACTION_TWO, + }, + }, + ); }); + }); - assert(incomingTransactionsController._fetchAll.calledOnce); + describe('when state is populated with prior data for network', function () { + it('should use the last fetched block for the current network and increment by 1 in state', async function () { + const incomingTransactionsController = new IncomingTransactionsController( + { + blockTracker: getMockBlockTracker(), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), + preferencesController: getMockPreferencesController(), + initState: getNonEmptyInitState(), + getCurrentChainId: () => ROPSTEN_CHAIN_ID, + }, + ); + sinon.spy(incomingTransactionsController.store, 'updateState'); + incomingTransactionsController._getNewIncomingTransactions = sinon + .stub() + .returns([]); - assert.deepEqual( - incomingTransactionsController._fetchAll.getCall(0).args, - ['fakeAddress', 4, ROPSTEN_CHAIN_ID], - ); - }); + await incomingTransactionsController._update('fakeAddress', 999); - it('should return the expected data', async function () { - const incomingTransactionsController = new IncomingTransactionsController( - { - blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), - preferencesController: getMockPreferencesController(), - initState: getNonEmptyInitState(), - }, - ); - incomingTransactionsController._fetchAll = sinon.stub().returns({ - latestIncomingTxBlockNumber: 444, - txs: [{ id: 555 }], - }); + assert( + incomingTransactionsController._getNewIncomingTransactions.calledOnce, + ); - const result = await incomingTransactionsController._getDataForUpdate({ - address: 'fakeAddress', - chainId: ROPSTEN_CHAIN_ID, - }); + assert.deepStrictEqual( + incomingTransactionsController._getNewIncomingTransactions.getCall(0) + .args, + ['fakeAddress', 4, ROPSTEN_CHAIN_ID], + ); - assert.deepEqual(result, { - latestIncomingTxBlockNumber: 444, - newTxs: [{ id: 555 }], - currentIncomingTxs: { - '0x123456': { id: 777 }, - }, - currentBlocksByNetwork: { - [GOERLI]: 1, - [KOVAN]: 2, - [MAINNET]: 3, - [RINKEBY]: 5, - [ROPSTEN]: 4, - }, - fetchedBlockNumber: 4, - chainId: ROPSTEN_CHAIN_ID, + assert.deepStrictEqual( + incomingTransactionsController.store.updateState.getCall(0).args[0], + { + incomingTxLastFetchedBlockByChainId: { + ...PREPOPULATED_BLOCKS_BY_NETWORK, + [ROPSTEN_CHAIN_ID]: + PREPOPULATED_BLOCKS_BY_NETWORK[ROPSTEN_CHAIN_ID] + 1, + }, + incomingTransactions: PREPOPULATED_INCOMING_TXS_BY_HASH, + }, + ); }); }); - }); - describe('_updateStateWithNewTxData', function () { - const MOCK_INPUT_WITHOUT_LASTEST = { - newTxs: [{ id: 555, hash: '0xfff' }], - currentIncomingTxs: { - '0x123456': { id: 777, hash: '0x123456' }, - }, - currentBlocksByNetwork: { - [GOERLI]: 1, - [KOVAN]: 2, - [MAINNET]: 3, - [RINKEBY]: 5, - [ROPSTEN]: 4, - }, - fetchedBlockNumber: 1111, - chainId: ROPSTEN_CHAIN_ID, - }; - - const MOCK_INPUT_WITH_LASTEST = { - ...MOCK_INPUT_WITHOUT_LASTEST, - latestIncomingTxBlockNumber: 444, - }; - - it('should update state with correct blockhash and transactions when passed a truthy latestIncomingTxBlockNumber', async function () { + it('should update the last fetched block for network to highest block seen in incoming txs', async function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), + getCurrentChainId: () => ROPSTEN_CHAIN_ID, }, ); - sinon.spy(incomingTransactionsController.store, 'updateState'); - - await incomingTransactionsController._updateStateWithNewTxData( - MOCK_INPUT_WITH_LASTEST, - ); - assert(incomingTransactionsController.store.updateState.calledOnce); - - assert.deepEqual( - incomingTransactionsController.store.updateState.getCall(0).args[0], - { - incomingTxLastFetchedBlocksByNetwork: { - ...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork, - [ROPSTEN]: 445, - }, - incomingTransactions: { - '0x123456': { id: 777, hash: '0x123456' }, - '0xfff': { id: 555, hash: '0xfff' }, - }, - }, - ); - }); + const NEW_TRANSACTION_ONE = { + id: 555, + hash: '0xfff', + blockNumber: 444, + }; + const NEW_TRANSACTION_TWO = { + id: 556, + hash: '0xffa', + blockNumber: 443, + }; - it('should update state with correct blockhash and transactions when passed a falsy latestIncomingTxBlockNumber', async function () { - const incomingTransactionsController = new IncomingTransactionsController( - { - blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), - preferencesController: getMockPreferencesController(), - initState: getNonEmptyInitState(), - }, - ); sinon.spy(incomingTransactionsController.store, 'updateState'); - await incomingTransactionsController._updateStateWithNewTxData( - MOCK_INPUT_WITHOUT_LASTEST, - ); + incomingTransactionsController._getNewIncomingTransactions = sinon + .stub() + .returns([NEW_TRANSACTION_ONE, NEW_TRANSACTION_TWO]); + await incomingTransactionsController._update('fakeAddress', 10); assert(incomingTransactionsController.store.updateState.calledOnce); - assert.deepEqual( + assert.deepStrictEqual( + incomingTransactionsController._getNewIncomingTransactions.getCall(0) + .args, + ['fakeAddress', 4, ROPSTEN_CHAIN_ID], + ); + + assert.deepStrictEqual( incomingTransactionsController.store.updateState.getCall(0).args[0], { - incomingTxLastFetchedBlocksByNetwork: { - ...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork, - [ROPSTEN]: 1112, + incomingTxLastFetchedBlockByChainId: { + ...PREPOPULATED_BLOCKS_BY_NETWORK, + [ROPSTEN_CHAIN_ID]: 445, }, incomingTransactions: { - '0x123456': { id: 777, hash: '0x123456' }, - '0xfff': { id: 555, hash: '0xfff' }, + ...PREPOPULATED_INCOMING_TXS_BY_HASH, + [NEW_TRANSACTION_ONE.hash]: NEW_TRANSACTION_ONE, + [NEW_TRANSACTION_TWO.hash]: NEW_TRANSACTION_TWO, }, }, ); }); }); - describe('_fetchTxs', function () { + describe('_getNewIncomingTransactions', function () { + const ADDRESS_TO_FETCH_FOR = '0xfakeaddress'; + const FETCHED_TX = getFakeEtherscanTransaction(ADDRESS_TO_FETCH_FOR); const mockFetch = sinon.stub().returns( Promise.resolve({ - json: () => Promise.resolve({ someKey: 'someValue' }), + json: () => Promise.resolve({ status: '1', result: [FETCHED_TX] }), }), ); let tempFetch; @@ -1060,262 +970,152 @@ describe('IncomingTransactionsController', function () { mockFetch.resetHistory(); }); - it('should call fetch with the expected url when passed an address, block number and supported network', async function () { + it('should call fetch with the expected url when passed an address, block number and supported chainId', async function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - await incomingTransactionsController._fetchTxs( - '0xfakeaddress', + await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, '789', ROPSTEN_CHAIN_ID, ); assert(mockFetch.calledOnce); - assert.equal( + assert.strictEqual( mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`, ); }); - it('should call fetch with the expected url when passed an address, block number and MAINNET', async function () { + it('should call fetch with the expected url when passed an address, block number and MAINNET chainId', async function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(MAINNET_CHAIN_ID), + ...getMockNetworkControllerMethods(MAINNET_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - await incomingTransactionsController._fetchTxs( - '0xfakeaddress', + await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, '789', MAINNET_CHAIN_ID, ); assert(mockFetch.calledOnce); - assert.equal( + assert.strictEqual( mockFetch.getCall(0).args[0], `https://api.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`, ); }); - it('should call fetch with the expected url when passed an address and supported network, but a falsy block number', async function () { + it('should call fetch with the expected url when passed an address and supported chainId, but a falsy block number', async function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - await incomingTransactionsController._fetchTxs( - '0xfakeaddress', + await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, null, ROPSTEN_CHAIN_ID, ); assert(mockFetch.calledOnce); - assert.equal( + assert.strictEqual( mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1`, ); }); - it('should return the results from the fetch call, plus the address and currentNetworkID, when passed an address, block number and supported network', async function () { + it('should return an array of normalized transactions', async function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - const result = await incomingTransactionsController._fetchTxs( - '0xfakeaddress', + const result = await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, '789', ROPSTEN_CHAIN_ID, ); assert(mockFetch.calledOnce); - assert.deepEqual(result, { - someKey: 'someValue', - address: '0xfakeaddress', - chainId: ROPSTEN_CHAIN_ID, - }); + assert.deepStrictEqual(result, [ + incomingTransactionsController._normalizeTxFromEtherscan( + FETCHED_TX, + ROPSTEN_CHAIN_ID, + ), + ]); }); - }); - describe('_processTxFetchResponse', function () { - it('should return a null block number and empty tx array if status is 0', function () { - const incomingTransactionsController = new IncomingTransactionsController( - { - blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), - preferencesController: getMockPreferencesController(), - initState: getNonEmptyInitState(), - }, + it('should return empty tx array if status is 0', async function () { + const mockFetchStatusZero = sinon.stub().returns( + Promise.resolve({ + json: () => Promise.resolve({ status: '0', result: [FETCHED_TX] }), + }), ); - - const result = incomingTransactionsController._processTxFetchResponse({ - status: '0', - result: [{ id: 1 }], - address: '0xfakeaddress', - }); - - assert.deepEqual(result, { - latestIncomingTxBlockNumber: null, - txs: [], - }); - }); - - it('should return a null block number and empty tx array if the passed result array is empty', function () { + const tempFetchStatusZero = window.fetch; + window.fetch = mockFetchStatusZero; const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - const result = incomingTransactionsController._processTxFetchResponse({ - status: '1', - result: [], - address: '0xfakeaddress', - }); - - assert.deepEqual(result, { - latestIncomingTxBlockNumber: null, - txs: [], - }); + const result = await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, + '789', + ROPSTEN_CHAIN_ID, + ); + assert.deepStrictEqual(result, []); + window.fetch = tempFetchStatusZero; + mockFetchStatusZero.reset(); }); - it('should return the expected block number and tx list when passed data from a successful fetch', function () { + it('should return empty tx array if result array is empty', async function () { + const mockFetchEmptyResult = sinon.stub().returns( + Promise.resolve({ + json: () => Promise.resolve({ status: '1', result: [] }), + }), + ); + const tempFetchEmptyResult = window.fetch; + window.fetch = mockFetchEmptyResult; const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, ); - incomingTransactionsController._normalizeTxFromEtherscan = (tx) => ({ - ...tx, - currentNetworkID: ROPSTEN_NETWORK_ID, - normalized: true, - }); - - const result = incomingTransactionsController._processTxFetchResponse({ - status: '1', - address: '0xfakeaddress', - chainId: ROPSTEN_CHAIN_ID, - result: [ - { - hash: '0xabc123', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5000, - time: 10, - }, - { - hash: '0xabc123', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5000, - time: 10, - }, - { - hash: '0xabc1234', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5000, - time: 9, - }, - { - hash: '0xabc12345', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5001, - time: 11, - }, - { - hash: '0xabc123456', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5001, - time: 12, - }, - { - hash: '0xabc1234567', - txParams: { - to: '0xanotherFakeaddress', - }, - blockNumber: 5002, - time: 13, - }, - ], - }); - - assert.deepEqual(result, { - latestIncomingTxBlockNumber: 5001, - txs: [ - { - hash: '0xabc1234', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5000, - time: 9, - normalized: true, - currentNetworkID: ROPSTEN_NETWORK_ID, - }, - { - hash: '0xabc123', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5000, - time: 10, - normalized: true, - currentNetworkID: ROPSTEN_NETWORK_ID, - }, - { - hash: '0xabc12345', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5001, - time: 11, - normalized: true, - currentNetworkID: ROPSTEN_NETWORK_ID, - }, - { - hash: '0xabc123456', - txParams: { - to: '0xfakeaddress', - }, - blockNumber: 5001, - time: 12, - normalized: true, - currentNetworkID: ROPSTEN_NETWORK_ID, - }, - ], - }); + const result = await incomingTransactionsController._getNewIncomingTransactions( + ADDRESS_TO_FETCH_FOR, + '789', + ROPSTEN_CHAIN_ID, + ); + assert.deepStrictEqual(result, []); + window.fetch = tempFetchEmptyResult; + mockFetchEmptyResult.reset(); }); }); @@ -1324,7 +1124,7 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, @@ -1346,7 +1146,7 @@ describe('IncomingTransactionsController', function () { ROPSTEN_CHAIN_ID, ); - assert.deepEqual(result, { + assert.deepStrictEqual(result, { blockNumber: 333, id: 54321, metamaskNetworkId: ROPSTEN_NETWORK_ID, @@ -1362,7 +1162,7 @@ describe('IncomingTransactionsController', function () { value: '0xf', }, hash: '0xg', - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + type: TRANSACTION_TYPES.INCOMING, }); }); @@ -1370,7 +1170,7 @@ describe('IncomingTransactionsController', function () { const incomingTransactionsController = new IncomingTransactionsController( { blockTracker: getMockBlockTracker(), - networkController: getMockNetworkController(ROPSTEN_CHAIN_ID), + ...getMockNetworkControllerMethods(ROPSTEN_CHAIN_ID), preferencesController: getMockPreferencesController(), initState: getNonEmptyInitState(), }, @@ -1392,7 +1192,7 @@ describe('IncomingTransactionsController', function () { ROPSTEN_CHAIN_ID, ); - assert.deepEqual(result, { + assert.deepStrictEqual(result, { blockNumber: 333, id: 54321, metamaskNetworkId: ROPSTEN_NETWORK_ID, @@ -1408,7 +1208,7 @@ describe('IncomingTransactionsController', function () { value: '0xf', }, hash: '0xg', - transactionCategory: TRANSACTION_CATEGORIES.INCOMING, + type: TRANSACTION_TYPES.INCOMING, }); }); }); diff --git a/test/unit/app/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js similarity index 97% rename from test/unit/app/controllers/metametrics.test.js rename to app/scripts/controllers/metametrics.test.js index 839b323c4..0d783badd 100644 --- a/test/unit/app/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1,14 +1,14 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import MetaMetricsController from '../../../../app/scripts/controllers/metametrics'; -import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../../shared/constants/app'; -import { createSegmentMock } from '../../../../app/scripts/lib/segment'; +import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; +import { createSegmentMock } from '../lib/segment'; import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, -} from '../../../../shared/constants/metametrics'; -import waitUntilCalled from '../../../lib/wait-until-called'; -import { NETWORK_EVENTS } from '../../../../app/scripts/controllers/network'; +} from '../../../shared/constants/metametrics'; +import waitUntilCalled from '../../../test/lib/wait-until-called'; +import MetaMetricsController from './metametrics'; +import { NETWORK_EVENTS } from './network'; const segment = createSegmentMock(2, 10000); const segmentLegacy = createSegmentMock(2, 10000); diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 00f5d8c68..eb298f37d 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -6,7 +6,7 @@ import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'; import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'; import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'; import createInfuraMiddleware from 'eth-json-rpc-infura'; -import BlockTracker from 'eth-block-tracker'; +import { PollingBlockTracker } from 'eth-block-tracker'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../../shared/constants/network'; @@ -18,7 +18,7 @@ export default function createInfuraClient({ network, projectId }) { source: 'metamask', }); const infuraProvider = providerFromMiddleware(infuraMiddleware); - const blockTracker = new BlockTracker({ provider: infuraProvider }); + const blockTracker = new PollingBlockTracker({ provider: infuraProvider }); const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 836801a0f..4b505258d 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -5,7 +5,7 @@ import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'; import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'; import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'; import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'; -import BlockTracker from 'eth-block-tracker'; +import { PollingBlockTracker } from 'eth-block-tracker'; const inTest = process.env.IN_TEST === 'true'; const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {}; @@ -16,7 +16,7 @@ const getTestMiddlewares = () => { export default function createJsonRpcClient({ rpcUrl, chainId }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }); const blockProvider = providerFromMiddleware(fetchMiddleware); - const blockTracker = new BlockTracker({ + const blockTracker = new PollingBlockTracker({ ...blockTrackerOpts, provider: blockProvider, }); diff --git a/test/unit/app/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js similarity index 95% rename from test/unit/app/controllers/network/network-controller.test.js rename to app/scripts/controllers/network/network-controller.test.js index b7e1ab502..3417241f6 100644 --- a/test/unit/app/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import NetworkController from '../../../../../app/scripts/controllers/network'; -import { getNetworkDisplayName } from '../../../../../app/scripts/controllers/network/util'; +import { getNetworkDisplayName } from './util'; +import NetworkController from './network'; describe('NetworkController', function () { describe('controller', function () { diff --git a/test/unit/app/controllers/network/pending-middleware.test.js b/app/scripts/controllers/network/pending-middleware.test.js similarity index 96% rename from test/unit/app/controllers/network/pending-middleware.test.js rename to app/scripts/controllers/network/pending-middleware.test.js index 65e011d09..1ce327b22 100644 --- a/test/unit/app/controllers/network/pending-middleware.test.js +++ b/app/scripts/controllers/network/pending-middleware.test.js @@ -1,9 +1,9 @@ import assert from 'assert'; +import { txMetaStub } from '../../../../test/stub/tx-meta-stub'; import { createPendingNonceMiddleware, createPendingTxMiddleware, -} from '../../../../../app/scripts/controllers/network/middleware/pending'; -import { txMetaStub } from './stubs'; +} from './middleware/pending'; describe('PendingNonceMiddleware', function () { describe('#createPendingNonceMiddleware', function () { diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index c4b6eae79..281c4e8a3 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -297,7 +297,7 @@ export class PermissionsController { this.validatePermittedAccounts([account]); const oldPermittedAccounts = this._getPermittedAccounts(origin); - if (!oldPermittedAccounts) { + if (oldPermittedAccounts.length === 0) { throw new Error(`Origin does not have 'eth_accounts' permission`); } else if (oldPermittedAccounts.includes(account)) { throw new Error('Account is already permitted for origin'); @@ -335,7 +335,7 @@ export class PermissionsController { this.validatePermittedAccounts([account]); const oldPermittedAccounts = this._getPermittedAccounts(origin); - if (!oldPermittedAccounts) { + if (oldPermittedAccounts.length === 0) { throw new Error(`Origin does not have 'eth_accounts' permission`); } else if (!oldPermittedAccounts.includes(account)) { throw new Error('Account is not permitted for origin'); @@ -612,7 +612,7 @@ export class PermissionsController { * Get current set of permitted accounts for the given origin * * @param {string} origin - The origin to obtain permitted accounts for - * @returns {Array|null} The list of permitted accounts + * @returns {Array} The list of permitted accounts */ _getPermittedAccounts(origin) { const permittedAccounts = this.permissions @@ -620,7 +620,7 @@ export class PermissionsController { ?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts) ?.value; - return permittedAccounts || null; + return permittedAccounts || []; } /** diff --git a/test/unit/app/controllers/permissions/permissions-controller.test.js b/app/scripts/controllers/permissions/permissions-controller.test.js similarity index 99% rename from test/unit/app/controllers/permissions/permissions-controller.test.js rename to app/scripts/controllers/permissions/permissions-controller.test.js index b36467566..46fa9d10f 100644 --- a/test/unit/app/controllers/permissions/permissions-controller.test.js +++ b/app/scripts/controllers/permissions/permissions-controller.test.js @@ -2,22 +2,20 @@ import { strict as assert } from 'assert'; import { find } from 'lodash'; import sinon from 'sinon'; -import { - METADATA_STORE_KEY, - METADATA_CACHE_MAX_SIZE, -} from '../../../../../app/scripts/controllers/permissions/enums'; - -import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'; - -import { getRequestUserApprovalHelper, grantPermissions } from './helpers'; - import { constants, getters, getNotifyDomain, getNotifyAllDomains, getPermControllerOpts, -} from './mocks'; +} from '../../../../test/mocks/permission-controller'; +import { + getRequestUserApprovalHelper, + grantPermissions, +} from '../../../../test/helpers/permission-controller-helpers'; +import { METADATA_STORE_KEY, METADATA_CACHE_MAX_SIZE } from './enums'; + +import { PermissionsController } from '.'; const { ERRORS, NOTIFICATIONS, PERMS } = getters; diff --git a/test/unit/app/controllers/permissions/permissions-log-controller.test.js b/app/scripts/controllers/permissions/permissions-log-controller.test.js similarity index 98% rename from test/unit/app/controllers/permissions/permissions-log-controller.test.js rename to app/scripts/controllers/permissions/permissions-log-controller.test.js index 05ff47a37..c973319e9 100644 --- a/test/unit/app/controllers/permissions/permissions-log-controller.test.js +++ b/app/scripts/controllers/permissions/permissions-log-controller.test.js @@ -3,16 +3,14 @@ import { ObservableStore } from '@metamask/obs-store'; import nanoid from 'nanoid'; import { useFakeTimers } from 'sinon'; -import PermissionsLogController from '../../../../../app/scripts/controllers/permissions/permissionsLog'; - import { - LOG_LIMIT, - LOG_METHOD_TYPES, -} from '../../../../../app/scripts/controllers/permissions/enums'; - -import { validateActivityEntry } from './helpers'; - -import { constants, getters, noop } from './mocks'; + constants, + getters, + noop, +} from '../../../../test/mocks/permission-controller'; +import { validateActivityEntry } from '../../../../test/helpers/permission-controller-helpers'; +import PermissionsLogController from './permissionsLog'; +import { LOG_LIMIT, LOG_METHOD_TYPES } from './enums'; const { PERMS, RPC_REQUESTS } = getters; @@ -50,7 +48,7 @@ const initMiddleware = (permLog) => { const initClock = () => { // useFakeTimers, is in fact, not a react-hook // eslint-disable-next-line - clock = useFakeTimers(1) + clock = useFakeTimers(1); }; const tearDownClock = () => { diff --git a/test/unit/app/controllers/permissions/permissions-middleware.test.js b/app/scripts/controllers/permissions/permissions-middleware.test.js similarity index 98% rename from test/unit/app/controllers/permissions/permissions-middleware.test.js rename to app/scripts/controllers/permissions/permissions-middleware.test.js index acb89ac05..dc027b1e7 100644 --- a/test/unit/app/controllers/permissions/permissions-middleware.test.js +++ b/app/scripts/controllers/permissions/permissions-middleware.test.js @@ -1,18 +1,19 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import { METADATA_STORE_KEY } from '../../../../../app/scripts/controllers/permissions/enums'; - -import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'; - -import { getUserApprovalPromise, grantPermissions } from './helpers'; - import { constants, getters, getPermControllerOpts, getPermissionsMiddleware, -} from './mocks'; +} from '../../../../test/mocks/permission-controller'; +import { + getUserApprovalPromise, + grantPermissions, +} from '../../../../test/helpers/permission-controller-helpers'; +import { METADATA_STORE_KEY } from './enums'; + +import { PermissionsController } from '.'; const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters; diff --git a/app/scripts/controllers/permissions/permissionsLog.js b/app/scripts/controllers/permissions/permissionsLog.js index d0ab55d23..1f1a80b5f 100644 --- a/app/scripts/controllers/permissions/permissionsLog.js +++ b/app/scripts/controllers/permissions/permissionsLog.js @@ -1,4 +1,4 @@ -import { cloneDeep } from 'lodash'; +import stringify from 'fast-safe-stringify'; import { CAVEAT_NAMES } from '../../../../shared/constants/permissions'; import { HISTORY_STORE_KEY, @@ -151,7 +151,7 @@ export default class PermissionsLogController { ? LOG_METHOD_TYPES.internal : LOG_METHOD_TYPES.restricted, origin: request.origin, - request: cloneDeep(request), + request: stringify(request, null, 2), requestTime: Date.now(), response: null, responseTime: null, @@ -174,7 +174,7 @@ export default class PermissionsLogController { return; } - entry.response = cloneDeep(response); + entry.response = stringify(response, null, 2); entry.responseTime = time; entry.success = !response.error; } diff --git a/test/unit/app/controllers/permissions/restricted-methods.test.js b/app/scripts/controllers/permissions/restricted-methods.test.js similarity index 98% rename from test/unit/app/controllers/permissions/restricted-methods.test.js rename to app/scripts/controllers/permissions/restricted-methods.test.js index a5cf3dc0b..237cce4b5 100644 --- a/test/unit/app/controllers/permissions/restricted-methods.test.js +++ b/app/scripts/controllers/permissions/restricted-methods.test.js @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import pify from 'pify'; -import getRestrictedMethods from '../../../../../app/scripts/controllers/permissions/restrictedMethods'; +import getRestrictedMethods from './restrictedMethods'; describe('restricted methods', function () { describe('eth_accounts', function () { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 240aaaa18..9fe84c3ff 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -378,7 +378,7 @@ export default class PreferencesController { */ async addToken(rawAddress, symbol, decimals, image) { const address = normalizeAddress(rawAddress); - const newEntry = { address, symbol, decimals }; + const newEntry = { address, symbol, decimals: Number(decimals) }; const { tokens, hiddenTokens } = this.store.getState(); const assetImages = this.getAssetImages(); const updatedHiddenTokens = hiddenTokens.filter( @@ -793,9 +793,14 @@ export default class PreferencesController { if (typeof symbol !== 'string') { throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`); } - if (!(symbol.length < 7)) { + if (!(symbol.length > 0)) { throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": longer than 6 characters.`, + `Invalid symbol "${symbol}": shorter than a character.`, + ); + } + if (!(symbol.length < 12)) { + throw ethErrors.rpc.invalidParams( + `Invalid symbol "${symbol}": longer than 11 characters.`, ); } const numDecimals = parseInt(decimals, 10); diff --git a/test/unit/app/controllers/preferences-controller.test.js b/app/scripts/controllers/preferences.test.js similarity index 97% rename from test/unit/app/controllers/preferences-controller.test.js rename to app/scripts/controllers/preferences.test.js index b56a6d65c..1c765a92b 100644 --- a/test/unit/app/controllers/preferences-controller.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -1,10 +1,10 @@ import assert from 'assert'; import sinon from 'sinon'; -import PreferencesController from '../../../../app/scripts/controllers/preferences'; import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, -} from '../../../../shared/constants/network'; +} from '../../../shared/constants/network'; +import PreferencesController from './preferences'; describe('preferences controller', function () { let preferencesController; @@ -539,6 +539,14 @@ describe('preferences controller', function () { decimals: 0, }), ); + assert.doesNotThrow(() => + validate({ + address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', + symbol: 'ABCDEFGHIJK', + decimals: 0, + }), + ); + assert.throws( () => validate({ symbol: 'ABC', decimals: 0 }), 'missing address should fail', @@ -563,10 +571,19 @@ describe('preferences controller', function () { () => validate({ address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABCDEFGHI', + symbol: 'ABCDEFGHIJKLM', + decimals: 0, + }), + 'long symbol should fail', + ); + assert.throws( + () => + validate({ + address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', + symbol: '', decimals: 0, }), - 'invalid symbol should fail', + 'empty symbol should fail', ); assert.throws( () => diff --git a/test/unit/app/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js similarity index 99% rename from test/unit/app/controllers/swaps.test.js rename to app/scripts/controllers/swaps.test.js index 3109fe509..7efdfbf01 100644 --- a/test/unit/app/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -9,13 +9,11 @@ import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, MAINNET_CHAIN_ID, -} from '../../../../shared/constants/network'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps'; -import { createTestProviderTools } from '../../../stub/provider'; -import SwapsController, { - utils, -} from '../../../../app/scripts/controllers/swaps'; -import { NETWORK_EVENTS } from '../../../../app/scripts/controllers/network'; +} from '../../../shared/constants/network'; +import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; +import { createTestProviderTools } from '../../../test/stub/provider'; +import SwapsController, { utils } from './swaps'; +import { NETWORK_EVENTS } from './network'; const MOCK_FETCH_PARAMS = { slippage: 3, diff --git a/test/unit/app/controllers/token-rates-controller.test.js b/app/scripts/controllers/token-rates-controller.test.js similarity index 92% rename from test/unit/app/controllers/token-rates-controller.test.js rename to app/scripts/controllers/token-rates-controller.test.js index 6c23abbaf..fc5d3af52 100644 --- a/test/unit/app/controllers/token-rates-controller.test.js +++ b/app/scripts/controllers/token-rates-controller.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import sinon from 'sinon'; import { ObservableStore } from '@metamask/obs-store'; -import TokenRatesController from '../../../../app/scripts/controllers/token-rates'; +import TokenRatesController from './token-rates'; describe('TokenRatesController', function () { let nativeCurrency; diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 031ed7863..3b0b9465d 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -19,7 +19,6 @@ import { import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'; import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/app/pages/swaps/swaps.util'; import { - TRANSACTION_CATEGORIES, TRANSACTION_STATUSES, TRANSACTION_TYPES, } from '../../../../shared/constants/transaction'; @@ -151,8 +150,8 @@ export default class TransactionController extends EventEmitter { Adds a tx to the txlist @emits ${txMeta.id}:unapproved */ - addTx(txMeta) { - this.txStateManager.addTx(txMeta); + addTransaction(txMeta) { + this.txStateManager.addTransaction(txMeta); this.emit(`${txMeta.id}:unapproved`, txMeta); } @@ -235,11 +234,10 @@ export default class TransactionController extends EventEmitter { `generateTxMeta` adds the default txMeta properties to the passed object. These include the tx's `id`. As we use the id for determining order of txes in the tx-state-manager, it is necessary to call the asynchronous - method `this._determineTransactionCategory` after `generateTxMeta`. + method `this._determineTransactionType` after `generateTxMeta`. */ let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, - type: TRANSACTION_TYPES.STANDARD, }); if (origin === 'metamask') { @@ -265,33 +263,38 @@ export default class TransactionController extends EventEmitter { txMeta.origin = origin; - const { - transactionCategory, - getCodeResponse, - } = await this._determineTransactionCategory(txParams); - txMeta.transactionCategory = transactionCategory; + const { type, getCodeResponse } = await this._determineTransactionType( + txParams, + ); + txMeta.type = type; // ensure value txMeta.txParams.value = txMeta.txParams.value ? addHexPrefix(txMeta.txParams.value) : '0x0'; - this.addTx(txMeta); + this.addTransaction(txMeta); this.emit('newUnapprovedTx', txMeta); try { txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse); } catch (error) { log.warn(error); - txMeta = this.txStateManager.getTx(txMeta.id); + txMeta = this.txStateManager.getTransaction(txMeta.id); txMeta.loadingDefaults = false; - this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.'); + this.txStateManager.updateTransaction( + txMeta, + 'Failed to calculate gas defaults.', + ); throw error; } txMeta.loadingDefaults = false; // save txMeta - this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.'); + this.txStateManager.updateTransaction( + txMeta, + 'Added new unapproved transaction.', + ); return txMeta; } @@ -309,7 +312,7 @@ export default class TransactionController extends EventEmitter { } = await this._getDefaultGasLimit(txMeta, getCodeResponse); // eslint-disable-next-line no-param-reassign - txMeta = this.txStateManager.getTx(txMeta.id); + txMeta = this.txStateManager.getTransaction(txMeta.id); if (simulationFails) { txMeta.simulationFails = simulationFails; } @@ -347,7 +350,7 @@ export default class TransactionController extends EventEmitter { return {}; } else if ( txMeta.txParams.to && - txMeta.transactionCategory === TRANSACTION_CATEGORIES.SENT_ETHER + txMeta.type === TRANSACTION_TYPES.SENT_ETHER ) { // if there's data in the params, but there's no contract code, it's not a valid transaction if (txMeta.txParams.data) { @@ -388,8 +391,8 @@ export default class TransactionController extends EventEmitter { * @param {string} [customGasPrice] - the hex value to use for the cancel transaction * @returns {txMeta} */ - async createCancelTransaction(originalTxId, customGasPrice) { - const originalTxMeta = this.txStateManager.getTx(originalTxId); + async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) { + const originalTxMeta = this.txStateManager.getTransaction(originalTxId); const { txParams } = originalTxMeta; const { gasPrice: lastGasPrice, from, nonce } = txParams; @@ -401,7 +404,7 @@ export default class TransactionController extends EventEmitter { from, to: from, nonce, - gas: '0x5208', + gas: customGasLimit || '0x5208', value: '0x0', gasPrice: newGasPrice, }, @@ -411,7 +414,7 @@ export default class TransactionController extends EventEmitter { type: TRANSACTION_TYPES.CANCEL, }); - this.addTx(newTxMeta); + this.addTransaction(newTxMeta); await this.approveTransaction(newTxMeta.id); return newTxMeta; } @@ -427,7 +430,7 @@ export default class TransactionController extends EventEmitter { * @returns {txMeta} */ async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) { - const originalTxMeta = this.txStateManager.getTx(originalTxId); + const originalTxMeta = this.txStateManager.getTransaction(originalTxId); const { txParams } = originalTxMeta; const { gasPrice: lastGasPrice } = txParams; @@ -450,7 +453,7 @@ export default class TransactionController extends EventEmitter { newTxMeta.txParams.gas = customGasLimit; } - this.addTx(newTxMeta); + this.addTransaction(newTxMeta); await this.approveTransaction(newTxMeta.id); return newTxMeta; } @@ -460,7 +463,10 @@ export default class TransactionController extends EventEmitter { @param {Object} txMeta - the updated txMeta */ async updateTransaction(txMeta) { - this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction'); + this.txStateManager.updateTransaction( + txMeta, + 'confTx: user updated transaction', + ); } /** @@ -468,7 +474,10 @@ export default class TransactionController extends EventEmitter { @param {Object} txMeta */ async updateAndApproveTransaction(txMeta) { - this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction'); + this.txStateManager.updateTransaction( + txMeta, + 'confTx: user approved transaction', + ); await this.approveTransaction(txMeta.id); } @@ -495,7 +504,7 @@ export default class TransactionController extends EventEmitter { // approve this.txStateManager.setTxStatusApproved(txId); // get next nonce - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); const fromAddress = txMeta.txParams.from; // wait for a nonce let { customNonceValue } = txMeta; @@ -516,7 +525,10 @@ export default class TransactionController extends EventEmitter { if (customNonceValue) { txMeta.nonceDetails.customNonceValue = customNonceValue; } - this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction'); + this.txStateManager.updateTransaction( + txMeta, + 'transactions#approveTransaction', + ); // sign transaction const rawTx = await this.signTransaction(txId); await this.publishTransaction(txId, rawTx); @@ -546,7 +558,7 @@ export default class TransactionController extends EventEmitter { @returns {string} rawTx */ async signTransaction(txId) { - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); // add network/chain id const chainId = this.getChainId(); const txParams = { ...txMeta.txParams, chainId }; @@ -561,7 +573,7 @@ export default class TransactionController extends EventEmitter { txMeta.s = ethUtil.bufferToHex(ethTx.s); txMeta.v = ethUtil.bufferToHex(ethTx.v); - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions#signTransaction: add r, s, v values', ); @@ -579,13 +591,16 @@ export default class TransactionController extends EventEmitter { @returns {Promise} */ async publishTransaction(txId, rawTx) { - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); txMeta.rawTx = rawTx; - if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) { + if (txMeta.type === TRANSACTION_TYPES.SWAP) { const preTxBalance = await this.query.getBalance(txMeta.txParams.from); txMeta.preTxBalance = preTxBalance.toString(16); } - this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction'); + this.txStateManager.updateTransaction( + txMeta, + 'transactions#publishTransaction', + ); let txHash; try { txHash = await this.query.sendRawTransaction(rawTx); @@ -611,7 +626,7 @@ export default class TransactionController extends EventEmitter { async confirmTransaction(txId, txReceipt) { // get the txReceipt before marking the transaction confirmed // to ensure the receipt is gotten before the ui revives the tx - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); if (!txMeta) { return; @@ -632,22 +647,22 @@ export default class TransactionController extends EventEmitter { this.txStateManager.setTxStatusConfirmed(txId); this._markNonceDuplicatesDropped(txId); - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions#confirmTransaction - add txReceipt', ); - if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) { + if (txMeta.type === TRANSACTION_TYPES.SWAP) { const postTxBalance = await this.query.getBalance(txMeta.txParams.from); - const latestTxMeta = this.txStateManager.getTx(txId); + const latestTxMeta = this.txStateManager.getTransaction(txId); const approvalTxMeta = latestTxMeta.approvalTxId - ? this.txStateManager.getTx(latestTxMeta.approvalTxId) + ? this.txStateManager.getTransaction(latestTxMeta.approvalTxId) : null; latestTxMeta.postTxBalance = postTxBalance.toString(16); - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( latestTxMeta, 'transactions#confirmTransaction - add postTxBalance', ); @@ -675,9 +690,9 @@ export default class TransactionController extends EventEmitter { */ setTxHash(txId, txHash) { // Add the tx hash to the persisted meta-tx object - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); txMeta.hash = txHash; - this.txStateManager.updateTx(txMeta, 'transactions#setTxHash'); + this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash'); } // @@ -707,8 +722,7 @@ export default class TransactionController extends EventEmitter { this.txStateManager.getPendingTransactions(account).length; /** see txStateManager */ - this.getFilteredTxList = (opts) => - this.txStateManager.getFilteredTxList(opts); + this.getTransactions = (opts) => this.txStateManager.getTransactions(opts); } // called once on startup @@ -727,23 +741,25 @@ export default class TransactionController extends EventEmitter { _onBootCleanUp() { this.txStateManager - .getFilteredTxList({ - status: TRANSACTION_STATUSES.UNAPPROVED, - loadingDefaults: true, + .getTransactions({ + searchCriteria: { + status: TRANSACTION_STATUSES.UNAPPROVED, + loadingDefaults: true, + }, }) .forEach((tx) => { this.addTxGasDefaults(tx) .then((txMeta) => { txMeta.loadingDefaults = false; - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions: gas estimation for tx on boot', ); }) .catch((error) => { - const txMeta = this.txStateManager.getTx(tx.id); + const txMeta = this.txStateManager.getTransaction(tx.id); txMeta.loadingDefaults = false; - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'failed to estimate gas during boot cleanup.', ); @@ -752,8 +768,10 @@ export default class TransactionController extends EventEmitter { }); this.txStateManager - .getFilteredTxList({ - status: TRANSACTION_STATUSES.APPROVED, + .getTransactions({ + searchCriteria: { + status: TRANSACTION_STATUSES.APPROVED, + }, }) .forEach((txMeta) => { const txSignError = new Error( @@ -774,7 +792,7 @@ export default class TransactionController extends EventEmitter { ); this._setupBlockTrackerListener(); this.pendingTxTracker.on('tx:warning', (txMeta) => { - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions/pending-tx-tracker#event: tx:warning', ); @@ -793,7 +811,7 @@ export default class TransactionController extends EventEmitter { this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { if (!txMeta.firstRetryBlockNumber) { txMeta.firstRetryBlockNumber = latestBlockNumber; - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions/pending-tx-tracker#event: tx:block-update', ); @@ -804,7 +822,7 @@ export default class TransactionController extends EventEmitter { txMeta.retryCount = 0; } txMeta.retryCount += 1; - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions/pending-tx-tracker#event: tx:retry', ); @@ -812,10 +830,27 @@ export default class TransactionController extends EventEmitter { } /** - Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove, - contractDeployment, contractMethodCall - */ - async _determineTransactionCategory(txParams) { + * @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'sentEther' } InferrableTransactionTypes + */ + + /** + * @typedef {Object} InferTransactionTypeResult + * @property {InferrableTransactionTypes} type - The type of transaction + * @property {string} getCodeResponse - The contract code, in hex format if + * it exists. '0x0' or '0x' are also indicators of non-existent contract + * code + */ + + /** + * Determines the type of the transaction by analyzing the txParams. + * This method will return one of the types defined in shared/constants/transactions + * It will never return TRANSACTION_TYPE_CANCEL or TRANSACTION_TYPE_RETRY as these + * represent specific events that we control from the extension and are added manually + * at transaction creation. + * @param {Object} txParams - Parameters for the transaction + * @returns {InferTransactionTypeResult} + */ + async _determineTransactionType(txParams) { const { data, to } = txParams; let name; try { @@ -825,16 +860,16 @@ export default class TransactionController extends EventEmitter { } const tokenMethodName = [ - TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE, - TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, - TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM, + TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM, ].find((methodName) => methodName === name && name.toLowerCase()); let result; if (data && tokenMethodName) { result = tokenMethodName; } else if (data && !to) { - result = TRANSACTION_CATEGORIES.DEPLOY_CONTRACT; + result = TRANSACTION_TYPES.DEPLOY_CONTRACT; } let code; @@ -849,11 +884,11 @@ export default class TransactionController extends EventEmitter { const codeIsEmpty = !code || code === '0x' || code === '0x0'; result = codeIsEmpty - ? TRANSACTION_CATEGORIES.SENT_ETHER - : TRANSACTION_CATEGORIES.CONTRACT_INTERACTION; + ? TRANSACTION_TYPES.SENT_ETHER + : TRANSACTION_TYPES.CONTRACT_INTERACTION; } - return { transactionCategory: result, getCodeResponse: code }; + return { type: result, getCodeResponse: code }; } /** @@ -864,9 +899,11 @@ export default class TransactionController extends EventEmitter { */ _markNonceDuplicatesDropped(txId) { // get the confirmed transactions nonce and from address - const txMeta = this.txStateManager.getTx(txId); + const txMeta = this.txStateManager.getTransaction(txId); const { nonce, from } = txMeta.txParams; - const sameNonceTxs = this.txStateManager.getFilteredTxList({ nonce, from }); + const sameNonceTxs = this.txStateManager.getTransactions({ + searchCriteria: { nonce, from }, + }); if (!sameNonceTxs.length) { return; } @@ -876,7 +913,7 @@ export default class TransactionController extends EventEmitter { return; } otherTxMeta.replacedBy = txMeta.hash; - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce', ); @@ -922,9 +959,9 @@ export default class TransactionController extends EventEmitter { */ _updateMemstore() { const unapprovedTxs = this.txStateManager.getUnapprovedTxList(); - const currentNetworkTxList = this.txStateManager.getTxList( - MAX_MEMSTORE_TX_LIST_SIZE, - ); + const currentNetworkTxList = this.txStateManager.getTransactions({ + limit: MAX_MEMSTORE_TX_LIST_SIZE, + }); this.memStore.updateState({ unapprovedTxs, currentNetworkTxList }); } diff --git a/test/unit/app/controllers/transactions/tx-controller.test.js b/app/scripts/controllers/transactions/index.test.js similarity index 76% rename from test/unit/app/controllers/transactions/tx-controller.test.js rename to app/scripts/controllers/transactions/index.test.js index 2dacf3c6a..2d1709cdf 100644 --- a/test/unit/app/controllers/transactions/tx-controller.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -4,23 +4,25 @@ import ethUtil from 'ethereumjs-util'; import EthTx from 'ethereumjs-tx'; import { ObservableStore } from '@metamask/obs-store'; import sinon from 'sinon'; -import TransactionController from '../../../../../app/scripts/controllers/transactions'; import { createTestProviderTools, getTestAccounts, -} from '../../../../stub/provider'; +} from '../../../../test/stub/provider'; import { - TRANSACTION_CATEGORIES, TRANSACTION_STATUSES, TRANSACTION_TYPES, -} from '../../../../../shared/constants/transaction'; -import { METAMASK_CONTROLLER_EVENTS } from '../../../../../app/scripts/metamask-controller'; +} from '../../../../shared/constants/transaction'; +import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; +import TransactionController from '.'; const noop = () => true; const currentNetworkId = '42'; const currentChainId = '0x2a'; +const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; +const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; + describe('Transaction Controller', function () { let txController, provider, providerResultStub, fromAccount; @@ -82,26 +84,35 @@ describe('Transaction Controller', function () { describe('#getUnapprovedTxCount', function () { it('should return the number of unapproved txs', function () { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 2, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 3, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, ]); @@ -112,26 +123,35 @@ describe('Transaction Controller', function () { describe('#getPendingTxCount', function () { it('should return the number of pending txs', function () { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 2, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 3, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, ]); @@ -147,7 +167,7 @@ describe('Transaction Controller', function () { from: address, to: '0xc684832530fcbddae4b4230a47e991ddcec2831d', }; - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 0, status: TRANSACTION_STATUSES.CONFIRMED, @@ -233,17 +253,19 @@ describe('Transaction Controller', function () { txParams, history: [{}], }; - txController.txStateManager._saveTxList([txMeta]); + txController.txStateManager._addTransactionsToState([txMeta]); stub = sinon .stub(txController, 'addUnapprovedTransaction') .callsFake(() => { txController.emit('newUnapprovedTx', txMeta); - return Promise.resolve(txController.txStateManager.addTx(txMeta)); + return Promise.resolve( + txController.txStateManager.addTransaction(txMeta), + ); }); }); afterEach(function () { - txController.txStateManager._saveTxList([]); + txController.txStateManager._addTransactionsToState([]); stub.restore(); }); @@ -313,7 +335,7 @@ describe('Transaction Controller', function () { 'should have added 0x0 as the value', ); - const memTxMeta = txController.txStateManager.getTx(txMeta.id); + const memTxMeta = txController.txStateManager.getTransaction(txMeta.id); assert.deepEqual(txMeta, memTxMeta); }); @@ -354,12 +376,15 @@ describe('Transaction Controller', function () { describe('#addTxGasDefaults', function () { it('should add the tx defaults if their are none', async function () { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, ]); @@ -387,13 +412,16 @@ describe('Transaction Controller', function () { }); }); - describe('#addTx', function () { + describe('#addTransaction', function () { it('should emit updates', function (done) { const txMeta = { id: '1', status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, }; const eventNames = [ @@ -420,7 +448,7 @@ describe('Transaction Controller', function () { done(); }) .catch(done); - txController.addTx(txMeta); + txController.addTransaction(txMeta); }); }); @@ -432,6 +460,8 @@ describe('Transaction Controller', function () { status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, nonce: originalValue, gas: originalValue, gasPrice: originalValue, @@ -441,7 +471,7 @@ describe('Transaction Controller', function () { this.timeout(15000); const wrongValue = '0x05'; - txController.addTx(txMeta); + txController.addTransaction(txMeta); providerResultStub.eth_gasPrice = wrongValue; providerResultStub.eth_estimateGas = '0x5209'; @@ -457,7 +487,7 @@ describe('Transaction Controller', function () { }); await txController.approveTransaction(txMeta.id); - const result = txController.txStateManager.getTx(txMeta.id); + const result = txController.txStateManager.getTransaction(txMeta.id); const params = result.txParams; assert.equal(params.gas, originalValue, 'gas unmodified'); @@ -475,12 +505,15 @@ describe('Transaction Controller', function () { describe('#sign replay-protected tx', function () { it('prepares a tx with the chainId set', async function () { - txController.addTx( + txController.addTransaction( { id: '1', status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, }, noop, ); @@ -504,9 +537,9 @@ describe('Transaction Controller', function () { }, metamaskNetworkId: currentNetworkId, }; - txController.txStateManager.addTx(txMeta); + txController.txStateManager.addTransaction(txMeta); const approvalPromise = txController.updateAndApproveTransaction(txMeta); - const tx = txController.txStateManager.getTx(1); + const tx = txController.txStateManager.getTransaction(1); assert.equal(tx.status, TRANSACTION_STATUSES.APPROVED); await approvalPromise; }); @@ -521,53 +554,74 @@ describe('Transaction Controller', function () { describe('#cancelTransaction', function () { it('should emit a status change to rejected', function (done) { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 0, status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 1, status: TRANSACTION_STATUSES.REJECTED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 2, status: TRANSACTION_STATUSES.APPROVED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 3, status: TRANSACTION_STATUSES.SIGNED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 4, status: TRANSACTION_STATUSES.SUBMITTED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 5, status: TRANSACTION_STATUSES.CONFIRMED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, { id: 6, status: TRANSACTION_STATUSES.FAILED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, history: [{}], }, @@ -592,13 +646,13 @@ describe('Transaction Controller', function () { }); describe('#createSpeedUpTransaction', function () { - let addTxSpy; + let addTransactionSpy; let approveTransactionSpy; let txParams; let expectedTxParams; beforeEach(function () { - addTxSpy = sinon.spy(txController, 'addTx'); + addTransactionSpy = sinon.spy(txController, 'addTransaction'); approveTransactionSpy = sinon.spy(txController, 'approveTransaction'); txParams = { @@ -608,7 +662,7 @@ describe('Transaction Controller', function () { gas: '0x5209', gasPrice: '0xa', }; - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.SUBMITTED, @@ -622,18 +676,18 @@ describe('Transaction Controller', function () { }); afterEach(function () { - addTxSpy.restore(); + addTransactionSpy.restore(); approveTransactionSpy.restore(); }); - it('should call this.addTx and this.approveTransaction with the expected args', async function () { + it('should call this.addTransaction and this.approveTransaction with the expected args', async function () { await txController.createSpeedUpTransaction(1); - assert.equal(addTxSpy.callCount, 1); + assert.equal(addTransactionSpy.callCount, 1); - const addTxArgs = addTxSpy.getCall(0).args[0]; - assert.deepEqual(addTxArgs.txParams, expectedTxParams); + const addTransactionArgs = addTransactionSpy.getCall(0).args[0]; + assert.deepEqual(addTransactionArgs.txParams, expectedTxParams); - const { lastGasPrice, type } = addTxArgs; + const { lastGasPrice, type } = addTransactionArgs; assert.deepEqual( { lastGasPrice, type }, { @@ -675,7 +729,10 @@ describe('Transaction Controller', function () { txMeta = { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, metamaskNetworkId: currentNetworkId, }; providerResultStub.eth_sendRawTransaction = hash; @@ -684,9 +741,9 @@ describe('Transaction Controller', function () { it('should publish a tx, updates the rawTx when provided a one', async function () { const rawTx = '0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c'; - txController.txStateManager.addTx(txMeta); + txController.txStateManager.addTransaction(txMeta); await txController.publishTransaction(txMeta.id, rawTx); - const publishedTx = txController.txStateManager.getTx(1); + const publishedTx = txController.txStateManager.getTransaction(1); assert.equal(publishedTx.hash, hash); assert.equal(publishedTx.status, TRANSACTION_STATUSES.SUBMITTED); }); @@ -697,9 +754,9 @@ describe('Transaction Controller', function () { }; const rawTx = '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a'; - txController.txStateManager.addTx(txMeta); + txController.txStateManager.addTransaction(txMeta); await txController.publishTransaction(txMeta.id, rawTx); - const publishedTx = txController.txStateManager.getTx(1); + const publishedTx = txController.txStateManager.getTransaction(1); assert.equal( publishedTx.hash, '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', @@ -710,62 +767,92 @@ describe('Transaction Controller', function () { describe('#_markNonceDuplicatesDropped', function () { it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 2, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 3, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 4, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 5, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 6, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, { id: 7, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, history: [{}], - txParams: { nonce: '0x01' }, + txParams: { + to: VALID_ADDRESS_TWO, + from: VALID_ADDRESS, + nonce: '0x01', + }, }, ]); txController._markNonceDuplicatesDropped(1); - const confirmedTx = txController.txStateManager.getTx(1); - const droppedTxs = txController.txStateManager.getFilteredTxList({ - nonce: '0x01', - status: TRANSACTION_STATUSES.DROPPED, + const confirmedTx = txController.txStateManager.getTransaction(1); + const droppedTxs = txController.txStateManager.getTransactions({ + searchCriteria: { + nonce: '0x01', + status: TRANSACTION_STATUSES.DROPPED, + }, }); assert.equal( confirmedTx.status, @@ -776,76 +863,76 @@ describe('Transaction Controller', function () { }); }); - describe('#_determineTransactionCategory', function () { - it('should return a simple send transactionCategory when to is truthy but data is falsy', async function () { - const result = await txController._determineTransactionCategory({ + describe('#_determineTransactionType', function () { + it('should return a simple send type when to is truthy but data is falsy', async function () { + const result = await txController._determineTransactionType({ to: '0xabc', data: '', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER, + type: TRANSACTION_TYPES.SENT_ETHER, getCodeResponse: null, }); }); - it('should return a token transfer transactionCategory when data is for the respective method call', async function () { - const result = await txController._determineTransactionCategory({ + it('should return a token transfer type when data is for the respective method call', async function () { + const result = await txController._determineTransactionType({ to: '0xabc', data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, + type: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, getCodeResponse: undefined, }); }); - it('should return a token approve transactionCategory when data is for the respective method call', async function () { - const result = await txController._determineTransactionCategory({ + it('should return a token approve type when data is for the respective method call', async function () { + const result = await txController._determineTransactionType({ to: '0xabc', data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE, + type: TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, getCodeResponse: undefined, }); }); - it('should return a contract deployment transactionCategory when to is falsy and there is data', async function () { - const result = await txController._determineTransactionCategory({ + it('should return a contract deployment type when to is falsy and there is data', async function () { + const result = await txController._determineTransactionType({ to: '', data: '0xabd', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.DEPLOY_CONTRACT, + type: TRANSACTION_TYPES.DEPLOY_CONTRACT, getCodeResponse: undefined, }); }); - it('should return a simple send transactionCategory with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () { - const result = await txController._determineTransactionCategory({ + it('should return a simple send type with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () { + const result = await txController._determineTransactionType({ to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', data: '0xabd', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER, + type: TRANSACTION_TYPES.SENT_ETHER, getCodeResponse: '0x', }); }); - it('should return a simple send transactionCategory with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () { - const result = await txController._determineTransactionCategory({ + it('should return a simple send type with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () { + const result = await txController._determineTransactionType({ to: '0xabc', data: '0xabd', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER, + type: TRANSACTION_TYPES.SENT_ETHER, getCodeResponse: null, }); }); - it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () { + it('should return a contract interaction type with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () { const _providerResultStub = { // 1 gwei eth_gasPrice: '0x0de0b6b3a7640000', @@ -875,17 +962,17 @@ describe('Transaction Controller', function () { }), getParticipateInMetrics: () => false, }); - const result = await _txController._determineTransactionCategory({ + const result = await _txController._determineTransactionType({ to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', data: 'abd', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + type: TRANSACTION_TYPES.CONTRACT_INTERACTION, getCodeResponse: '0x0a', }); }); - it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is a contract address and data is falsy', async function () { + it('should return a contract interaction type with the correct getCodeResponse when to is a contract address and data is falsy', async function () { const _providerResultStub = { // 1 gwei eth_gasPrice: '0x0de0b6b3a7640000', @@ -915,12 +1002,12 @@ describe('Transaction Controller', function () { }), getParticipateInMetrics: () => false, }); - const result = await _txController._determineTransactionCategory({ + const result = await _txController._determineTransactionType({ to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', data: '', }); assert.deepEqual(result, { - transactionCategory: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + type: TRANSACTION_TYPES.CONTRACT_INTERACTION, getCodeResponse: '0x0a', }); }); @@ -928,53 +1015,74 @@ describe('Transaction Controller', function () { describe('#getPendingTransactions', function () { it('should show only submitted and approved transactions as pending transaction', function () { - txController.txStateManager._saveTxList([ + txController.txStateManager._addTransactionsToState([ { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, }, { id: 2, status: TRANSACTION_STATUSES.REJECTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 3, status: TRANSACTION_STATUSES.APPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 4, status: TRANSACTION_STATUSES.SIGNED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 5, status: TRANSACTION_STATUSES.SUBMITTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 6, status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, { id: 7, status: TRANSACTION_STATUSES.FAILED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS_TWO, + }, history: [{}], }, ]); diff --git a/test/unit/app/controllers/transactions/tx-state-history-helpers.test.js b/app/scripts/controllers/transactions/lib/tx-state-history-helpers.test.js similarity index 96% rename from test/unit/app/controllers/transactions/tx-state-history-helpers.test.js rename to app/scripts/controllers/transactions/lib/tx-state-history-helpers.test.js index 6ff852bae..5e22b5d69 100644 --- a/test/unit/app/controllers/transactions/tx-state-history-helpers.test.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helpers.test.js @@ -1,11 +1,11 @@ import { strict as assert } from 'assert'; +import testData from '../../../../../test/data/mock-tx-history.json'; import { snapshotFromTxMeta, migrateFromSnapshotsToDiffs, replayHistory, generateHistoryEntry, -} from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helpers'; -import testData from '../../../../data/mock-tx-history.json'; +} from './tx-state-history-helpers'; describe('Transaction state history helper', function () { describe('#snapshotFromTxMeta', function () { diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index f1cb1f661..70652a3c1 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -14,6 +14,12 @@ const normalizers = { gasPrice: (gasPrice) => addHexPrefix(gasPrice), }; +export function normalizeAndValidateTxParams(txParams, lowerCase = true) { + const normalizedTxParams = normalizeTxParams(txParams, lowerCase); + validateTxParams(normalizedTxParams); + return normalizedTxParams; +} + /** * Normalizes the given txParams * @param {Object} txParams - The transaction params @@ -49,22 +55,48 @@ export function validateTxParams(txParams) { ); } - validateFrom(txParams); - validateRecipient(txParams); - if ('value' in txParams) { - const value = txParams.value.toString(); - if (value.includes('-')) { - throw ethErrors.rpc.invalidParams( - `Invalid transaction value "${txParams.value}": not a positive number.`, - ); - } + Object.entries(txParams).forEach(([key, value]) => { + // validate types + switch (key) { + case 'from': + validateFrom(txParams); + break; + case 'to': + validateRecipient(txParams); + break; + case 'value': + if (typeof value !== 'string') { + throw ethErrors.rpc.invalidParams( + `Invalid transaction params: ${key} is not a string. got: (${value})`, + ); + } + if (value.toString().includes('-')) { + throw ethErrors.rpc.invalidParams( + `Invalid transaction value "${value}": not a positive number.`, + ); + } - if (value.includes('.')) { - throw ethErrors.rpc.invalidParams( - `Invalid transaction value of "${txParams.value}": number must be in wei.`, - ); + if (value.toString().includes('.')) { + throw ethErrors.rpc.invalidParams( + `Invalid transaction value of "${value}": number must be in wei.`, + ); + } + break; + case 'chainId': + if (typeof value !== 'number' && typeof value !== 'string') { + throw ethErrors.rpc.invalidParams( + `Invalid transaction params: ${key} is not a Number or hex string. got: (${value})`, + ); + } + break; + default: + if (typeof value !== 'string') { + throw ethErrors.rpc.invalidParams( + `Invalid transaction params: ${key} is not a string. got: (${value})`, + ); + } } - } + }); } /** diff --git a/test/unit/app/controllers/transactions/tx-utils.test.js b/app/scripts/controllers/transactions/lib/util.test.js similarity index 98% rename from test/unit/app/controllers/transactions/tx-utils.test.js rename to app/scripts/controllers/transactions/lib/util.test.js index a643d9dea..f6dd83e4a 100644 --- a/test/unit/app/controllers/transactions/tx-utils.test.js +++ b/app/scripts/controllers/transactions/lib/util.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import * as txUtils from '../../../../../app/scripts/controllers/transactions/lib/util'; +import * as txUtils from './util'; describe('txUtils', function () { describe('#validateTxParams', function () { diff --git a/test/unit/app/controllers/transactions/pending-tx-tracker.test.js b/app/scripts/controllers/transactions/pending-tx-tracker.test.js similarity index 99% rename from test/unit/app/controllers/transactions/pending-tx-tracker.test.js rename to app/scripts/controllers/transactions/pending-tx-tracker.test.js index db0f4d1d3..b359b25f9 100644 --- a/test/unit/app/controllers/transactions/pending-tx-tracker.test.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.test.js @@ -1,8 +1,8 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; import BN from 'bn.js'; -import PendingTransactionTracker from '../../../../../app/scripts/controllers/transactions/pending-tx-tracker'; -import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; +import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; +import PendingTransactionTracker from './pending-tx-tracker'; describe('PendingTransactionTracker', function () { describe('#resubmitPendingTxs', function () { diff --git a/test/unit/app/controllers/transactions/tx-gas-util.test.js b/app/scripts/controllers/transactions/tx-gas-utils.test.js similarity index 94% rename from test/unit/app/controllers/transactions/tx-gas-util.test.js rename to app/scripts/controllers/transactions/tx-gas-utils.test.js index d3e9cfddf..635802925 100644 --- a/test/unit/app/controllers/transactions/tx-gas-util.test.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.test.js @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import Transaction from 'ethereumjs-tx'; -import { hexToBn, bnToHex } from '../../../../../app/scripts/lib/util'; -import TxUtils from '../../../../../app/scripts/controllers/transactions/tx-gas-utils'; +import { hexToBn, bnToHex } from '../../lib/util'; +import TxUtils from './tx-gas-utils'; describe('txUtils', function () { let txUtils; diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index c62a97d1e..9eaa260a1 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,7 +1,8 @@ import EventEmitter from 'safe-event-emitter'; import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; -import createId from '../../lib/random-id'; +import { keyBy, mapValues, omitBy, pickBy, sortBy } from 'lodash'; +import createId from '../../../../shared/modules/random-id'; import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils'; @@ -10,11 +11,29 @@ import { replayHistory, snapshotFromTxMeta, } from './lib/tx-state-history-helpers'; -import { getFinalStates, normalizeTxParams } from './lib/util'; +import { getFinalStates, normalizeAndValidateTxParams } from './lib/util'; /** * TransactionStatuses reimported from the shared transaction constants file - * @typedef {import('../../../../shared/constants/transaction').TransactionStatuses} TransactionStatuses + * @typedef {import( + * '../../../../shared/constants/transaction' + * ).TransactionStatusString} TransactionStatusString + */ + +/** + * @typedef {import('../../../../shared/constants/transaction').TxParams} TxParams + */ + +/** + * @typedef {import( + * '../../../../shared/constants/transaction' + * ).TransactionMeta} TransactionMeta + */ + +/** + * @typedef {Object} TransactionState + * @property {Record} transactions - TransactionMeta + * keyed by the transaction's id. */ /** @@ -22,7 +41,8 @@ import { getFinalStates, normalizeTxParams } from './lib/util'; * storing the transaction. It also has some convenience methods for finding * subsets of transactions. * @param {Object} opts - * @param {Object} [opts.initState={ transactions: [] }] - initial transactions list with the key transaction {Array} + * @param {TransactionState} [opts.initState={ transactions: {} }] - initial + * transactions list keyed by id * @param {number} [opts.txHistoryLimit] - limit for how many finished * transactions can hang around in state * @param {Function} opts.getNetwork - return network number @@ -32,15 +52,25 @@ export default class TransactionStateManager extends EventEmitter { constructor({ initState, txHistoryLimit, getNetwork, getCurrentChainId }) { super(); - this.store = new ObservableStore({ transactions: [], ...initState }); + this.store = new ObservableStore({ + transactions: {}, + ...initState, + }); this.txHistoryLimit = txHistoryLimit; this.getNetwork = getNetwork; this.getCurrentChainId = getCurrentChainId; } /** - * @param {Object} opts - the object to use when overwriting defaults - * @returns {txMeta} the default txMeta object + * Generates a TransactionMeta object consisting of the fields required for + * use throughout the extension. The argument here will override everything + * in the resulting transaction meta. + * + * TODO: Don't overwrite everything? + * + * @param {Partial} opts - the object to use when + * overwriting default keys of the TransactionMeta + * @returns {TransactionMeta} the default txMeta object */ generateTxMeta(opts) { const netId = this.getNetwork(); @@ -60,100 +90,70 @@ export default class TransactionStateManager extends EventEmitter { } /** - * Returns the full tx list for the current network - * - * The list is iterated backwards as new transactions are pushed onto it. + * Get an object containing all unapproved transactions for the current + * network. This is the only transaction fetching method that returns an + * object, so it doesn't use getTransactions like everything else. * - * @param {number} [limit] - a limit for the number of transactions to return - * @returns {Object[]} The {@code txMeta}s, filtered to the current network - */ - getTxList(limit) { - const network = this.getNetwork(); - const chainId = this.getCurrentChainId(); - const fullTxList = this.getFullTxList(); - - const nonces = new Set(); - const txs = []; - for (let i = fullTxList.length - 1; i > -1; i--) { - const txMeta = fullTxList[i]; - if (transactionMatchesNetwork(txMeta, chainId, network) === false) { - continue; - } - - if (limit !== undefined) { - const { nonce } = txMeta.txParams; - if (!nonces.has(nonce)) { - if (nonces.size < limit) { - nonces.add(nonce); - } else { - continue; - } - } - } - - txs.unshift(txMeta); - } - return txs; - } - - /** - * @returns {Array} of all the txMetas in store - */ - getFullTxList() { - return this.store.getState().transactions; - } - - /** - * @returns {Array} the tx list with unapproved status + * @returns {Record} Unapproved transactions keyed + * by id */ getUnapprovedTxList() { - const txList = this.getTxsByMetaData( - 'status', - TRANSACTION_STATUSES.UNAPPROVED, + const chainId = this.getCurrentChainId(); + const network = this.getNetwork(); + return pickBy( + this.store.getState().transactions, + (transaction) => + transaction.status === TRANSACTION_STATUSES.UNAPPROVED && + transactionMatchesNetwork(transaction, chainId, network), ); - return txList.reduce((result, tx) => { - result[tx.id] = tx; - return result; - }, {}); } /** - * @param {string} [address] - hex prefixed address to sort the txMetas for [optional] - * @returns {Array} the tx list with approved status if no address is provide - * returns all txMetas with approved statuses for the current network + * Get all approved transactions for the current network. If an address is + * provided, the list will be further refined to only those transactions + * originating from the supplied address. + * + * @param {string} [address] - hex prefixed address to find transactions for. + * @returns {TransactionMeta[]} the filtered list of transactions */ getApprovedTransactions(address) { - const opts = { status: TRANSACTION_STATUSES.APPROVED }; + const searchCriteria = { status: TRANSACTION_STATUSES.APPROVED }; if (address) { - opts.from = address; + searchCriteria.from = address; } - return this.getFilteredTxList(opts); + return this.getTransactions({ searchCriteria }); } /** - * @param {string} [address] - hex prefixed address to sort the txMetas for [optional] - * @returns {Array} the tx list submitted status if no address is provide - * returns all txMetas with submitted statuses for the current network + * Get all pending transactions for the current network. If an address is + * provided, the list will be further refined to only those transactions + * originating from the supplied address. + * + * @param {string} [address] - hex prefixed address to find transactions for. + * @returns {TransactionMeta[]} the filtered list of transactions */ getPendingTransactions(address) { - const opts = { status: TRANSACTION_STATUSES.SUBMITTED }; + const searchCriteria = { status: TRANSACTION_STATUSES.SUBMITTED }; if (address) { - opts.from = address; + searchCriteria.from = address; } - return this.getFilteredTxList(opts); + return this.getTransactions({ searchCriteria }); } /** - @param {string} [address] - hex prefixed address to sort the txMetas for [optional] - @returns {Array} the tx list whose status is confirmed if no address is provide - returns all txMetas who's status is confirmed for the current network - */ + * Get all confirmed transactions for the current network. If an address is + * provided, the list will be further refined to only those transactions + * originating from the supplied address. + * + * @param {string} [address] - hex prefixed address to find transactions for. + * @returns {TransactionMeta[]} the filtered list of transactions + */ getConfirmedTransactions(address) { - const opts = { status: TRANSACTION_STATUSES.CONFIRMED }; + const searchCriteria = { status: TRANSACTION_STATUSES.CONFIRMED }; if (address) { - opts.from = address; + searchCriteria.from = address; } - return this.getFilteredTxList(opts); + return this.getTransactions({ searchCriteria }); } /** @@ -162,13 +162,14 @@ export default class TransactionStateManager extends EventEmitter { * is in its final state. * it will also add the key `history` to the txMeta with the snap shot of * the original object - * @param {Object} txMeta - * @returns {Object} the txMeta + * @param {TransactionMeta} txMeta - The TransactionMeta object to add. + * @returns {TransactionMeta} The same TransactionMeta, but with validated + * txParams and transaction history. */ - addTx(txMeta) { + addTransaction(txMeta) { // normalize and validate txParams if present if (txMeta.txParams) { - txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams); + txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false); } this.once(`${txMeta.id}:signed`, () => { @@ -183,42 +184,43 @@ export default class TransactionStateManager extends EventEmitter { const snapshot = snapshotFromTxMeta(txMeta); txMeta.history.push(snapshot); - const transactions = this.getFullTxList(); + const transactions = this.getTransactions({ + filterToCurrentNetwork: false, + }); const txCount = transactions.length; const { txHistoryLimit } = this; - // checks if the length of the tx history is - // longer then desired persistence limit - // and then if it is removes only confirmed - // or rejected tx's. - // not tx's that are pending or unapproved + // checks if the length of the tx history is longer then desired persistence + // limit and then if it is removes the oldest confirmed or rejected tx. + // Pending or unapproved transactions will not be removed by this + // operation. + // + // TODO: we are already limiting what we send to the UI, and in the future + // we will send UI only collected groups of transactions *per page* so at + // some point in the future, this persistence limit can be adjusted. When + // we do that I think we should figure out a better storage solution for + // transaction history entries. if (txCount > txHistoryLimit - 1) { const index = transactions.findIndex((metaTx) => { return getFinalStates().includes(metaTx.status); }); if (index !== -1) { - transactions.splice(index, 1); + this._deleteTransaction(transactions[index].id); } } - const newTxIndex = transactions.findIndex( - (currentTxMeta) => currentTxMeta.time > txMeta.time, - ); - newTxIndex === -1 - ? transactions.push(txMeta) - : transactions.splice(newTxIndex, 0, txMeta); - this._saveTxList(transactions); + this._addTransactionsToState([txMeta]); return txMeta; } /** * @param {number} txId - * @returns {Object} the txMeta who matches the given id if none found + * @returns {TransactionMeta} the txMeta who matches the given id if none found * for the network returns undefined */ - getTx(txId) { - const txMeta = this.getTxsByMetaData('id', txId)[0]; - return txMeta; + getTransaction(txId) { + const { transactions } = this.store.getState(); + return transactions[txId]; } /** @@ -226,10 +228,10 @@ export default class TransactionStateManager extends EventEmitter { * @param {Object} txMeta - the txMeta to update * @param {string} [note] - a note about the update for history */ - updateTx(txMeta, note) { + updateTransaction(txMeta, note) { // normalize and validate txParams if present if (txMeta.txParams) { - txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams); + txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false); } // create txMeta snapshot for history @@ -244,232 +246,277 @@ export default class TransactionStateManager extends EventEmitter { // commit txMeta to state const txId = txMeta.id; - const txList = this.getFullTxList(); - const index = txList.findIndex((txData) => txData.id === txId); - txList[index] = txMeta; - this._saveTxList(txList); + this.store.updateState({ + transactions: { + ...this.store.getState().transactions, + [txId]: txMeta, + }, + }); } /** - * merges txParams obj onto txMeta.txParams use extend to ensure - * that all fields are filled - * @param {number} txId - the id of the txMeta - * @param {Object} txParams - the updated txParams + * SearchCriteria can search in any key in TxParams or the base + * TransactionMeta. This type represents any key on either of those two + * types. + * @typedef {TxParams[keyof TxParams] | TransactionMeta[keyof TransactionMeta]} SearchableKeys */ - updateTxParams(txId, txParams) { - const txMeta = this.getTx(txId); - txMeta.txParams = { ...txMeta.txParams, ...txParams }; - this.updateTx(txMeta, `txStateManager#updateTxParams`); - } /** - * normalize and validate txParams members - * @param {Object} txParams - txParams + * Predicates can either be strict values, which is shorthand for using + * strict equality, or a method that receives he value of the specified key + * and returns a boolean. + * @typedef {(v: unknown) => boolean | unknown} FilterPredicate */ - normalizeAndValidateTxParams(txParams) { - if (typeof txParams.data === 'undefined') { - delete txParams.data; - } - // eslint-disable-next-line no-param-reassign - txParams = normalizeTxParams(txParams, false); - this.validateTxParams(txParams); - return txParams; - } /** - * validates txParams members by type - * @param {Object} txParams - txParams to validate + * Retrieve a list of transactions from state. By default this will return + * the full list of Transactions for the currently selected chain/network. + * Additional options can be provided to change what is included in the final + * list. + * + * @param opts - options to change filter behavior + * @param {Record} [opts.searchCriteria] - + * an object with keys that match keys in TransactionMeta or TxParams, and + * values that are predicates. Predicates can either be strict values, + * which is shorthand for using strict equality, or a method that receives + * the value of the specified key and returns a boolean. The transaction + * list will be filtered to only those items that the predicate returns + * truthy for. **HINT**: `err: undefined` is like looking for a tx with no + * err. so you can also search txs that don't have something as well by + * setting the value as undefined. + * @param {TransactionMeta[]} [opts.initialList] - If provided the filtering + * will occur on the provided list. By default this will be the full list + * from state sorted by time ASC. + * @param {boolean} [opts.filterToCurrentNetwork=true] - Filter transaction + * list to only those that occurred on the current chain or network. + * Defaults to true. + * @param {number} [opts.limit] - limit the number of transactions returned + * to N unique nonces. + * @returns {TransactionMeta[]} The TransactionMeta objects that all provided + * predicates return truthy for. */ - validateTxParams(txParams) { - Object.keys(txParams).forEach((key) => { - const value = txParams[key]; - // validate types - switch (key) { - case 'chainId': - if (typeof value !== 'number' && typeof value !== 'string') { - throw new Error( - `${key} in txParams is not a Number or hex string. got: (${value})`, - ); - } - break; - default: - if (typeof value !== 'string') { - throw new Error( - `${key} in txParams is not a string. got: (${value})`, - ); - } - break; - } - }); - } - - /** - @param {Object} opts - an object of fields to search for eg:
- let thingsToLookFor = {
- to: '0x0..',
- from: '0x0..',
- status: 'signed', \\ (status) => status !== 'rejected' give me all txs who's status is not rejected
- err: undefined,
- }
- optionally the values of the keys can be functions for situations like where - you want all but one status. - @param {Array} [initialList=this.getTxList()] - @returns {Array} array of txMeta with all - options matching - */ - /* - ****************HINT**************** - | `err: undefined` is like looking | - | for a tx with no err | - | so you can also search txs that | - | dont have something as well by | - | setting the value as undefined | - ************************************ - - this is for things like filtering a the tx list - for only tx's from 1 account - or for filtering for all txs from one account - and that have been 'confirmed' - */ - getFilteredTxList(opts, initialList) { - let filteredTxList = initialList; - Object.keys(opts).forEach((key) => { - filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList); + getTransactions({ + searchCriteria = {}, + initialList, + filterToCurrentNetwork = true, + limit, + } = {}) { + const chainId = this.getCurrentChainId(); + const network = this.getNetwork(); + // searchCriteria is an object that might have values that aren't predicate + // methods. When providing any other value type (string, number, etc), we + // consider this shorthand for "check the value at key for strict equality + // with the provided value". To conform this object to be only methods, we + // mapValues (lodash) such that every value on the object is a method that + // returns a boolean. + const predicateMethods = mapValues(searchCriteria, (predicate) => { + return typeof predicate === 'function' + ? predicate + : (v) => v === predicate; }); - return filteredTxList; - } - /** - * @param {string} key - the key to check - * @param {any} value - the value your looking for can also be a function that returns a bool - * @param {Array} [txList=this.getTxList()] - the list to search. default is the txList - * from txStateManager#getTxList - * @returns {Array} a list of txMetas who matches the search params - */ - getTxsByMetaData(key, value, txList = this.getTxList()) { - const filter = typeof value === 'function' ? value : (v) => v === value; + // If an initial list is provided we need to change it back into an object + // first, so that it matches the shape of our state. This is done by the + // lodash keyBy method. This is the edge case for this method, typically + // initialList will be undefined. + const transactionsToFilter = initialList + ? keyBy(initialList, 'id') + : this.store.getState().transactions; + + // Combine sortBy and pickBy to transform our state object into an array of + // matching transactions that are sorted by time. + const filteredTransactions = sortBy( + pickBy(transactionsToFilter, (transaction) => { + // default matchesCriteria to the value of transactionMatchesNetwork + // when filterToCurrentNetwork is true. + if ( + filterToCurrentNetwork && + transactionMatchesNetwork(transaction, chainId, network) === false + ) { + return false; + } + // iterate over the predicateMethods keys to check if the transaction + // matches the searchCriteria + for (const [key, predicate] of Object.entries(predicateMethods)) { + // We return false early as soon as we know that one of the specified + // search criteria do not match the transaction. This prevents + // needlessly checking all criteria when we already know the criteria + // are not fully satisfied. We check both txParams and the base + // object as predicate keys can be either. + if (key in transaction.txParams) { + if (predicate(transaction.txParams[key]) === false) { + return false; + } + } else if (predicate(transaction[key]) === false) { + return false; + } + } - return txList.filter((txMeta) => { - if (key in txMeta.txParams) { - return filter(txMeta.txParams[key]); + return true; + }), + 'time', + ); + if (limit !== undefined) { + // We need to have all transactions of a given nonce in order to display + // necessary details in the UI. We use the size of this set to determine + // whether we have reached the limit provided, thus ensuring that all + // transactions of nonces we include will be sent to the UI. + const nonces = new Set(); + const txs = []; + // By default, the transaction list we filter from is sorted by time ASC. + // To ensure that filtered results prefers the newest transactions we + // iterate from right to left, inserting transactions into front of a new + // array. The original order is preserved, but we ensure that newest txs + // are preferred. + for (let i = filteredTransactions.length - 1; i > -1; i--) { + const txMeta = filteredTransactions[i]; + const { nonce } = txMeta.txParams; + if (!nonces.has(nonce)) { + if (nonces.size < limit) { + nonces.add(nonce); + } else { + continue; + } + } + // Push transaction into the beginning of our array to ensure the + // original order is preserved. + txs.unshift(txMeta); } - return filter(txMeta[key]); - }); - } - - // get::set status - - /** - * @param {number} txId - the txMeta Id - * @returns {string} the status of the tx. - */ - getTxStatus(txId) { - const txMeta = this.getTx(txId); - return txMeta.status; + return txs; + } + return filteredTransactions; } /** - * Update the status of the tx to 'rejected'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'rejected'. + * After setting the status, the TransactionMeta is deleted from state. + * + * TODO: Should we show historically rejected transactions somewhere in the + * UI? Seems like it could be valuable for information purposes. Of course + * only after limit issues are reduced. + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusRejected(txId) { - this._setTxStatus(txId, 'rejected'); - this._removeTx(txId); + this._setTransactionStatus(txId, 'rejected'); + this._deleteTransaction(txId); } /** - * Update the status of the tx to 'unapproved'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'unapproved' + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusUnapproved(txId) { - this._setTxStatus(txId, TRANSACTION_STATUSES.UNAPPROVED); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.UNAPPROVED); } /** - * Update the status of the tx to 'approved'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'approved' + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusApproved(txId) { - this._setTxStatus(txId, TRANSACTION_STATUSES.APPROVED); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.APPROVED); } /** - * Update the status of the tx to 'signed'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'signed' + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusSigned(txId) { - this._setTxStatus(txId, TRANSACTION_STATUSES.SIGNED); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.SIGNED); } /** - * Update the status of the tx to 'submitted' and add a time stamp - * for when it was called - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'submitted' + * and sets the 'submittedTime' property with the current Unix epoch time. + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusSubmitted(txId) { - const txMeta = this.getTx(txId); + const txMeta = this.getTransaction(txId); txMeta.submittedTime = new Date().getTime(); - this.updateTx(txMeta, 'txStateManager - add submitted time stamp'); - this._setTxStatus(txId, TRANSACTION_STATUSES.SUBMITTED); + this.updateTransaction(txMeta, 'txStateManager - add submitted time stamp'); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.SUBMITTED); } /** - * Update the status of the tx to 'confirmed'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'confirmed' + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusConfirmed(txId) { - this._setTxStatus(txId, TRANSACTION_STATUSES.CONFIRMED); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.CONFIRMED); } /** - * Update the status of the tx to 'dropped'. - * @param {number} txId - the txMeta Id + * Update status of the TransactionMeta with provided id to 'dropped' + * + * @param {number} txId - the target TransactionMeta's Id */ setTxStatusDropped(txId) { - this._setTxStatus(txId, TRANSACTION_STATUSES.DROPPED); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.DROPPED); } /** - * Updates the status of the tx to 'failed' and put the error on the txMeta - * @param {number} txId - the txMeta Id - * @param {erroObject} err - error object + * Update status of the TransactionMeta with provided id to 'failed' and put + * the error on the TransactionMeta object. + * + * @param {number} txId - the target TransactionMeta's Id + * @param {Error} err - error object */ setTxStatusFailed(txId, err) { const error = err || new Error('Internal metamask failure'); - const txMeta = this.getTx(txId); + const txMeta = this.getTransaction(txId); txMeta.err = { message: error.toString(), rpc: error.value, stack: error.stack, }; - this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error'); - this._setTxStatus(txId, TRANSACTION_STATUSES.FAILED); + this.updateTransaction( + txMeta, + 'transactions:tx-state-manager#fail - add error', + ); + this._setTransactionStatus(txId, TRANSACTION_STATUSES.FAILED); } /** - * Removes transaction from the given address for the current network - * from the txList + * Removes all transactions for the given address on the current network, + * preferring chainId for comparison over networkId. + * * @param {string} address - hex string of the from address on the txParams * to remove */ wipeTransactions(address) { // network only tx - const txs = this.getFullTxList(); + const { transactions } = this.store.getState(); const network = this.getNetwork(); const chainId = this.getCurrentChainId(); - // Filter out the ones from the current account and network - const otherAccountTxs = txs.filter( - (txMeta) => - !( - txMeta.txParams.from === address && - transactionMatchesNetwork(txMeta, chainId, network) - ), - ); - // Update state - this._saveTxList(otherAccountTxs); + this.store.updateState({ + transactions: omitBy( + transactions, + (transaction) => + transaction.txParams.from === address && + transactionMatchesNetwork(transaction, chainId, network), + ), + }); + } + + /** + * Filters out the unapproved transactions from state + */ + clearUnapprovedTxs() { + this.store.updateState({ + transactions: omitBy( + this.store.getState().transactions, + (transaction) => transaction.status === TRANSACTION_STATUSES.UNAPPROVED, + ), + }); } // @@ -477,14 +524,37 @@ export default class TransactionStateManager extends EventEmitter { // /** - * @param {number} txId - the txMeta Id - * @param {TransactionStatuses[keyof TransactionStatuses]} status - the status to set on the txMeta - * @emits tx:status-update - passes txId and status - * @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta - * @emits 'updateBadge' + * Updates a transaction's status in state, and then emits events that are + * subscribed to elsewhere. See below for best guesses on where and how these + * events are received. + * @param {number} txId - the TransactionMeta Id + * @param {TransactionStatusString} status - the status to set on the + * TransactionMeta + * @emits txMeta.id:txMeta.status - every time a transaction's status changes + * we emit the change passing along the id. This does not appear to be used + * outside of this file, which only listens to this to unsubscribe listeners + * of :rejected and :signed statuses when the inverse status changes. Likely + * safe to drop. + * @emits tx:status-update - every time a transaction's status changes we + * emit this event and pass txId and status. This event is subscribed to in + * the TransactionController and re-broadcast by the TransactionController. + * It is used internally within the TransactionController to try and update + * pending transactions on each new block (from blockTracker). It's also + * subscribed to in metamask-controller to display a browser notification on + * confirmed or failed transactions. + * @emits txMeta.id:finished - When a transaction moves to a finished state + * this event is emitted, which is used in the TransactionController to pass + * along details of the transaction to the dapp that suggested them. This + * pattern is replicated across all of the message managers and can likely + * be supplemented or replaced by the ApprovalController. + * @emits updateBadge - When the number of transactions changes in state, + * the badge in the browser extension bar should be updated to reflect the + * number of pending transactions. This particular emit doesn't appear to + * bubble up anywhere that is actually used. TransactionController emits + * this *anytime the state changes*, so this is probably superfluous. */ - _setTxStatus(txId, status) { - const txMeta = this.getTx(txId); + _setTransactionStatus(txId, status) { + const txMeta = this.getTransaction(txId); if (!txMeta) { return; @@ -492,7 +562,10 @@ export default class TransactionStateManager extends EventEmitter { txMeta.status = status; try { - this.updateTx(txMeta, `txStateManager: setting status to ${status}`); + this.updateTransaction( + txMeta, + `txStateManager: setting status to ${status}`, + ); this.emit(`${txMeta.id}:${status}`, txId); this.emit(`tx:status-update`, txId, status); if ( @@ -511,26 +584,32 @@ export default class TransactionStateManager extends EventEmitter { } /** - * Saves the new/updated txList. Intended only for internal use - * @param {Array} transactions - the list of transactions to save + * Adds one or more transactions into state. This is not intended for + * external use. + * + * @private + * @param {TransactionMeta[]} transactions - the list of transactions to save */ - _saveTxList(transactions) { - this.store.updateState({ transactions }); - } - - _removeTx(txId) { - const transactionList = this.getFullTxList(); - this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId)); + _addTransactionsToState(transactions) { + this.store.updateState({ + transactions: transactions.reduce((result, newTx) => { + result[newTx.id] = newTx; + return result; + }, this.store.getState().transactions), + }); } /** - * Filters out the unapproved transactions + * removes one transaction from state. This is not intended for external use. + * + * @private + * @param {number} targetTransactionId - the transaction to delete */ - clearUnapprovedTxs() { - const transactions = this.getFullTxList(); - const nonUnapprovedTxs = transactions.filter( - (tx) => tx.status !== TRANSACTION_STATUSES.UNAPPROVED, - ); - this._saveTxList(nonUnapprovedTxs); + _deleteTransaction(targetTransactionId) { + const { transactions } = this.store.getState(); + delete transactions[targetTransactionId]; + this.store.updateState({ + transactions, + }); } } diff --git a/test/unit/app/controllers/transactions/tx-state-manager.test.js b/app/scripts/controllers/transactions/tx-state-manager.test.js similarity index 62% rename from test/unit/app/controllers/transactions/tx-state-manager.test.js rename to app/scripts/controllers/transactions/tx-state-manager.test.js index db9a4bdbc..79c764643 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager.test.js +++ b/app/scripts/controllers/transactions/tx-state-manager.test.js @@ -1,15 +1,15 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import TxStateManager from '../../../../../app/scripts/controllers/transactions/tx-state-manager'; -import { snapshotFromTxMeta } from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helpers'; -import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; +import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; import { KOVAN_CHAIN_ID, KOVAN_NETWORK_ID, -} from '../../../../../shared/constants/network'; - -const noop = () => true; +} from '../../../../shared/constants/network'; +import TxStateManager from './tx-state-manager'; +import { snapshotFromTxMeta } from './lib/tx-state-history-helpers'; +const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; +const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; describe('TransactionStateManager', function () { let txStateManager; const currentNetworkId = KOVAN_NETWORK_ID; @@ -19,7 +19,7 @@ describe('TransactionStateManager', function () { beforeEach(function () { txStateManager = new TxStateManager({ initState: { - transactions: [], + transactions: {}, }, txHistoryLimit: 10, getNetwork: () => currentNetworkId, @@ -33,11 +33,14 @@ describe('TransactionStateManager', function () { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); + txStateManager.addTransaction(tx); txStateManager.setTxStatusSigned(1); - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.ok(Array.isArray(result)); assert.equal(result.length, 1); assert.equal(result[0].status, TRANSACTION_STATUSES.SIGNED); @@ -48,12 +51,15 @@ describe('TransactionStateManager', function () { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; const clock = sinon.useFakeTimers(); const onSigned = sinon.spy(); - txStateManager.addTx(tx); + txStateManager.addTransaction(tx); txStateManager.on('1:signed', onSigned); txStateManager.setTxStatusSigned(1); clock.runAll(); @@ -69,11 +75,14 @@ describe('TransactionStateManager', function () { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx); + txStateManager.addTransaction(tx); txStateManager.setTxStatusRejected(1); - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.ok(Array.isArray(result)); assert.equal(result.length, 0); }); @@ -83,12 +92,15 @@ describe('TransactionStateManager', function () { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; const clock = sinon.useFakeTimers(); const onSigned = sinon.spy(); - txStateManager.addTx(tx); + txStateManager.addTransaction(tx); txStateManager.on('1:rejected', onSigned); txStateManager.setTxStatusRejected(1); clock.runAll(); @@ -98,17 +110,9 @@ describe('TransactionStateManager', function () { }); }); - describe('#getFullTxList', function () { + describe('#getTransactions', function () { it('when new should return empty array', function () { - const result = txStateManager.getTxList(); - assert.ok(Array.isArray(result)); - assert.equal(result.length, 0); - }); - }); - - describe('#getTxList', function () { - it('when new should return empty array', function () { - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.ok(Array.isArray(result)); assert.equal(result.length, 0); }); @@ -140,13 +144,16 @@ describe('TransactionStateManager', function () { const txm = new TxStateManager({ initState: { - transactions: [submittedTx, confirmedTx], + transactions: { + [submittedTx.id]: submittedTx, + [confirmedTx.id]: confirmedTx, + }, }, getNetwork: () => currentNetworkId, getCurrentChainId: () => currentChainId, }); - assert.deepEqual(txm.getTxList(), [submittedTx, confirmedTx]); + assert.deepEqual(txm.getTransactions(), [submittedTx, confirmedTx]); }); it('should return a list of transactions, limited by N unique nonces when there are NO duplicates', function () { @@ -200,48 +207,49 @@ describe('TransactionStateManager', function () { const txm = new TxStateManager({ initState: { - transactions: [ - submittedTx0, - unapprovedTx1, - approvedTx2, - confirmedTx3, - ], + transactions: { + [submittedTx0.id]: submittedTx0, + [unapprovedTx1.id]: unapprovedTx1, + [approvedTx2.id]: approvedTx2, + [confirmedTx3.id]: confirmedTx3, + }, }, getNetwork: () => currentNetworkId, getCurrentChainId: () => currentChainId, }); - assert.deepEqual(txm.getTxList(2), [approvedTx2, confirmedTx3]); + assert.deepEqual(txm.getTransactions({ limit: 2 }), [ + approvedTx2, + confirmedTx3, + ]); }); it('should return a list of transactions, limited by N unique nonces when there ARE duplicates', function () { - const submittedTx0s = [ - { - id: 0, - metamaskNetworkId: currentNetworkId, - time: 0, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x0', - }, - status: TRANSACTION_STATUSES.SUBMITTED, + const submittedTx0 = { + id: 0, + metamaskNetworkId: currentNetworkId, + time: 0, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x0', }, - { - id: 0, - metamaskNetworkId: currentNetworkId, - time: 0, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x0', - }, - status: TRANSACTION_STATUSES.SUBMITTED, + status: TRANSACTION_STATUSES.SUBMITTED, + }; + const submittedTx0Dupe = { + id: 1, + metamaskNetworkId: currentNetworkId, + time: 0, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x0', }, - ]; + status: TRANSACTION_STATUSES.SUBMITTED, + }; const unapprovedTx1 = { - id: 1, + id: 2, metamaskNetworkId: currentNetworkId, chainId: currentChainId, time: 1, @@ -253,85 +261,215 @@ describe('TransactionStateManager', function () { status: TRANSACTION_STATUSES.UNAPPROVED, }; - const approvedTx2s = [ + const approvedTx2 = { + id: 3, + metamaskNetworkId: currentNetworkId, + time: 2, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x2', + }, + status: TRANSACTION_STATUSES.APPROVED, + }; + const approvedTx2Dupe = { + id: 4, + metamaskNetworkId: currentNetworkId, + chainId: currentChainId, + time: 2, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x2', + }, + status: TRANSACTION_STATUSES.APPROVED, + }; + + const failedTx3 = { + id: 5, + metamaskNetworkId: currentNetworkId, + time: 3, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x3', + }, + status: TRANSACTION_STATUSES.FAILED, + }; + const failedTx3Dupe = { + id: 6, + metamaskNetworkId: currentNetworkId, + chainId: currentChainId, + time: 3, + txParams: { + from: '0xAddress', + to: '0xRecipient', + nonce: '0x3', + }, + status: TRANSACTION_STATUSES.FAILED, + }; + + const txm = new TxStateManager({ + initState: { + transactions: { + [submittedTx0.id]: submittedTx0, + [submittedTx0Dupe.id]: submittedTx0Dupe, + + [unapprovedTx1.id]: unapprovedTx1, + [approvedTx2.id]: approvedTx2, + [approvedTx2Dupe.id]: approvedTx2Dupe, + + [failedTx3.id]: failedTx3, + [failedTx3Dupe.id]: failedTx3Dupe, + }, + }, + getNetwork: () => currentNetworkId, + getCurrentChainId: () => currentChainId, + }); + + assert.deepEqual(txm.getTransactions({ limit: 2 }), [ + approvedTx2, + approvedTx2Dupe, + failedTx3, + failedTx3Dupe, + ]); + }); + + it('returns a tx with the requested data', function () { + const txMetas = [ { - id: 2, + id: 0, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, + metamaskNetworkId: currentNetworkId, + }, + { + id: 1, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: currentNetworkId, - time: 2, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x2', - }, - status: TRANSACTION_STATUSES.APPROVED, }, { id: 2, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: currentNetworkId, - chainId: currentChainId, - time: 2, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x2', - }, - status: TRANSACTION_STATUSES.APPROVED, }, - ]; - - const failedTx3s = [ { id: 3, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { from: VALID_ADDRESS_TWO, to: VALID_ADDRESS }, metamaskNetworkId: currentNetworkId, - time: 3, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x3', - }, - status: TRANSACTION_STATUSES.FAILED, }, { - id: 3, + id: 4, + status: TRANSACTION_STATUSES.UNAPPROVED, + txParams: { from: VALID_ADDRESS_TWO, to: VALID_ADDRESS }, metamaskNetworkId: currentNetworkId, - chainId: currentChainId, - time: 3, - txParams: { - from: '0xAddress', - to: '0xRecipient', - nonce: '0x3', - }, - status: TRANSACTION_STATUSES.FAILED, }, - ]; - - const txm = new TxStateManager({ - initState: { - transactions: [ - ...submittedTx0s, - unapprovedTx1, - ...approvedTx2s, - ...failedTx3s, - ], + { + id: 5, + status: TRANSACTION_STATUSES.CONFIRMED, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, + metamaskNetworkId: currentNetworkId, }, - getNetwork: () => currentNetworkId, - getCurrentChainId: () => currentChainId, - }); + { + id: 6, + status: TRANSACTION_STATUSES.CONFIRMED, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, + metamaskNetworkId: currentNetworkId, + }, + { + id: 7, + status: TRANSACTION_STATUSES.CONFIRMED, + txParams: { from: VALID_ADDRESS_TWO, to: VALID_ADDRESS }, + metamaskNetworkId: currentNetworkId, + }, + { + id: 8, + status: TRANSACTION_STATUSES.CONFIRMED, + txParams: { from: VALID_ADDRESS_TWO, to: VALID_ADDRESS }, + metamaskNetworkId: currentNetworkId, + }, + { + id: 9, + status: TRANSACTION_STATUSES.CONFIRMED, + txParams: { from: VALID_ADDRESS_TWO, to: VALID_ADDRESS }, + metamaskNetworkId: currentNetworkId, + }, + ]; + txMetas.forEach((txMeta) => txStateManager.addTransaction(txMeta)); + let searchCriteria; - assert.deepEqual(txm.getTxList(2), [...approvedTx2s, ...failedTx3s]); + searchCriteria = { + status: TRANSACTION_STATUSES.UNAPPROVED, + from: VALID_ADDRESS, + }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 3, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { + status: TRANSACTION_STATUSES.UNAPPROVED, + to: VALID_ADDRESS, + }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 2, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { + status: TRANSACTION_STATUSES.CONFIRMED, + from: VALID_ADDRESS_TWO, + }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 3, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { status: TRANSACTION_STATUSES.CONFIRMED }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 5, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { from: VALID_ADDRESS }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 5, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { to: VALID_ADDRESS }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 5, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); + searchCriteria = { + status: (status) => status !== TRANSACTION_STATUSES.CONFIRMED, + }; + assert.equal( + txStateManager.getTransactions({ searchCriteria }).length, + 5, + `getTransactions - ${JSON.stringify(searchCriteria)}`, + ); }); }); - describe('#addTx', function () { - it('adds a tx returned in getTxList', function () { + describe('#addTransaction', function () { + it('adds a tx returned in getTransactions', function () { const tx = { id: 1, status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); - const result = txStateManager.getTxList(); + txStateManager.addTransaction(tx); + const result = txStateManager.getTransactions(); assert.ok(Array.isArray(result)); assert.equal(result.length, 1); assert.equal(result[0].id, 1); @@ -361,10 +499,10 @@ describe('TransactionStateManager', function () { }, }; assert.throws( - txStateManager.addTx.bind(txStateManager, tx), - 'addTx should throw error', + txStateManager.addTransaction.bind(txStateManager, tx), + 'addTransaction should throw error', ); - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.ok(Array.isArray(result), 'txList should be an array'); assert.equal(result.length, 0, 'txList should be empty'); } @@ -376,18 +514,26 @@ describe('TransactionStateManager', function () { id: 1, status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; const tx2 = { id: 2, status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: otherNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); - txStateManager.addTx(tx2, noop); - const result = txStateManager.getFullTxList(); - const result2 = txStateManager.getTxList(); + txStateManager.addTransaction(tx); + txStateManager.addTransaction(tx2); + const result = txStateManager.getTransactions({ + filterToCurrentNetwork: false, + }); + const result2 = txStateManager.getTransactions(); assert.equal(result.length, 2, 'txs were deleted'); assert.equal(result2.length, 1, 'incorrect number of txs on network.'); }); @@ -400,11 +546,14 @@ describe('TransactionStateManager', function () { time: new Date(), status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); + txStateManager.addTransaction(tx); } - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.equal(result.length, limit, `limit of ${limit} txs enforced`); assert.equal(result[0].id, 1, 'early txs truncated'); }); @@ -417,11 +566,14 @@ describe('TransactionStateManager', function () { time: new Date(), status: TRANSACTION_STATUSES.REJECTED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); + txStateManager.addTransaction(tx); } - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.equal(result.length, limit, `limit of ${limit} txs enforced`); assert.equal(result[0].id, 1, 'early txs truncated'); }); @@ -432,9 +584,12 @@ describe('TransactionStateManager', function () { time: new Date(), status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(unconfirmedTx, noop); + txStateManager.addTransaction(unconfirmedTx); const limit = txStateManager.txHistoryLimit; for (let i = 1; i < limit + 1; i++) { const tx = { @@ -442,11 +597,14 @@ describe('TransactionStateManager', function () { time: new Date(), status: TRANSACTION_STATUSES.CONFIRMED, metamaskNetworkId: currentNetworkId, - txParams: {}, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, + }, }; - txStateManager.addTx(tx, noop); + txStateManager.addTransaction(tx); } - const result = txStateManager.getTxList(); + const result = txStateManager.getTransactions(); assert.equal(result.length, limit, `limit of ${limit} txs enforced`); assert.equal(result[0].id, 0, 'first tx should still be there'); assert.equal( @@ -458,30 +616,30 @@ describe('TransactionStateManager', function () { }); }); - describe('#updateTx', function () { + describe('#updateTransaction', function () { it('replaces the tx with the same id', function () { - txStateManager.addTx( - { - id: '1', - status: TRANSACTION_STATUSES.UNAPPROVED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + txStateManager.addTransaction({ + id: '1', + status: TRANSACTION_STATUSES.UNAPPROVED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); - txStateManager.addTx( - { - id: '2', - status: TRANSACTION_STATUSES.CONFIRMED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + }); + txStateManager.addTransaction({ + id: '2', + status: TRANSACTION_STATUSES.CONFIRMED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); - const txMeta = txStateManager.getTx('1'); + }); + const txMeta = txStateManager.getTransaction('1'); txMeta.hash = 'foo'; - txStateManager.updateTx(txMeta); - const result = txStateManager.getTx('1'); + txStateManager.updateTransaction(txMeta); + const result = txStateManager.getTransaction('1'); assert.equal(result.hash, 'foo'); }); @@ -497,7 +655,7 @@ describe('TransactionStateManager', function () { }; const invalidValues = [1, true, {}, Symbol('1')]; - txStateManager.addTx({ + txStateManager.addTransaction({ id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, @@ -506,7 +664,7 @@ describe('TransactionStateManager', function () { Object.keys(validTxParams).forEach((key) => { for (const value of invalidValues) { - const originalTx = txStateManager.getTx(1); + const originalTx = txStateManager.getTransaction(1); const newTx = { ...originalTx, txParams: { @@ -515,10 +673,10 @@ describe('TransactionStateManager', function () { }, }; assert.throws( - txStateManager.updateTx.bind(txStateManager, newTx), - 'updateTx should throw an error', + txStateManager.updateTransaction.bind(txStateManager, newTx), + 'updateTransaction should throw an error', ); - const result = txStateManager.getTx(1); + const result = txStateManager.getTransaction(1); assert.deepEqual(result, originalTx, 'tx should not be updated'); } }); @@ -533,12 +691,14 @@ describe('TransactionStateManager', function () { status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, txParams: { + from: VALID_ADDRESS_TWO, + to: VALID_ADDRESS, gasPrice: originalGasPrice, }, }; - txStateManager.addTx(txMeta); - const updatedTx = txStateManager.getTx('1'); + txStateManager.addTransaction(txMeta); + const updatedTx = txStateManager.getTransaction('1'); // verify tx was initialized correctly assert.equal(updatedTx.history.length, 1, 'one history item (initial)'); assert.equal( @@ -551,13 +711,13 @@ describe('TransactionStateManager', function () { snapshotFromTxMeta(updatedTx), 'first history item is initial state', ); - // modify value and updateTx + // modify value and updateTransaction updatedTx.txParams.gasPrice = desiredGasPrice; const before = new Date().getTime(); - txStateManager.updateTx(updatedTx); + txStateManager.updateTransaction(updatedTx); const after = new Date().getTime(); // check updated value - const result = txStateManager.getTx('1'); + const result = txStateManager.getTransaction('1'); assert.equal( result.txParams.gasPrice, desiredGasPrice, @@ -607,38 +767,40 @@ describe('TransactionStateManager', function () { status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, txParams: { + from: VALID_ADDRESS_TWO, + to: VALID_ADDRESS, gasPrice: '0x01', }, }; - txStateManager.addTx(txMeta); - txStateManager.updateTx(txMeta); + txStateManager.addTransaction(txMeta); + txStateManager.updateTransaction(txMeta); - const { history } = txStateManager.getTx('1'); + const { history } = txStateManager.getTransaction('1'); assert.equal(history.length, 1, 'two history items (initial + diff)'); }); }); describe('#getUnapprovedTxList', function () { it('returns unapproved txs in a hash', function () { - txStateManager.addTx( - { - id: '1', - status: TRANSACTION_STATUSES.UNAPPROVED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + txStateManager.addTransaction({ + id: '1', + status: TRANSACTION_STATUSES.UNAPPROVED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); - txStateManager.addTx( - { - id: '2', - status: TRANSACTION_STATUSES.CONFIRMED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + }); + txStateManager.addTransaction({ + id: '2', + status: TRANSACTION_STATUSES.CONFIRMED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); + }); const result = txStateManager.getUnapprovedTxList(); assert.equal(typeof result, 'object'); assert.equal(result['1'].status, TRANSACTION_STATUSES.UNAPPROVED); @@ -646,154 +808,40 @@ describe('TransactionStateManager', function () { }); }); - describe('#getTx', function () { + describe('#getTransaction', function () { it('returns a tx with the requested id', function () { - txStateManager.addTx( - { - id: '1', - status: TRANSACTION_STATUSES.UNAPPROVED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + txStateManager.addTransaction({ + id: '1', + status: TRANSACTION_STATUSES.UNAPPROVED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); - txStateManager.addTx( - { - id: '2', - status: TRANSACTION_STATUSES.CONFIRMED, - metamaskNetworkId: currentNetworkId, - txParams: {}, + }); + txStateManager.addTransaction({ + id: '2', + status: TRANSACTION_STATUSES.CONFIRMED, + metamaskNetworkId: currentNetworkId, + txParams: { + to: VALID_ADDRESS, + from: VALID_ADDRESS, }, - noop, - ); + }); assert.equal( - txStateManager.getTx('1').status, + txStateManager.getTransaction('1').status, TRANSACTION_STATUSES.UNAPPROVED, ); assert.equal( - txStateManager.getTx('2').status, + txStateManager.getTransaction('2').status, TRANSACTION_STATUSES.CONFIRMED, ); }); }); - describe('#getFilteredTxList', function () { - it('returns a tx with the requested data', function () { - const txMetas = [ - { - id: 0, - status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xaa', to: '0xbb' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 1, - status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xaa', to: '0xbb' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 2, - status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xaa', to: '0xbb' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 3, - status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xbb', to: '0xaa' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 4, - status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xbb', to: '0xaa' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 5, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xaa', to: '0xbb' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 6, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xaa', to: '0xbb' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 7, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xbb', to: '0xaa' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 8, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xbb', to: '0xaa' }, - metamaskNetworkId: currentNetworkId, - }, - { - id: 9, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xbb', to: '0xaa' }, - metamaskNetworkId: currentNetworkId, - }, - ]; - txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)); - let filterParams; - - filterParams = { status: TRANSACTION_STATUSES.UNAPPROVED, from: '0xaa' }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 3, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { status: TRANSACTION_STATUSES.UNAPPROVED, to: '0xaa' }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 2, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { status: TRANSACTION_STATUSES.CONFIRMED, from: '0xbb' }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 3, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { status: TRANSACTION_STATUSES.CONFIRMED }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 5, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { from: '0xaa' }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 5, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { to: '0xaa' }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 5, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - filterParams = { - status: (status) => status !== TRANSACTION_STATUSES.CONFIRMED, - }; - assert.equal( - txStateManager.getFilteredTxList(filterParams).length, - 5, - `getFilteredTxList - ${JSON.stringify(filterParams)}`, - ); - }); - }); - describe('#wipeTransactions', function () { - const specificAddress = '0xaa'; - const otherAddress = '0xbb'; + const specificAddress = VALID_ADDRESS; + const otherAddress = VALID_ADDRESS_TWO; it('should remove only the transactions from a specific address', function () { const txMetas = [ @@ -816,15 +864,15 @@ describe('TransactionStateManager', function () { metamaskNetworkId: currentNetworkId, }, ]; - txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)); + txMetas.forEach((txMeta) => txStateManager.addTransaction(txMeta)); txStateManager.wipeTransactions(specificAddress); const transactionsFromCurrentAddress = txStateManager - .getTxList() + .getTransactions() .filter((txMeta) => txMeta.txParams.from === specificAddress); const transactionsFromOtherAddresses = txStateManager - .getTxList() + .getTransactions() .filter((txMeta) => txMeta.txParams.from !== specificAddress); assert.equal(transactionsFromCurrentAddress.length, 0); @@ -853,15 +901,15 @@ describe('TransactionStateManager', function () { }, ]; - txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)); + txMetas.forEach((txMeta) => txStateManager.addTransaction(txMeta)); txStateManager.wipeTransactions(specificAddress); const txsFromCurrentNetworkAndAddress = txStateManager - .getTxList() + .getTransactions() .filter((txMeta) => txMeta.txParams.from === specificAddress); const txFromOtherNetworks = txStateManager - .getFullTxList() + .getTransactions({ filterToCurrentNetwork: false }) .filter((txMeta) => txMeta.metamaskNetworkId === otherNetworkId); assert.equal(txsFromCurrentNetworkAndAddress.length, 0); @@ -869,21 +917,26 @@ describe('TransactionStateManager', function () { }); }); - describe('#_removeTx', function () { + describe('#_deleteTransaction', function () { it('should remove the transaction from the storage', function () { - txStateManager._saveTxList([{ id: 1 }]); - txStateManager._removeTx(1); + txStateManager.addTransaction({ id: 1 }); + txStateManager._deleteTransaction(1); assert.ok( - !txStateManager.getFullTxList().length, + !txStateManager.getTransactions({ filterToCurrentNetwork: false }) + .length, 'txList should be empty', ); }); it('should only remove the transaction with ID 1 from the storage', function () { - txStateManager._saveTxList([{ id: 1 }, { id: 2 }]); - txStateManager._removeTx(1); + txStateManager.store.updateState({ + transactions: { 1: { id: 1 }, 2: { id: 2 } }, + }); + txStateManager._deleteTransaction(1); assert.equal( - txStateManager.getFullTxList()[0].id, + txStateManager.getTransactions({ + filterToCurrentNetwork: false, + })[0].id, 2, 'txList should have a id of 2', ); @@ -896,35 +949,35 @@ describe('TransactionStateManager', function () { { id: 0, status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xaa', to: '0xbb' }, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: currentNetworkId, }, { id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, - txParams: { from: '0xaa', to: '0xbb' }, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: currentNetworkId, }, { id: 2, status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xaa', to: '0xbb' }, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: otherNetworkId, }, { id: 3, status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { from: '0xaa', to: '0xbb' }, + txParams: { from: VALID_ADDRESS, to: VALID_ADDRESS_TWO }, metamaskNetworkId: otherNetworkId, }, ]; - txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)); + txMetas.forEach((txMeta) => txStateManager.addTransaction(txMeta)); txStateManager.clearUnapprovedTxs(); const unapprovedTxList = txStateManager - .getFullTxList() + .getTransactions({ filterToCurrentNetwork: false }) .filter((tx) => tx.status === TRANSACTION_STATUSES.UNAPPROVED); assert.equal(unapprovedTxList.length, 0); diff --git a/test/unit/app/ComposableObservableStore.test.js b/app/scripts/lib/ComposableObservableStore.test.js similarity index 93% rename from test/unit/app/ComposableObservableStore.test.js rename to app/scripts/lib/ComposableObservableStore.test.js index 816e0de59..a079984c1 100644 --- a/test/unit/app/ComposableObservableStore.test.js +++ b/app/scripts/lib/ComposableObservableStore.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; import { ObservableStore } from '@metamask/obs-store'; -import ComposableObservableStore from '../../../app/scripts/lib/ComposableObservableStore'; +import ComposableObservableStore from './ComposableObservableStore'; describe('ComposableObservableStore', function () { it('should register initial state', function () { diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index bf1b131f0..2cb1b0e81 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -1,18 +1,26 @@ +import { + GOERLI_CHAIN_ID, + KOVAN_CHAIN_ID, + MAINNET_CHAIN_ID, + RINKEBY_CHAIN_ID, + ROPSTEN_CHAIN_ID, +} from '../../../shared/constants/network'; + /** * Gives the caller a url at which the user can acquire eth, depending on the network they are in * * @param {Object} opts - Options required to determine the correct url - * @param {string} opts.network - The network for which to return a url - * @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if network === '1'. - * @returns {string|undefined} The url at which the user can access ETH, while in the given network. If the passed - * network does not match any of the specified cases, or if no network is given, returns undefined. + * @param {string} opts.chainId - The chainId for which to return a url + * @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if chainId === '0x1'. + * @returns {string|undefined} The url at which the user can access ETH, while in the given chain. If the passed + * chainId does not match any of the specified cases, or if no chainId is given, returns undefined. * */ -export default function getBuyEthUrl({ network, address, service }) { +export default function getBuyEthUrl({ chainId, address, service }) { // default service by network if not specified if (!service) { // eslint-disable-next-line no-param-reassign - service = getDefaultServiceForNetwork(network); + service = getDefaultServiceForChain(chainId); } switch (service) { @@ -33,21 +41,21 @@ export default function getBuyEthUrl({ network, address, service }) { } } -function getDefaultServiceForNetwork(network) { - switch (network) { - case '1': +function getDefaultServiceForChain(chainId) { + switch (chainId) { + case MAINNET_CHAIN_ID: return 'wyre'; - case '3': + case ROPSTEN_CHAIN_ID: return 'metamask-faucet'; - case '4': + case RINKEBY_CHAIN_ID: return 'rinkeby-faucet'; - case '42': + case KOVAN_CHAIN_ID: return 'kovan-faucet'; - case '5': + case GOERLI_CHAIN_ID: return 'goerli-faucet'; default: throw new Error( - `No default cryptocurrency exchange or faucet for networkId: "${network}"`, + `No default cryptocurrency exchange or faucet for chainId: "${chainId}"`, ); } } diff --git a/test/unit/app/buy-eth-url.test.js b/app/scripts/lib/buy-eth-url.test.js similarity index 78% rename from test/unit/app/buy-eth-url.test.js rename to app/scripts/lib/buy-eth-url.test.js index d9730b4fb..17ba3d64a 100644 --- a/test/unit/app/buy-eth-url.test.js +++ b/app/scripts/lib/buy-eth-url.test.js @@ -1,20 +1,26 @@ import assert from 'assert'; -import getBuyEthUrl from '../../../app/scripts/lib/buy-eth-url'; +import { + KOVAN_CHAIN_ID, + MAINNET_CHAIN_ID, + RINKEBY_CHAIN_ID, + ROPSTEN_CHAIN_ID, +} from '../../../shared/constants/network'; +import getBuyEthUrl from './buy-eth-url'; describe('buy-eth-url', function () { const mainnet = { - network: '1', + chainId: MAINNET_CHAIN_ID, amount: 5, address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }; const ropsten = { - network: '3', + chainId: ROPSTEN_CHAIN_ID, }; const rinkeby = { - network: '4', + chainId: RINKEBY_CHAIN_ID, }; const kovan = { - network: '42', + chainId: KOVAN_CHAIN_ID, }; it('returns wyre url with address for network 1', function () { diff --git a/test/unit/app/cleanErrorStack.test.js b/app/scripts/lib/cleanErrorStack.test.js similarity index 92% rename from test/unit/app/cleanErrorStack.test.js rename to app/scripts/lib/cleanErrorStack.test.js index e6bf8951f..9f01e8252 100644 --- a/test/unit/app/cleanErrorStack.test.js +++ b/app/scripts/lib/cleanErrorStack.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import cleanErrorStack from '../../../app/scripts/lib/cleanErrorStack'; +import cleanErrorStack from './cleanErrorStack'; describe('Clean Error Stack', function () { const testMessage = 'Test Message'; diff --git a/app/scripts/lib/createMetaRPCHandler.js b/app/scripts/lib/createMetaRPCHandler.js new file mode 100644 index 000000000..24ac0ff6a --- /dev/null +++ b/app/scripts/lib/createMetaRPCHandler.js @@ -0,0 +1,33 @@ +import { ethErrors, serializeError } from 'eth-rpc-errors'; + +const createMetaRPCHandler = (api, outStream) => { + return (data) => { + if (!api[data.method]) { + outStream.write({ + jsonrpc: '2.0', + error: ethErrors.rpc.methodNotFound({ + message: `${data.method} not found`, + }), + id: data.id, + }); + return; + } + api[data.method](...data.params, (err, result) => { + if (err) { + outStream.write({ + jsonrpc: '2.0', + error: serializeError(err, { shouldIncludeStack: true }), + id: data.id, + }); + } else { + outStream.write({ + jsonrpc: '2.0', + result, + id: data.id, + }); + } + }); + }; +}; + +export default createMetaRPCHandler; diff --git a/app/scripts/lib/createMetaRPCHandler.test.js b/app/scripts/lib/createMetaRPCHandler.test.js new file mode 100644 index 000000000..e37985105 --- /dev/null +++ b/app/scripts/lib/createMetaRPCHandler.test.js @@ -0,0 +1,61 @@ +import assert from 'assert'; +import { obj as createThoughStream } from 'through2'; +import createMetaRPCHandler from './createMetaRPCHandler'; + +describe('createMetaRPCHandler', function () { + it('can call the api when handler receives a JSON-RPC request', function (done) { + const api = { + foo: (param1) => { + assert.strictEqual(param1, 'bar'); + done(); + }, + }; + const streamTest = createThoughStream(); + const handler = createMetaRPCHandler(api, streamTest); + handler({ + id: 1, + method: 'foo', + params: ['bar'], + }); + }); + it('can write the response to the outstream when api callback is called', function (done) { + const api = { + foo: (param1, cb) => { + assert.strictEqual(param1, 'bar'); + cb(null, 'foobarbaz'); + }, + }; + const streamTest = createThoughStream(); + const handler = createMetaRPCHandler(api, streamTest); + handler({ + id: 1, + method: 'foo', + params: ['bar'], + }); + streamTest.on('data', (data) => { + assert.strictEqual(data.result, 'foobarbaz'); + streamTest.end(); + done(); + }); + }); + it('can write the error to the outstream when api callback is called with an error', function (done) { + const api = { + foo: (param1, cb) => { + assert.strictEqual(param1, 'bar'); + cb(new Error('foo-error')); + }, + }; + const streamTest = createThoughStream(); + const handler = createMetaRPCHandler(api, streamTest); + handler({ + id: 1, + method: 'foo', + params: ['bar'], + }); + streamTest.on('data', (data) => { + assert.strictEqual(data.error.message, 'foo-error'); + streamTest.end(); + done(); + }); + }); +}); diff --git a/app/scripts/lib/decrypt-message-manager.js b/app/scripts/lib/decrypt-message-manager.js index 32ae13626..674c73572 100644 --- a/app/scripts/lib/decrypt-message-manager.js +++ b/app/scripts/lib/decrypt-message-manager.js @@ -5,8 +5,8 @@ import { ethErrors } from 'eth-rpc-errors'; import log from 'loglevel'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; +import createId from '../../../shared/modules/random-id'; import { addHexPrefix } from './util'; -import createId from './random-id'; const hexRe = /^[0-9A-Fa-f]+$/gu; diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js index e29723b5b..2a4b2296e 100644 --- a/app/scripts/lib/encryption-public-key-manager.js +++ b/app/scripts/lib/encryption-public-key-manager.js @@ -4,7 +4,7 @@ import { ethErrors } from 'eth-rpc-errors'; import log from 'loglevel'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from './random-id'; +import createId from '../../../shared/modules/random-id'; /** * Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 7f4218f8b..ce48327f3 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -4,7 +4,7 @@ import ethUtil from 'ethereumjs-util'; import { ethErrors } from 'eth-rpc-errors'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from './random-id'; +import createId from '../../../shared/modules/random-id'; /** * Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for diff --git a/test/unit/app/message-manager.test.js b/app/scripts/lib/message-manager.test.js similarity index 98% rename from test/unit/app/message-manager.test.js rename to app/scripts/lib/message-manager.test.js index 2c0589e63..947cb2688 100644 --- a/test/unit/app/message-manager.test.js +++ b/app/scripts/lib/message-manager.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import MessageManager from '../../../app/scripts/lib/message-manager'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import MessageManager from './message-manager'; describe('Message Manager', function () { let messageManager; diff --git a/app/scripts/lib/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js new file mode 100644 index 000000000..108da0e4c --- /dev/null +++ b/app/scripts/lib/metaRPCClientFactory.js @@ -0,0 +1,81 @@ +import { EthereumRpcError } from 'eth-rpc-errors'; +import SafeEventEmitter from 'safe-event-emitter'; +import createRandomId from '../../../shared/modules/random-id'; + +class MetaRPCClient { + constructor(connectionStream) { + this.connectionStream = connectionStream; + this.notificationChannel = new SafeEventEmitter(); + this.requests = new Map(); + this.connectionStream.on('data', this.handleResponse.bind(this)); + this.connectionStream.on('end', this.close.bind(this)); + } + + onNotification(handler) { + this.notificationChannel.addListener('notification', (data) => { + handler(data); + }); + } + + close() { + this.notificationChannel.removeAllListeners(); + } + + handleResponse(data) { + const { id, result, error, method, params } = data; + const cb = this.requests.get(id); + + if (method && params && id) { + // dont handle server-side to client-side requests + return; + } + if (method && params && !id) { + // handle servier-side to client-side notification + this.notificationChannel.emit('notification', data); + return; + } + if (!cb) { + // not found in request list + return; + } + + if (error) { + const e = new EthereumRpcError(error.code, error.message, error.data); + // preserve the stack from serializeError + e.stack = error.stack; + this.requests.delete(id); + cb(e); + return; + } + + this.requests.delete(id); + + cb(null, result); + } +} + +const metaRPCClientFactory = (connectionStream) => { + const metaRPCClient = new MetaRPCClient(connectionStream); + return new Proxy(metaRPCClient, { + get: (object, property) => { + if (object[property]) { + return object[property]; + } + return (...p) => { + const cb = p[p.length - 1]; + const params = p.slice(0, -1); + const id = createRandomId(); + + object.requests.set(id, cb); + object.connectionStream.write({ + jsonrpc: '2.0', + method: property, + params, + id, + }); + }; + }, + }); +}; + +export default metaRPCClientFactory; diff --git a/app/scripts/lib/metaRPCClientFactory.test.js b/app/scripts/lib/metaRPCClientFactory.test.js new file mode 100644 index 000000000..d270a4e1a --- /dev/null +++ b/app/scripts/lib/metaRPCClientFactory.test.js @@ -0,0 +1,88 @@ +import assert from 'assert'; +import { obj as createThoughStream } from 'through2'; +import metaRPCClientFactory from './metaRPCClientFactory'; + +describe('metaRPCClientFactory', function () { + it('should be able to make an rpc request with the method', function (done) { + const streamTest = createThoughStream((chunk) => { + assert.strictEqual(chunk.method, 'foo'); + done(); + }); + const metaRPCClient = metaRPCClientFactory(streamTest); + metaRPCClient.foo(); + }); + it('should be able to make an rpc request/response with the method and params and node-style callback', function (done) { + const streamTest = createThoughStream(); + const metaRPCClient = metaRPCClientFactory(streamTest); + + // make a "foo" method call + metaRPCClient.foo('bar', (_, result) => { + assert.strictEqual(result, 'foobarbaz'); + done(); + }); + + // fake a response + metaRPCClient.requests.forEach((_, key) => { + streamTest.write({ + jsonrpc: '2.0', + id: key, + result: 'foobarbaz', + }); + }); + }); + it('should be able to make an rpc request/error with the method and params and node-style callback', function (done) { + const streamTest = createThoughStream(); + const metaRPCClient = metaRPCClientFactory(streamTest); + + // make a "foo" method call + metaRPCClient.foo('bar', (err) => { + assert.strictEqual(err.message, 'foo-message'); + assert.strictEqual(err.code, 1); + done(); + }); + + metaRPCClient.requests.forEach((_, key) => { + streamTest.write({ + jsonrpc: '2.0', + id: key, + error: { + code: 1, + message: 'foo-message', + }, + }); + }); + }); + + it('should be able to make an rpc request/response with the method and params and node-style callback with multiple instances of metaRPCClientFactory and the same connectionStream', function (done) { + const streamTest = createThoughStream(); + const metaRPCClient = metaRPCClientFactory(streamTest); + const metaRPCClient2 = metaRPCClientFactory(streamTest); + + // make a "foo" method call, followed by "baz" call on metaRPCClient2 + metaRPCClient.foo('bar', (_, result) => { + assert.strictEqual(result, 'foobarbaz'); + metaRPCClient2.baz('bar', (err) => { + assert.strictEqual(err, null); + done(); + }); + }); + + // fake a response + metaRPCClient.requests.forEach((_, key) => { + streamTest.write({ + jsonrpc: '2.0', + id: key, + result: 'foobarbaz', + }); + }); + + // fake client2's response + metaRPCClient2.requests.forEach((_, key) => { + streamTest.write({ + jsonrpc: '2.0', + id: key, + result: 'foobarbaz', + }); + }); + }); +}); diff --git a/test/unit/migrations/migrator.test.js b/app/scripts/lib/migrator/index.test.js similarity index 92% rename from test/unit/migrations/migrator.test.js rename to app/scripts/lib/migrator/index.test.js index bb0771cd9..0fbb3e8a9 100644 --- a/test/unit/migrations/migrator.test.js +++ b/app/scripts/lib/migrator/index.test.js @@ -1,9 +1,9 @@ import fs from 'fs'; import assert from 'assert'; import { cloneDeep } from 'lodash'; -import Migrator from '../../../app/scripts/lib/migrator'; -import liveMigrations from '../../../app/scripts/migrations'; -import data from '../../../app/scripts/first-time-state'; +import liveMigrations from '../../migrations'; +import data from '../../first-time-state'; +import Migrator from '.'; const stubMigrations = [ { @@ -67,7 +67,7 @@ describe('migrations', function () { }); it('should have tests for all migrations', function () { - const fileNames = fs.readdirSync('./test/unit/migrations/'); + const fileNames = fs.readdirSync('./app/scripts/migrations/'); const testNumbers = fileNames .reduce((acc, filename) => { const name = filename.split('.test.')[0]; diff --git a/test/unit/app/nodeify.test.js b/app/scripts/lib/nodeify.test.js similarity index 97% rename from test/unit/app/nodeify.test.js rename to app/scripts/lib/nodeify.test.js index 2418f40bc..4f2c2a2eb 100644 --- a/test/unit/app/nodeify.test.js +++ b/app/scripts/lib/nodeify.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import nodeify from '../../../app/scripts/lib/nodeify'; +import nodeify from './nodeify'; describe('nodeify', function () { const obj = { diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 1fb59144b..0149e4802 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -5,8 +5,8 @@ import { ethErrors } from 'eth-rpc-errors'; import log from 'loglevel'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; +import createId from '../../../shared/modules/random-id'; import { addHexPrefix } from './util'; -import createId from './random-id'; const hexRe = /^[0-9A-Fa-f]+$/gu; diff --git a/test/unit/app/personal-message-manager.test.js b/app/scripts/lib/personal-message-manager.test.js similarity index 98% rename from test/unit/app/personal-message-manager.test.js rename to app/scripts/lib/personal-message-manager.test.js index 15035a111..c3ce3f615 100644 --- a/test/unit/app/personal-message-manager.test.js +++ b/app/scripts/lib/personal-message-manager.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import PersonalMessageManager from '../../../app/scripts/lib/personal-message-manager'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import PersonalMessageManager from './personal-message-manager'; describe('Personal Message Manager', function () { let messageManager; diff --git a/test/unit/app/seed-phrase-verifier.test.js b/app/scripts/lib/seed-phrase-verifier.test.js similarity index 95% rename from test/unit/app/seed-phrase-verifier.test.js rename to app/scripts/lib/seed-phrase-verifier.test.js index 650f97eb3..d7ac0143f 100644 --- a/test/unit/app/seed-phrase-verifier.test.js +++ b/app/scripts/lib/seed-phrase-verifier.test.js @@ -1,9 +1,9 @@ import assert from 'assert'; import { cloneDeep } from 'lodash'; import KeyringController from 'eth-keyring-controller'; -import firstTimeState from '../../../app/scripts/first-time-state'; -import seedPhraseVerifier from '../../../app/scripts/lib/seed-phrase-verifier'; -import mockEncryptor from '../../lib/mock-encryptor'; +import firstTimeState from '../first-time-state'; +import mockEncryptor from '../../../test/lib/mock-encryptor'; +import seedPhraseVerifier from './seed-phrase-verifier'; describe('SeedPhraseVerifier', function () { describe('verifyAccounts', function () { diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js deleted file mode 100644 index 724500b9b..000000000 --- a/app/scripts/lib/setupFetchDebugging.js +++ /dev/null @@ -1,41 +0,0 @@ -// -// This is a utility to help resolve cases where `window.fetch` throws a -// `TypeError: Failed to Fetch` without any stack or context for the request -// https://github.com/getsentry/sentry-javascript/pull/1293 -// - -export default function setupFetchDebugging() { - if (!window.fetch) { - return; - } - const originalFetch = window.fetch; - - window.fetch = wrappedFetch; - - async function wrappedFetch(...args) { - const initialStack = getCurrentStack(); - try { - return await originalFetch.call(window, ...args); - } catch (err) { - if (!err.stack) { - console.warn( - 'FetchDebugger - fetch encountered an Error without a stack', - err, - ); - console.warn( - 'FetchDebugger - overriding stack to point of original call', - ); - err.stack = initialStack; - } - throw err; - } - } -} - -function getCurrentStack() { - try { - throw new Error('Fake error for generating stack trace'); - } catch (err) { - return err.stack; - } -} diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index eace3f918..09482d24b 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -31,7 +31,7 @@ export const SENTRY_STATE = { featureFlags: true, firstTimeFlowType: true, forgottenPassword: true, - incomingTxLastFetchedBlocksByNetwork: true, + incomingTxLastFetchedBlockByChainId: true, ipfsGateway: true, isAccountMenuOpen: true, isInitialized: true, diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index d1ea6491b..28e7a2534 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -8,7 +8,7 @@ import log from 'loglevel'; import jsonschema from 'jsonschema'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from './random-id'; +import createId from '../../../shared/modules/random-id'; /** * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a diff --git a/test/unit/app/typed-message-manager.test.js b/app/scripts/lib/typed-message-manager.test.js similarity index 97% rename from test/unit/app/typed-message-manager.test.js rename to app/scripts/lib/typed-message-manager.test.js index 5d7333688..c994a586f 100644 --- a/test/unit/app/typed-message-manager.test.js +++ b/app/scripts/lib/typed-message-manager.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import sinon from 'sinon'; -import TypedMessageManager from '../../../app/scripts/lib/typed-message-manager'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import TypedMessageManager from './typed-message-manager'; describe('Typed Message Manager', function () { let typedMessageManager, diff --git a/test/unit/app/util.test.js b/app/scripts/lib/util.test.js similarity index 98% rename from test/unit/app/util.test.js rename to app/scripts/lib/util.test.js index 7f9a285ff..c2753df56 100644 --- a/test/unit/app/util.test.js +++ b/app/scripts/lib/util.test.js @@ -1,8 +1,4 @@ import { strict as assert } from 'assert'; -import { - getEnvironmentType, - sufficientBalance, -} from '../../../app/scripts/lib/util'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { @@ -11,6 +7,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_BACKGROUND, } from '../../../shared/constants/app'; +import { getEnvironmentType, sufficientBalance } from './util'; describe('app utils', function () { describe('getEnvironmentType', function () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3b9dffdff..7e3ec936d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,6 +1,5 @@ import EventEmitter from 'events'; import pump from 'pump'; -import Dnode from 'dnode'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; import { JsonRpcEngine } from 'json-rpc-engine'; @@ -24,7 +23,6 @@ import { CurrencyRateController, PhishingController, } from '@metamask/controllers'; -import { getBackgroundMetaMetricState } from '../../ui/app/selectors'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; import ComposableObservableStore from './lib/ComposableObservableStore'; @@ -61,6 +59,7 @@ import accountImporter from './account-import-strategies'; import seedPhraseVerifier from './lib/seed-phrase-verifier'; import MetaMetricsController from './controllers/metametrics'; import { segment, segmentLegacy } from './lib/segment'; +import createMetaRPCHandler from './lib/createMetaRPCHandler'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -114,7 +113,6 @@ export default class MetamaskController extends EventEmitter { this.approvalController = new ApprovalController({ showApprovalRequest: opts.showUserConfirmation, - defaultApprovalType: 'NO_TYPE', }); this.networkController = new NetworkController(initState.NetworkController); @@ -190,7 +188,13 @@ export default class MetamaskController extends EventEmitter { this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, - networkController: this.networkController, + onNetworkDidChange: this.networkController.on.bind( + this.networkController, + NETWORK_EVENTS.NETWORK_DID_CHANGE, + ), + getCurrentChainId: this.networkController.getCurrentChainId.bind( + this.networkController, + ), preferencesController: this.preferencesController, initState: initState.IncomingTransactionsController, }); @@ -318,7 +322,7 @@ export default class MetamaskController extends EventEmitter { status === TRANSACTION_STATUSES.CONFIRMED || status === TRANSACTION_STATUSES.FAILED ) { - const txMeta = this.txController.txStateManager.getTx(txId); + const txMeta = this.txController.txStateManager.getTransaction(txId); const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); let rpcPrefs = {}; if (txMeta.chainId) { @@ -330,12 +334,23 @@ export default class MetamaskController extends EventEmitter { this.platform.showTransactionNotification(txMeta, rpcPrefs); const { txReceipt } = txMeta; + const metamaskState = await this.getState(); + if (txReceipt && txReceipt.status === '0x0') { - this.sendBackgroundMetaMetrics({ - action: 'Transactions', - name: 'On Chain Failure', - customVariables: { errorMessage: txMeta.simulationFails?.reason }, - }); + this.metaMetricsController.trackEvent( + { + category: 'Background', + properties: { + action: 'Transactions', + errorMessage: txMeta.simulationFails?.reason, + numberOfTokens: metamaskState.tokens.length, + numberOfAccounts: Object.keys(metamaskState.accounts).length, + }, + }, + { + matomoEvent: true, + }, + ); } } }); @@ -489,9 +504,11 @@ export default class MetamaskController extends EventEmitter { processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), getPendingTransactionByHash: (hash) => - this.txController.getFilteredTxList({ - hash, - status: TRANSACTION_STATUSES.SUBMITTED, + this.txController.getTransactions({ + searchCriteria: { + hash, + status: TRANSACTION_STATUSES.SUBMITTED, + }, })[0], }; const providerProxy = this.networkController.initializeProvider( @@ -580,7 +597,7 @@ export default class MetamaskController extends EventEmitter { const isInitialized = Boolean(vault); return { - ...{ isInitialized }, + isInitialized, ...this.memStore.getFlatState(), }; } @@ -588,7 +605,7 @@ export default class MetamaskController extends EventEmitter { /** * Returns an Object containing API Callback Functions. * These functions are the interface for the UI. - * The API object can be transmitted over a stream with dnode. + * The API object can be transmitted over a stream via JSON-RPC. * * @returns {Object} Object containing API functions. */ @@ -748,7 +765,6 @@ export default class MetamaskController extends EventEmitter { ), createCancelTransaction: nodeify(this.createCancelTransaction, this), createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this), - getFilteredTxList: nodeify(txController.getFilteredTxList, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController), estimateGas: nodeify(this.estimateGas, this), getPendingNonce: nodeify(this.getPendingNonce, this), @@ -1061,10 +1077,6 @@ export default class MetamaskController extends EventEmitter { }); } - getCurrentNetwork = () => { - return this.networkController.store.getState().network; - }; - /** * Collects all the information that we want to share * with the mobile client for syncing purposes @@ -1861,10 +1873,11 @@ export default class MetamaskController extends EventEmitter { * @param {string} [customGasPrice] - the hex value to use for the cancel transaction * @returns {Object} MetaMask state */ - async createCancelTransaction(originalTxId, customGasPrice) { + async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) { await this.txController.createCancelTransaction( originalTxId, customGasPrice, + customGasLimit, ); const state = await this.getState(); return state; @@ -1989,36 +2002,34 @@ export default class MetamaskController extends EventEmitter { } /** - * A method for providing our API over a stream using Dnode. + * A method for providing our API over a stream using JSON-RPC. * @param {*} outStream - The stream to provide our API over. */ setupControllerConnection(outStream) { const api = this.getApi(); - // the "weak: false" option is for nodejs only (eg unit tests) - // it is a workaround for node v12 support - const dnode = Dnode(api, { weak: false }); + // report new active controller connection this.activeControllerConnections += 1; this.emit('controllerConnectionChanged', this.activeControllerConnections); - // connect dnode api to remote connection - pump(outStream, dnode, outStream, (err) => { - // report new active controller connection + + // set up postStream transport + outStream.on('data', createMetaRPCHandler(api, outStream)); + const handleUpdate = (update) => { + // send notification to client-side + outStream.write({ + jsonrpc: '2.0', + method: 'sendUpdate', + params: [update], + }); + }; + this.on('update', handleUpdate); + outStream.on('end', () => { this.activeControllerConnections -= 1; this.emit( 'controllerConnectionChanged', this.activeControllerConnections, ); - // report any error - if (err) { - log.error(err); - } - }); - dnode.on('remote', (remote) => { - // push updates to popup - const sendUpdate = (update) => remote.sendUpdate(update); - this.on('update', sendUpdate); - // remove update listener once the connection ends - dnode.on('end', () => this.removeListener('update', sendUpdate)); + this.removeListener('update', handleUpdate); }); } @@ -2416,32 +2427,6 @@ export default class MetamaskController extends EventEmitter { return nonceLock.nextNonce; } - async sendBackgroundMetaMetrics({ action, name, customVariables } = {}) { - if (!action || !name) { - throw new Error('Must provide action and name.'); - } - - const metamaskState = await this.getState(); - const additionalProperties = getBackgroundMetaMetricState({ - metamask: metamaskState, - }); - - this.metaMetricsController.trackEvent( - { - event: name, - category: 'Background', - properties: { - action, - ...additionalProperties, - ...customVariables, - }, - }, - { - matomoEvent: true, - }, - ); - } - /** * Migrate address book state from old to new chainId. * diff --git a/test/unit/app/controllers/metamask-controller.test.js b/app/scripts/metamask-controller.test.js similarity index 97% rename from test/unit/app/controllers/metamask-controller.test.js rename to app/scripts/metamask-controller.test.js index d92da4ed4..6756025ad 100644 --- a/test/unit/app/controllers/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -6,12 +6,23 @@ import ethUtil from 'ethereumjs-util'; import { obj as createThoughStream } from 'through2'; import EthQuery from 'eth-query'; import proxyquire from 'proxyquire'; -import firstTimeState from '../../localhostState'; -import createTxMeta from '../../../lib/createTxMeta'; -import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; - -const Ganache = require('../../../e2e/ganache'); +import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; +import createTxMeta from '../../test/lib/createTxMeta'; +import { NETWORK_TYPE_RPC } from '../../shared/constants/network'; +import { addHexPrefix } from './lib/util'; + +const Ganache = require('../../test/e2e/ganache'); + +const firstTimeState = { + config: {}, + NetworkController: { + provider: { + type: NETWORK_TYPE_RPC, + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + }, + }, +}; const ganacheServer = new Ganache(); @@ -67,13 +78,10 @@ const createLoggerMiddlewareMock = () => (req, res, next) => { next(); }; -const MetaMaskController = proxyquire( - '../../../../app/scripts/metamask-controller', - { - './controllers/threebox': { default: ThreeBoxControllerMock }, - './lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock }, - }, -).default; +const MetaMaskController = proxyquire('./metamask-controller', { + './controllers/threebox': { default: ThreeBoxControllerMock }, + './lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock }, +}).default; const currentNetworkId = '42'; const DEFAULT_LABEL = 'Account 1'; @@ -735,7 +743,7 @@ describe('MetaMaskController', function () { selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'); getNetworkstub.returns(42); - metamaskController.txController.txStateManager._saveTxList([ + metamaskController.txController.txStateManager._addTransactionsToState([ createTxMeta({ id: 1, status: TRANSACTION_STATUSES.UNAPPROVED, @@ -763,7 +771,7 @@ describe('MetaMaskController', function () { await metamaskController.resetAccount(); assert.equal( - metamaskController.txController.txStateManager.getTx(1), + metamaskController.txController.txStateManager.getTransaction(1), undefined, ); }); @@ -1108,7 +1116,7 @@ describe('MetaMaskController', function () { }); describe('#setupTrustedCommunication', function () { - it('sets up controller dnode api for trusted communication', async function () { + it('sets up controller JSON-RPC api for trusted communication', async function () { const messageSender = { url: 'http://mycrypto.com', tab: {}, diff --git a/test/unit/migrations/021.test.js b/app/scripts/migrations/021.test.js similarity index 80% rename from test/unit/migrations/021.test.js rename to app/scripts/migrations/021.test.js index d17615713..45c727e57 100644 --- a/test/unit/migrations/021.test.js +++ b/app/scripts/migrations/021.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import wallet2 from '../../lib/migrations/002.json'; -import migration21 from '../../../app/scripts/migrations/021'; +import wallet2 from '../../../test/lib/migrations/002.json'; +import migration21 from './021'; describe('wallet2 is migrated successfully with out the BlacklistController', function () { it('should delete BlacklistController key', function (done) { diff --git a/test/unit/migrations/022.test.js b/app/scripts/migrations/022.test.js similarity index 95% rename from test/unit/migrations/022.test.js rename to app/scripts/migrations/022.test.js index 537a2ba41..a102bcb7e 100644 --- a/test/unit/migrations/022.test.js +++ b/app/scripts/migrations/022.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import migration22 from '../../../app/scripts/migrations/022'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration22 from './022'; const properTime = new Date().getTime(); const storage = { diff --git a/test/unit/migrations/023.test.js b/app/scripts/migrations/023.test.js similarity index 98% rename from test/unit/migrations/023.test.js rename to app/scripts/migrations/023.test.js index d1eb7109b..0b40679b2 100644 --- a/test/unit/migrations/023.test.js +++ b/app/scripts/migrations/023.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import migration23 from '../../../app/scripts/migrations/023'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration23 from './023'; const storage = { meta: {}, diff --git a/test/unit/migrations/024.test.js b/app/scripts/migrations/024.test.js similarity index 93% rename from test/unit/migrations/024.test.js rename to app/scripts/migrations/024.test.js index a90729c5d..b2056193d 100644 --- a/test/unit/migrations/024.test.js +++ b/app/scripts/migrations/024.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; -import migration24 from '../../../app/scripts/migrations/024'; -import data from '../../../app/scripts/first-time-state'; +import data from '../first-time-state'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration24 from './024'; const firstTimeState = { meta: {}, diff --git a/test/unit/migrations/025.test.js b/app/scripts/migrations/025.test.js similarity index 93% rename from test/unit/migrations/025.test.js rename to app/scripts/migrations/025.test.js index f99db5010..7d666c517 100644 --- a/test/unit/migrations/025.test.js +++ b/app/scripts/migrations/025.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; -import migration25 from '../../../app/scripts/migrations/025'; -import data from '../../../app/scripts/first-time-state'; +import data from '../first-time-state'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration25 from './025'; const firstTimeState = { meta: {}, diff --git a/test/unit/migrations/026.test.js b/app/scripts/migrations/026.test.js similarity index 90% rename from test/unit/migrations/026.test.js rename to app/scripts/migrations/026.test.js index 684300ba5..71245115b 100644 --- a/test/unit/migrations/026.test.js +++ b/app/scripts/migrations/026.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import firstTimeState from '../../../app/scripts/first-time-state'; -import migration26 from '../../../app/scripts/migrations/026'; +import firstTimeState from '../first-time-state'; +import migration26 from './026'; const oldStorage = { meta: { version: 25 }, diff --git a/test/unit/migrations/027.test.js b/app/scripts/migrations/027.test.js similarity index 92% rename from test/unit/migrations/027.test.js rename to app/scripts/migrations/027.test.js index c6d2ada97..2687e17c2 100644 --- a/test/unit/migrations/027.test.js +++ b/app/scripts/migrations/027.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; -import firstTimeState from '../../../app/scripts/first-time-state'; -import migration27 from '../../../app/scripts/migrations/027'; +import firstTimeState from '../first-time-state'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration27 from './027'; const oldStorage = { meta: {}, diff --git a/test/unit/migrations/028.test.js b/app/scripts/migrations/028.test.js similarity index 94% rename from test/unit/migrations/028.test.js rename to app/scripts/migrations/028.test.js index d7483d116..01381e754 100644 --- a/test/unit/migrations/028.test.js +++ b/app/scripts/migrations/028.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import firstTimeState from '../../../app/scripts/first-time-state'; -import migration28 from '../../../app/scripts/migrations/028'; +import firstTimeState from '../first-time-state'; +import migration28 from './028'; const oldStorage = { meta: {}, diff --git a/test/unit/migrations/029.test.js b/app/scripts/migrations/029.test.js similarity index 96% rename from test/unit/migrations/029.test.js rename to app/scripts/migrations/029.test.js index 2eb97c128..90d698fd7 100644 --- a/test/unit/migrations/029.test.js +++ b/app/scripts/migrations/029.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import migration29 from '../../../app/scripts/migrations/029'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import migration29 from './029'; const properTime = new Date().getTime(); const storage = { diff --git a/test/unit/migrations/030.test.js b/app/scripts/migrations/030.test.js similarity index 95% rename from test/unit/migrations/030.test.js rename to app/scripts/migrations/030.test.js index 3610a72b0..985aa02e1 100644 --- a/test/unit/migrations/030.test.js +++ b/app/scripts/migrations/030.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migrationTemplate from '../../../app/scripts/migrations/030'; +import migrationTemplate from './030'; const storage = { meta: {}, diff --git a/test/unit/migrations/031.test.js b/app/scripts/migrations/031.test.js similarity index 96% rename from test/unit/migrations/031.test.js rename to app/scripts/migrations/031.test.js index 57159176b..d7b6ee046 100644 --- a/test/unit/migrations/031.test.js +++ b/app/scripts/migrations/031.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration31 from '../../../app/scripts/migrations/031'; +import migration31 from './031'; describe('migration #31', function () { it('should set completedOnboarding to true if vault exists', function (done) { diff --git a/test/unit/migrations/033.test.js b/app/scripts/migrations/033.test.js similarity index 93% rename from test/unit/migrations/033.test.js rename to app/scripts/migrations/033.test.js index ca8018460..bb12e83d7 100644 --- a/test/unit/migrations/033.test.js +++ b/app/scripts/migrations/033.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration33 from '../../../app/scripts/migrations/033'; +import migration33 from './033'; describe('Migration to delete notice controller', function () { const oldStorage = { diff --git a/test/unit/migrations/034.test.js b/app/scripts/migrations/034.test.js similarity index 97% rename from test/unit/migrations/034.test.js rename to app/scripts/migrations/034.test.js index e9554793d..bfb929997 100644 --- a/test/unit/migrations/034.test.js +++ b/app/scripts/migrations/034.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration34 from '../../../app/scripts/migrations/034'; +import migration34 from './034'; describe('migration #34', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/035.test.js b/app/scripts/migrations/035.test.js similarity index 97% rename from test/unit/migrations/035.test.js rename to app/scripts/migrations/035.test.js index f3e600df2..385f12fb5 100644 --- a/test/unit/migrations/035.test.js +++ b/app/scripts/migrations/035.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration35 from '../../../app/scripts/migrations/035'; +import migration35 from './035'; describe('migration #35', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/036.test.js b/app/scripts/migrations/036.test.js similarity index 97% rename from test/unit/migrations/036.test.js rename to app/scripts/migrations/036.test.js index 679fc8a3b..679080bf7 100644 --- a/test/unit/migrations/036.test.js +++ b/app/scripts/migrations/036.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration36 from '../../../app/scripts/migrations/036'; +import migration36 from './036'; describe('migration #36', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/037.test.js b/app/scripts/migrations/037.test.js similarity index 98% rename from test/unit/migrations/037.test.js rename to app/scripts/migrations/037.test.js index 78fe85cb5..0c145bccf 100644 --- a/test/unit/migrations/037.test.js +++ b/app/scripts/migrations/037.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration37 from '../../../app/scripts/migrations/037'; +import migration37 from './037'; describe('migration #37', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/038.test.js b/app/scripts/migrations/038.test.js similarity index 95% rename from test/unit/migrations/038.test.js rename to app/scripts/migrations/038.test.js index c1e8e2a1e..93d826f78 100644 --- a/test/unit/migrations/038.test.js +++ b/app/scripts/migrations/038.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration38 from '../../../app/scripts/migrations/038'; +import migration38 from './038'; describe('migration #38', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/039.test.js b/app/scripts/migrations/039.test.js similarity index 99% rename from test/unit/migrations/039.test.js rename to app/scripts/migrations/039.test.js index 8cfc3e0b1..55ba12982 100644 --- a/test/unit/migrations/039.test.js +++ b/app/scripts/migrations/039.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration39 from '../../../app/scripts/migrations/039'; +import migration39 from './039'; describe('migration #39', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/040.test.js b/app/scripts/migrations/040.test.js similarity index 94% rename from test/unit/migrations/040.test.js rename to app/scripts/migrations/040.test.js index 98fce305b..f4d1de68e 100644 --- a/test/unit/migrations/040.test.js +++ b/app/scripts/migrations/040.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration40 from '../../../app/scripts/migrations/040'; +import migration40 from './040'; describe('migration #40', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/041.test.js b/app/scripts/migrations/041.test.js similarity index 96% rename from test/unit/migrations/041.test.js rename to app/scripts/migrations/041.test.js index e61ce66e2..7c6e4c567 100644 --- a/test/unit/migrations/041.test.js +++ b/app/scripts/migrations/041.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration41 from '../../../app/scripts/migrations/041'; +import migration41 from './041'; describe('migration #41', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/042.test.js b/app/scripts/migrations/042.test.js similarity index 96% rename from test/unit/migrations/042.test.js rename to app/scripts/migrations/042.test.js index 9159fb756..7cca8d2a7 100644 --- a/test/unit/migrations/042.test.js +++ b/app/scripts/migrations/042.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration42 from '../../../app/scripts/migrations/042'; +import migration42 from './042'; describe('migration #42', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/043.test.js b/app/scripts/migrations/043.test.js similarity index 95% rename from test/unit/migrations/043.test.js rename to app/scripts/migrations/043.test.js index 25ac0336d..aa37f13d7 100644 --- a/test/unit/migrations/043.test.js +++ b/app/scripts/migrations/043.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration43 from '../../../app/scripts/migrations/043'; +import migration43 from './043'; describe('migration #43', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/044.test.js b/app/scripts/migrations/044.test.js similarity index 96% rename from test/unit/migrations/044.test.js rename to app/scripts/migrations/044.test.js index 0b53b506b..35b95762d 100644 --- a/test/unit/migrations/044.test.js +++ b/app/scripts/migrations/044.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration44 from '../../../app/scripts/migrations/044'; +import migration44 from './044'; describe('migration #44', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/045.test.js b/app/scripts/migrations/045.test.js similarity index 96% rename from test/unit/migrations/045.test.js rename to app/scripts/migrations/045.test.js index bd102a927..907489195 100644 --- a/test/unit/migrations/045.test.js +++ b/app/scripts/migrations/045.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration45 from '../../../app/scripts/migrations/045'; +import migration45 from './045'; describe('migration #45', function () { it('should update the version metadata', function (done) { diff --git a/test/unit/migrations/046.test.js b/app/scripts/migrations/046.test.js similarity index 94% rename from test/unit/migrations/046.test.js rename to app/scripts/migrations/046.test.js index 43a79da94..05e3b93dd 100644 --- a/test/unit/migrations/046.test.js +++ b/app/scripts/migrations/046.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration46 from '../../../app/scripts/migrations/046'; +import migration46 from './046'; describe('migration #46', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/047.test.js b/app/scripts/migrations/047.test.js similarity index 97% rename from test/unit/migrations/047.test.js rename to app/scripts/migrations/047.test.js index 12dc35fdf..7124a274c 100644 --- a/test/unit/migrations/047.test.js +++ b/app/scripts/migrations/047.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration47 from '../../../app/scripts/migrations/047'; +import migration47 from './047'; describe('migration #47', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/048.test.js b/app/scripts/migrations/048.test.js similarity index 99% rename from test/unit/migrations/048.test.js rename to app/scripts/migrations/048.test.js index cd731c38e..a46212cfd 100644 --- a/test/unit/migrations/048.test.js +++ b/app/scripts/migrations/048.test.js @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import migration48 from '../../../app/scripts/migrations/048'; +import migration48 from './048'; const localhostNetwork = { rpcUrl: 'http://localhost:8545', diff --git a/test/unit/migrations/049.test.js b/app/scripts/migrations/049.test.js similarity index 97% rename from test/unit/migrations/049.test.js rename to app/scripts/migrations/049.test.js index df441a47a..5242fab52 100644 --- a/test/unit/migrations/049.test.js +++ b/app/scripts/migrations/049.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migration49 from '../../../app/scripts/migrations/049'; +import migration49 from './049'; describe('migration #49', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/050.test.js b/app/scripts/migrations/050.test.js similarity index 97% rename from test/unit/migrations/050.test.js rename to app/scripts/migrations/050.test.js index 05aa735ae..fd942ee87 100644 --- a/test/unit/migrations/050.test.js +++ b/app/scripts/migrations/050.test.js @@ -1,6 +1,6 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import migration50 from '../../../app/scripts/migrations/050'; +import migration50 from './050'; const LEGACY_LOCAL_STORAGE_KEYS = [ 'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED', diff --git a/test/unit/migrations/051.test.js b/app/scripts/migrations/051.test.js similarity index 98% rename from test/unit/migrations/051.test.js rename to app/scripts/migrations/051.test.js index 4d1428a87..8d32fbf2c 100644 --- a/test/unit/migrations/051.test.js +++ b/app/scripts/migrations/051.test.js @@ -1,9 +1,9 @@ import { strict as assert } from 'assert'; -import migration51 from '../../../app/scripts/migrations/051'; import { INFURA_PROVIDER_TYPES, NETWORK_TYPE_TO_ID_MAP, } from '../../../shared/constants/network'; +import migration51 from './051'; describe('migration #51', function () { it('should update the version metadata', async function () { diff --git a/test/unit/migrations/052.test.js b/app/scripts/migrations/052.test.js similarity index 99% rename from test/unit/migrations/052.test.js rename to app/scripts/migrations/052.test.js index 23dc7c603..8d9d13afb 100644 --- a/test/unit/migrations/052.test.js +++ b/app/scripts/migrations/052.test.js @@ -1,5 +1,4 @@ import assert from 'assert'; -import migration52 from '../../../app/scripts/migrations/052'; import { GOERLI, GOERLI_CHAIN_ID, @@ -13,6 +12,7 @@ import { ROPSTEN, ROPSTEN_CHAIN_ID, } from '../../../shared/constants/network'; +import migration52 from './052'; const TOKEN1 = { symbol: 'TST', address: '0x10', decimals: 18 }; const TOKEN2 = { symbol: 'TXT', address: '0x11', decimals: 18 }; diff --git a/app/scripts/migrations/053.js b/app/scripts/migrations/053.js new file mode 100644 index 000000000..f9d4dc55c --- /dev/null +++ b/app/scripts/migrations/053.js @@ -0,0 +1,46 @@ +import { cloneDeep } from 'lodash'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; + +const version = 53; + +/** + * Deprecate transactionCategory and consolidate on 'type' + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + versionedData.data = transformState(state); + return versionedData; + }, +}; + +function transformState(state) { + const transactions = state?.TransactionController?.transactions; + const incomingTransactions = + state?.IncomingTransactionsController?.incomingTransactions; + if (Array.isArray(transactions)) { + transactions.forEach((transaction) => { + if ( + transaction.type !== TRANSACTION_TYPES.RETRY && + transaction.type !== TRANSACTION_TYPES.CANCEL + ) { + transaction.type = transaction.transactionCategory; + } + delete transaction.transactionCategory; + }); + } + if (incomingTransactions) { + const incomingTransactionsEntries = Object.entries(incomingTransactions); + incomingTransactionsEntries.forEach(([key, transaction]) => { + delete transaction.transactionCategory; + state.IncomingTransactionsController.incomingTransactions[key] = { + ...transaction, + type: TRANSACTION_TYPES.INCOMING, + }; + }); + } + return state; +} diff --git a/app/scripts/migrations/053.test.js b/app/scripts/migrations/053.test.js new file mode 100644 index 000000000..788110afa --- /dev/null +++ b/app/scripts/migrations/053.test.js @@ -0,0 +1,136 @@ +import { strict as assert } from 'assert'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; +import migration53 from './053'; + +describe('migration #53', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 52, + }, + data: {}, + }; + + const newStorage = await migration53.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 53, + }); + }); + + it('should update type of standard transactions', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [ + { + type: TRANSACTION_TYPES.CANCEL, + transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + txParams: { foo: 'bar' }, + }, + { + type: 'standard', + transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + txParams: { foo: 'bar' }, + }, + { + type: 'standard', + transactionCategory: TRANSACTION_TYPES.CONTRACT_INTERACTION, + txParams: { foo: 'bar' }, + }, + { + type: TRANSACTION_TYPES.RETRY, + transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + txParams: { foo: 'bar' }, + }, + ], + }, + IncomingTransactionsController: { + incomingTransactions: { + test: { + transactionCategory: 'incoming', + txParams: { + foo: 'bar', + }, + }, + }, + }, + foo: 'bar', + }, + }; + + const newStorage = await migration53.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + TransactionController: { + transactions: [ + { type: TRANSACTION_TYPES.CANCEL, txParams: { foo: 'bar' } }, + { type: TRANSACTION_TYPES.SENT_ETHER, txParams: { foo: 'bar' } }, + { + type: TRANSACTION_TYPES.CONTRACT_INTERACTION, + txParams: { foo: 'bar' }, + }, + { type: TRANSACTION_TYPES.RETRY, txParams: { foo: 'bar' } }, + ], + }, + IncomingTransactionsController: { + incomingTransactions: { + test: { + type: 'incoming', + txParams: { + foo: 'bar', + }, + }, + }, + }, + foo: 'bar', + }); + }); + + it('should do nothing if transactions state does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + bar: 'baz', + }, + IncomingTransactionsController: { + foo: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration53.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should do nothing if transactions state is empty', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [], + bar: 'baz', + }, + IncomingTransactionsController: { + incomingTransactions: {}, + baz: 'bar', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration53.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should do nothing if state is empty', async function () { + const oldStorage = { + meta: {}, + data: {}, + }; + + const newStorage = await migration53.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); +}); diff --git a/app/scripts/migrations/054.js b/app/scripts/migrations/054.js new file mode 100644 index 000000000..105a46750 --- /dev/null +++ b/app/scripts/migrations/054.js @@ -0,0 +1,75 @@ +import { cloneDeep } from 'lodash'; + +const version = 54; + +function isValidDecimals(decimals) { + return ( + typeof decimals === 'number' || + (typeof decimals === 'string' && decimals.match(/^(0x)?\d+$/u)) + ); +} + +/** + * Migrates preference tokens with decimals typed as string to number. + * It also removes any tokens with corrupted or inconvertible decimal values. + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(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.PreferencesController) { + return newState; + } + + const tokens = newState.PreferencesController.tokens || []; + // Filter out any tokens with corrupted decimal values + const validTokens = tokens.filter(({ decimals }) => + isValidDecimals(decimals), + ); + for (const token of validTokens) { + // In the case of a decimal value type string, convert to a number. + if (typeof token.decimals === 'string') { + // eslint-disable-next-line radix + token.decimals = parseInt(token.decimals); + } + } + newState.PreferencesController.tokens = validTokens; + + const { accountTokens } = newState.PreferencesController; + if (accountTokens && typeof accountTokens === 'object') { + for (const address of Object.keys(accountTokens)) { + const networkTokens = accountTokens[address]; + if (networkTokens && typeof networkTokens === 'object') { + for (const network of Object.keys(networkTokens)) { + const tokensOnNetwork = networkTokens[network] || []; + // Filter out any tokens with corrupted decimal values + const validTokensOnNetwork = tokensOnNetwork.filter(({ decimals }) => + isValidDecimals(decimals), + ); + // In the case of a decimal value type string, convert to a number. + for (const token of validTokensOnNetwork) { + if (typeof token.decimals === 'string') { + // eslint-disable-next-line radix + token.decimals = parseInt(token.decimals); + } + } + networkTokens[network] = validTokensOnNetwork; + } + } + } + } + newState.PreferencesController.accountTokens = accountTokens; + + return newState; +} diff --git a/app/scripts/migrations/054.test.js b/app/scripts/migrations/054.test.js new file mode 100644 index 000000000..b0d78bcfd --- /dev/null +++ b/app/scripts/migrations/054.test.js @@ -0,0 +1,687 @@ +import { strict as assert } from 'assert'; +import { + MAINNET_CHAIN_ID, + ROPSTEN_CHAIN_ID, +} from '../../../shared/constants/network'; +import migration54 from './054'; + +describe('migration #54', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 53, + }, + data: {}, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 54, + }); + }); + + it('should retype instance of 0 decimal values to numbers [tokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + accountTokens: [], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + accountTokens: [], + }, + }); + }); + + it('should do nothing if all decimal value typings are correct [tokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + accountTokens: [], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + accountTokens: [], + }, + }); + }); + + it('should retype instance of 0 decimal values to numbers [accountTokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + }, + tokens: [], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + }, + tokens: [], + }, + }); + }); + + it('should do nothing if all decimal value typings are correct [accountTokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + }, + tokens: [], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + }, + tokens: [], + }, + }); + }); + + it('should retype instance of 0 decimal values to numbers [accountTokens and tokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + }, + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + }, + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + }); + }); + + it('should retype instance of 0 decimal values to numbers, and remove tokens with corrupted decimal values [accountTokens and tokens]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '0', + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 'corrupted_decimal?', + symbol: 'SOR', + }, + ], + }, + }, + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: '0', + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: '18xx', + symbol: 'SOR', + }, + ], + }, + }, + }; + + const newStorage = await migration54.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + PreferencesController: { + accountTokens: { + '0x1111': { + [MAINNET_CHAIN_ID]: [ + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + { + address: '0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205', + decimals: 0, + symbol: 'SOR', + }, + ], + }, + '0x1112': { + [ROPSTEN_CHAIN_ID]: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + ], + }, + }, + tokens: [ + { + address: '0x06012c8cf97bead5deae237070f9587f8e7a266d', + decimals: 0, + symbol: 'CK', + }, + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + decimals: 18, + symbol: 'BAT', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + decimals: 18, + symbol: 'LINK', + }, + ], + }, + }); + }); +}); diff --git a/app/scripts/migrations/055.js b/app/scripts/migrations/055.js new file mode 100644 index 000000000..98676bc63 --- /dev/null +++ b/app/scripts/migrations/055.js @@ -0,0 +1,43 @@ +import { cloneDeep, mapKeys } from 'lodash'; +import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; + +const version = 55; + +/** + * replace 'incomingTxLastFetchedBlocksByNetwork' with 'incomingTxLastFetchedBlockByChainId' + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + versionedData.data = transformState(state); + return versionedData; + }, +}; + +const UNKNOWN_CHAIN_ID_KEY = 'UNKNOWN'; + +function transformState(state) { + if ( + state?.IncomingTransactionsController?.incomingTxLastFetchedBlocksByNetwork + ) { + state.IncomingTransactionsController.incomingTxLastFetchedBlockByChainId = mapKeys( + state.IncomingTransactionsController.incomingTxLastFetchedBlocksByNetwork, + // using optional chaining in case user's state has fetched blocks for + // RPC network types (which don't map to a single chainId). This should + // not be possible, but it's safer + (_, key) => NETWORK_TYPE_TO_ID_MAP[key]?.chainId ?? UNKNOWN_CHAIN_ID_KEY, + ); + // Now that mainnet and test net last fetched blocks are keyed by their + // respective chainIds, we can safely delete anything we had for custom + // networks. Any custom network that shares a chainId with one of the + // aforementioned networks will use the value stored by chainId. + delete state.IncomingTransactionsController + .incomingTxLastFetchedBlockByChainId[UNKNOWN_CHAIN_ID_KEY]; + delete state.IncomingTransactionsController + .incomingTxLastFetchedBlocksByNetwork; + } + return state; +} diff --git a/app/scripts/migrations/055.test.js b/app/scripts/migrations/055.test.js new file mode 100644 index 000000000..449c6291f --- /dev/null +++ b/app/scripts/migrations/055.test.js @@ -0,0 +1,96 @@ +import { strict as assert } from 'assert'; +import { + GOERLI, + GOERLI_CHAIN_ID, + KOVAN, + KOVAN_CHAIN_ID, + MAINNET, + MAINNET_CHAIN_ID, + RINKEBY, + RINKEBY_CHAIN_ID, + ROPSTEN, + ROPSTEN_CHAIN_ID, +} from '../../../shared/constants/network'; +import migration55 from './055'; + +describe('migration #55', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 54, + }, + data: {}, + }; + + const newStorage = await migration55.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 55, + }); + }); + + it('should replace incomingTxLastFetchedBlocksByNetwork with incomingTxLastFetchedBlockByChainId, and carry over old values', async function () { + const oldStorage = { + meta: {}, + data: { + IncomingTransactionsController: { + incomingTransactions: { + test: { + transactionCategory: 'incoming', + txParams: { + foo: 'bar', + }, + }, + }, + incomingTxLastFetchedBlocksByNetwork: { + [MAINNET]: 1, + [ROPSTEN]: 2, + [RINKEBY]: 3, + [GOERLI]: 4, + [KOVAN]: 5, + }, + }, + foo: 'bar', + }, + }; + + const newStorage = await migration55.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + IncomingTransactionsController: { + incomingTransactions: + oldStorage.data.IncomingTransactionsController.incomingTransactions, + incomingTxLastFetchedBlockByChainId: { + [MAINNET_CHAIN_ID]: 1, + [ROPSTEN_CHAIN_ID]: 2, + [RINKEBY_CHAIN_ID]: 3, + [GOERLI_CHAIN_ID]: 4, + [KOVAN_CHAIN_ID]: 5, + }, + }, + foo: 'bar', + }); + }); + + it('should do nothing if incomingTxLastFetchedBlocksByNetwork key is not populated', async function () { + const oldStorage = { + meta: {}, + data: { + IncomingTransactionsController: { + foo: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration55.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + it('should do nothing if state is empty', async function () { + const oldStorage = { + meta: {}, + data: {}, + }; + + const newStorage = await migration55.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); +}); diff --git a/app/scripts/migrations/056.js b/app/scripts/migrations/056.js new file mode 100644 index 000000000..f11d4b3f1 --- /dev/null +++ b/app/scripts/migrations/056.js @@ -0,0 +1,51 @@ +import { cloneDeep } from 'lodash'; + +const version = 56; + +/** + * Remove tokens that don't have an address due to + * lack of previous addToken validation. Also removes + * an unwanted, undefined image property + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + + const { PreferencesController } = versionedData.data; + + if (Array.isArray(PreferencesController.tokens)) { + PreferencesController.tokens = PreferencesController.tokens.filter( + ({ address }) => address, + ); + } + + if ( + PreferencesController.accountTokens && + typeof PreferencesController.accountTokens === 'object' + ) { + Object.keys(PreferencesController.accountTokens).forEach((account) => { + const chains = Object.keys( + PreferencesController.accountTokens[account], + ); + chains.forEach((chain) => { + PreferencesController.accountTokens[account][ + chain + ] = PreferencesController.accountTokens[account][chain].filter( + ({ address }) => address, + ); + }); + }); + } + + if ( + PreferencesController.assetImages && + 'undefined' in PreferencesController.assetImages + ) { + delete PreferencesController.assetImages.undefined; + } + + return versionedData; + }, +}; diff --git a/app/scripts/migrations/056.test.js b/app/scripts/migrations/056.test.js new file mode 100644 index 000000000..e311d4aee --- /dev/null +++ b/app/scripts/migrations/056.test.js @@ -0,0 +1,155 @@ +import assert from 'assert'; +import migration56 from './056'; + +const BAD_TOKEN_DATA = { symbol: null, decimals: null }; +const TOKEN2 = { symbol: 'TXT', address: '0x11', decimals: 18 }; +const TOKEN3 = { symbol: 'TVT', address: '0x12', decimals: 18 }; + +describe('migration #56', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 55, + }, + data: { + PreferencesController: { + tokens: [], + accountTokens: {}, + assetImages: {}, + }, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + assert.deepStrictEqual(newStorage.meta, { + version: 56, + }); + }); + + it(`should filter out tokens without a valid address property`, async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [BAD_TOKEN_DATA, TOKEN2, BAD_TOKEN_DATA, TOKEN3], + accountTokens: {}, + assetImages: {}, + }, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data.PreferencesController.tokens, [ + TOKEN2, + TOKEN3, + ]); + }); + + it(`should not filter any tokens when all token information is valid`, async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [TOKEN2, TOKEN3], + accountTokens: {}, + assetImages: {}, + }, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data.PreferencesController.tokens, [ + TOKEN2, + TOKEN3, + ]); + }); + + it(`should filter out accountTokens without a valid address property`, async function () { + const originalAccountTokens = { + '0x1111111111111111111111111': { + '0x1': [TOKEN2, TOKEN3, BAD_TOKEN_DATA], + '0x3': [], + '0x4': [BAD_TOKEN_DATA, BAD_TOKEN_DATA], + }, + '0x1111111111111111111111112': { + '0x1': [TOKEN2], + '0x3': [], + '0x4': [BAD_TOKEN_DATA, BAD_TOKEN_DATA], + }, + }; + + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [], + accountTokens: originalAccountTokens, + assetImages: {}, + }, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + + const desiredResult = { ...originalAccountTokens }; + // The last item in the array was bad and should be removed + desiredResult['0x1111111111111111111111111']['0x1'].pop(); + // All items in 0x4 were bad + desiredResult['0x1111111111111111111111111']['0x4'] = []; + desiredResult['0x1111111111111111111111112']['0x4'] = []; + + assert.deepStrictEqual( + newStorage.data.PreferencesController.accountTokens, + desiredResult, + ); + }); + + it(`should remove a bad assetImages key`, async function () { + const desiredAssetImages = { + '0x514910771af9ca656af840dff83e8264ecf986ca': + 'images/contract/chainlink.svg', + }; + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + tokens: [], + accountTokens: {}, + assetImages: { ...desiredAssetImages, undefined: null }, + }, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + assert.deepStrictEqual( + newStorage.data.PreferencesController.assetImages, + desiredAssetImages, + ); + }); + + it(`token data with no problems should preserve all data`, async function () { + const perfectData = { + tokens: [TOKEN2, TOKEN3], + accountTokens: { + '0x1111111111111111111111111': { + '0x1': [], + '0x3': [TOKEN2], + }, + '0x1111111111111111111111112': { + '0x1': [TOKEN2, TOKEN3], + '0x3': [], + }, + }, + }; + + const oldStorage = { + meta: {}, + data: { + PreferencesController: perfectData, + }, + }; + + const newStorage = await migration56.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data.PreferencesController, perfectData); + }); +}); diff --git a/app/scripts/migrations/057.js b/app/scripts/migrations/057.js new file mode 100644 index 000000000..3cf1ba315 --- /dev/null +++ b/app/scripts/migrations/057.js @@ -0,0 +1,44 @@ +import { cloneDeep, keyBy } from 'lodash'; +import createId from '../../../shared/modules/random-id'; + +const version = 57; + +/** + * replace 'incomingTxLastFetchedBlocksByNetwork' with 'incomingTxLastFetchedBlockByChainId' + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + versionedData.data = transformState(state); + return versionedData; + }, +}; + +function transformState(state) { + if ( + state?.TransactionController?.transactions && + Array.isArray(state.TransactionController.transactions) && + !state.TransactionController.transactions.some( + (item) => + typeof item !== 'object' || typeof item.txParams === 'undefined', + ) + ) { + state.TransactionController.transactions = keyBy( + state.TransactionController.transactions, + // In case for some reason any of a user's transactions do not have an id + // generate a new one for the transaction. + (tx) => { + if (typeof tx.id === 'undefined' || tx.id === null) { + // This mutates the item in the array, so will result in a change to + // the state. + tx.id = createId(); + } + return tx.id; + }, + ); + } + return state; +} diff --git a/app/scripts/migrations/057.test.js b/app/scripts/migrations/057.test.js new file mode 100644 index 000000000..825a78993 --- /dev/null +++ b/app/scripts/migrations/057.test.js @@ -0,0 +1,193 @@ +import { strict as assert } from 'assert'; +import migration57 from './057'; + +describe('migration #57', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 56, + }, + data: {}, + }; + + const newStorage = await migration57.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 57, + }); + }); + + it('should transactions array into an object keyed by id', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [ + { + id: 0, + txParams: { foo: 'bar' }, + }, + { + id: 1, + txParams: { foo: 'bar' }, + }, + { + id: 2, + txParams: { foo: 'bar' }, + }, + { + id: 3, + txParams: { foo: 'bar' }, + }, + ], + }, + foo: 'bar', + }, + }; + + const newStorage = await migration57.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + TransactionController: { + transactions: { + 0: { + id: 0, + txParams: { foo: 'bar' }, + }, + 1: { + id: 1, + txParams: { foo: 'bar' }, + }, + 2: { + id: 2, + txParams: { foo: 'bar' }, + }, + 3: { id: 3, txParams: { foo: 'bar' } }, + }, + }, + foo: 'bar', + }); + }); + + it('should handle transactions without an id, just in case', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [ + { + id: 0, + txParams: { foo: 'bar' }, + }, + { + txParams: { foo: 'bar' }, + }, + { + txParams: { foo: 'bar' }, + }, + { + txParams: { foo: 'bar' }, + }, + ], + }, + foo: 'bar', + }, + }; + + const newStorage = await migration57.migrate(oldStorage); + const expectedTransactions = {}; + for (const transaction of Object.values( + newStorage.data.TransactionController.transactions, + )) { + // Make sure each transaction now has an id. + assert.ok( + typeof transaction.id !== 'undefined', + 'transaction id is undefined', + ); + // Build expected transaction object + expectedTransactions[transaction.id] = transaction; + } + // Ensure that we got the correct number of transactions + assert.equal( + Object.keys(expectedTransactions).length, + oldStorage.data.TransactionController.transactions.length, + ); + // Ensure that the one transaction with id is preserved, even though it is + // a falsy id. + assert.equal(newStorage.data.TransactionController.transactions[0].id, 0); + }); + + it('should not blow up if transactions are not an array', async function () { + const storageWithTransactionsAsString = { + meta: {}, + data: { + TransactionController: { + transactions: 'someone might have weird state in the future', + }, + }, + }; + const storageWithTransactionsAsArrayOfString = { + meta: {}, + data: { + TransactionController: { + transactions: 'someone might have weird state in the future'.split( + '', + ), + }, + }, + }; + const result1 = await migration57.migrate(storageWithTransactionsAsString); + + const result2 = await migration57.migrate( + storageWithTransactionsAsArrayOfString, + ); + + assert.deepEqual(storageWithTransactionsAsString.data, result1.data); + assert.deepEqual(storageWithTransactionsAsArrayOfString.data, result2.data); + }); + + it('should do nothing if transactions state does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + bar: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration57.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should convert empty array into empty object', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [], + bar: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration57.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + TransactionController: { + transactions: {}, + bar: 'baz', + }, + foo: 'bar', + }); + }); + + it('should do nothing if state is empty', async function () { + const oldStorage = { + meta: {}, + data: {}, + }; + + const newStorage = await migration57.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index b4368c1b5..8d56dcc85 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -57,6 +57,11 @@ const migrations = [ require('./050').default, require('./051').default, require('./052').default, + require('./053').default, + require('./054').default, + require('./055').default, + require('./056').default, + require('./057').default, ]; export default migrations; diff --git a/test/unit/migrations/migrations.test.js b/app/scripts/migrations/migrations.test.js similarity index 89% rename from test/unit/migrations/migrations.test.js rename to app/scripts/migrations/migrations.test.js index d5fcd6bdc..a7c921323 100644 --- a/test/unit/migrations/migrations.test.js +++ b/app/scripts/migrations/migrations.test.js @@ -1,18 +1,18 @@ import assert from 'assert'; -import wallet1 from '../../lib/migrations/001.json'; -import vault4 from '../../lib/migrations/004.json'; -import migration2 from '../../../app/scripts/migrations/002'; -import migration3 from '../../../app/scripts/migrations/003'; -import migration4 from '../../../app/scripts/migrations/004'; -import migration5 from '../../../app/scripts/migrations/005'; -import migration6 from '../../../app/scripts/migrations/006'; -import migration7 from '../../../app/scripts/migrations/007'; -import migration8 from '../../../app/scripts/migrations/008'; -import migration9 from '../../../app/scripts/migrations/009'; -import migration10 from '../../../app/scripts/migrations/010'; -import migration11 from '../../../app/scripts/migrations/011'; -import migration12 from '../../../app/scripts/migrations/012'; -import migration13 from '../../../app/scripts/migrations/013'; +import wallet1 from '../../../test/lib/migrations/001.json'; +import vault4 from '../../../test/lib/migrations/004.json'; +import migration2 from './002'; +import migration3 from './003'; +import migration4 from './004'; +import migration5 from './005'; +import migration6 from './006'; +import migration7 from './007'; +import migration8 from './008'; +import migration9 from './009'; +import migration10 from './010'; +import migration11 from './011'; +import migration12 from './012'; +import migration13 from './013'; let vault5, vault6, vault7, vault8, vault9; // vault10, vault11 diff --git a/test/unit/migrations/template.test.js b/app/scripts/migrations/template.test.js similarity index 82% rename from test/unit/migrations/template.test.js rename to app/scripts/migrations/template.test.js index 79b127ed9..92a4e8937 100644 --- a/test/unit/migrations/template.test.js +++ b/app/scripts/migrations/template.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import migrationTemplate from '../../../app/scripts/migrations/template'; +import migrationTemplate from './template'; const storage = { meta: {}, diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js index 9ade869f0..f036461c3 100644 --- a/app/scripts/phishing-detect.js +++ b/app/scripts/phishing-detect.js @@ -1,8 +1,7 @@ import querystring from 'querystring'; -import { EventEmitter } from 'events'; -import dnode from 'dnode'; import PortStream from 'extension-port-stream'; import extension from 'extensionizer'; +import createRandomId from '../../shared/modules/random-id'; import { setupMultiplex } from './lib/stream-utils'; import { getEnvironmentType } from './lib/util'; import ExtensionPlatform from './platforms/extension'; @@ -22,36 +21,15 @@ function start() { }); const connectionStream = new PortStream(extensionPort); const mx = setupMultiplex(connectionStream); - setupControllerConnection( - mx.createStream('controller'), - (err, metaMaskController) => { - if (err) { - return; - } - - const continueLink = document.getElementById('unsafe-continue'); - continueLink.addEventListener('click', () => { - metaMaskController.safelistPhishingDomain(suspect.hostname); - window.location.href = suspect.href; - }); - }, - ); -} - -function setupControllerConnection(connectionStream, cb) { - const eventEmitter = new EventEmitter(); - // the "weak: false" option is for nodejs only (eg unit tests) - // it is a workaround for node v12 support - const metaMaskControllerDnode = dnode( - { - sendUpdate(state) { - eventEmitter.emit('update', state); - }, - }, - { weak: false }, - ); - connectionStream.pipe(metaMaskControllerDnode).pipe(connectionStream); - metaMaskControllerDnode.once('remote', (backgroundConnection) => - cb(null, backgroundConnection), - ); + const backgroundConnection = mx.createStream('controller'); + const continueLink = document.getElementById('unsafe-continue'); + continueLink.addEventListener('click', () => { + backgroundConnection.write({ + jsonrpc: '2.0', + method: 'safelistPhishingDomain', + params: [suspect.hostname], + id: createRandomId(), + }); + window.location.href = suspect.href; + }); } diff --git a/app/scripts/runLockdown.js b/app/scripts/runLockdown.js index 8dc1b0154..2918368e7 100644 --- a/app/scripts/runLockdown.js +++ b/app/scripts/runLockdown.js @@ -6,6 +6,7 @@ try { errorTaming: 'unsafe', mathTaming: 'unsafe', dateTaming: 'unsafe', + overrideTaming: 'severe', }); } catch (error) { // If the `lockdown` call throws an exception, it interferes with the diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 0d048191c..b00c3d6da 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -2,11 +2,9 @@ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; import '@formatjs/intl-relativetimeformat/polyfill'; -import { EventEmitter } from 'events'; import PortStream from 'extension-port-stream'; import extension from 'extensionizer'; -import Dnode from 'dnode'; import Eth from 'ethjs'; import EthQuery from 'eth-query'; import StreamProvider from 'web3-stream-provider'; @@ -19,6 +17,7 @@ import { import ExtensionPlatform from './platforms/extension'; import { setupMultiplex } from './lib/stream-utils'; import { getEnvironmentType } from './lib/util'; +import metaRPCClientFactory from './lib/metaRPCClientFactory'; start().catch(log.error); @@ -138,20 +137,6 @@ function setupWeb3Connection(connectionStream) { * @param {Function} cb - Called when the remote account manager connection is established */ function setupControllerConnection(connectionStream, cb) { - const eventEmitter = new EventEmitter(); - // the "weak: false" option is for nodejs only (eg unit tests) - // it is a workaround for node v12 support - const backgroundDnode = Dnode( - { - sendUpdate(state) { - eventEmitter.emit('update', state); - }, - }, - { weak: false }, - ); - connectionStream.pipe(backgroundDnode).pipe(connectionStream); - backgroundDnode.once('remote', function (backgroundConnection) { - backgroundConnection.on = eventEmitter.on.bind(eventEmitter); - cb(null, backgroundConnection); - }); + const backgroundRPC = metaRPCClientFactory(connectionStream); + cb(null, backgroundRPC); } diff --git a/development/auto-changelog.js b/development/auto-changelog.js new file mode 100755 index 000000000..042a934d9 --- /dev/null +++ b/development/auto-changelog.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +const fs = require('fs').promises; +const assert = require('assert').strict; +const path = require('path'); +const { escapeRegExp } = require('lodash'); +const { version } = require('../app/manifest/_base.json'); +const runCommand = require('./lib/runCommand'); + +const URL = 'https://github.com/MetaMask/metamask-extension'; + +async function main() { + await runCommand('git', ['fetch', '--tags']); + + const [mostRecentTagCommitHash] = await runCommand('git', [ + 'rev-list', + '--tags', + '--max-count=1', + ]); + const [mostRecentTag] = await runCommand('git', [ + 'describe', + '--tags', + mostRecentTagCommitHash, + ]); + assert.equal(mostRecentTag[0], 'v', 'Most recent tag should start with v'); + + const commitsSinceLastRelease = await runCommand('git', [ + 'rev-list', + `${mostRecentTag}..HEAD`, + ]); + + const commitEntries = []; + for (const commit of commitsSinceLastRelease) { + const [subject] = await runCommand('git', [ + 'show', + '-s', + '--format=%s', + commit, + ]); + + let prNumber; + let description = subject; + + // Squash & Merge: the commit subject is parsed as ` (#)` + if (subject.match(/\(#\d+\)/u)) { + const matchResults = subject.match(/\(#(\d+)\)/u); + prNumber = matchResults[1]; + description = subject.match(/^(.+)\s\(#\d+\)/u)[1]; + // Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request + // # from `, and the description is assumed to be the first line of the body. + // If no body is found, the description is set to the commit subject + } else if (subject.match(/#\d+\sfrom/u)) { + const matchResults = subject.match(/#(\d+)\sfrom/u); + prNumber = matchResults[1]; + const [firstLineOfBody] = await runCommand('git', [ + 'show', + '-s', + '--format=%b', + commit, + ]); + description = firstLineOfBody || subject; + } + // Otherwise: + // Normal commits: The commit subject is the description, and the PR ID is omitted. + + commitEntries.push({ prNumber, description }); + } + + const changelogFilename = path.resolve(__dirname, '..', 'CHANGELOG.md'); + const changelog = await fs.readFile(changelogFilename, { encoding: 'utf8' }); + const changelogLines = changelog.split('\n'); + + // remove the "v" prefix + const mostRecentVersion = mostRecentTag.slice(1); + + const isReleaseCandidate = mostRecentVersion !== version; + const versionHeader = `## [${version}]`; + const escapedVersionHeader = escapeRegExp(versionHeader); + const currentDevelopBranchHeader = '## [Unreleased]'; + const currentReleaseHeaderPattern = isReleaseCandidate + ? // This ensures this doesn't match on a version with a suffix + // e.g. v9.0.0 should not match on the header v9.0.0-beta.0 + `${escapedVersionHeader}$|${escapedVersionHeader}\\s` + : escapeRegExp(currentDevelopBranchHeader); + + let releaseHeaderIndex = changelogLines.findIndex((line) => + line.match(new RegExp(currentReleaseHeaderPattern, 'u')), + ); + if (releaseHeaderIndex === -1) { + if (!isReleaseCandidate) { + throw new Error( + `Failed to find release header '${currentDevelopBranchHeader}'`, + ); + } + + // Add release header if not found + const firstReleaseHeaderIndex = changelogLines.findIndex((line) => + line.match(/## \[\d+\.\d+\.\d+\]/u), + ); + const [, previousVersion] = changelogLines[firstReleaseHeaderIndex].match( + /## \[(\d+\.\d+\.\d+)\]/u, + ); + changelogLines.splice(firstReleaseHeaderIndex, 0, versionHeader, ''); + releaseHeaderIndex = firstReleaseHeaderIndex; + + // Update release link reference definitions + // A link reference definition is added for the new release, and the + // "Unreleased" header is updated to point at the range of commits merged + // after the most recent release. + const unreleasedLinkIndex = changelogLines.findIndex((line) => + line.match(/\[Unreleased\]:/u), + ); + changelogLines.splice( + unreleasedLinkIndex, + 1, + `[Unreleased]: ${URL}/compare/v${version}...HEAD`, + `[${version}]: ${URL}/compare/v${previousVersion}...v${version}`, + ); + } + + const prNumbersWithChangelogEntries = []; + for (const line of changelogLines) { + const matchResults = line.match(/- \[#(\d+)\]/u); + if (matchResults === null) { + continue; + } + const prNumber = matchResults[1]; + prNumbersWithChangelogEntries.push(prNumber); + } + + const changelogEntries = []; + for (const { prNumber, description } of commitEntries) { + if (prNumbersWithChangelogEntries.includes(prNumber)) { + continue; + } + + let changelogEntry; + if (prNumber) { + const prefix = `[#${prNumber}](${URL}/pull/${prNumber})`; + changelogEntry = `- ${prefix}: ${description}`; + } else { + changelogEntry = `- ${description}`; + } + changelogEntries.push(changelogEntry); + } + + changelogLines.splice(releaseHeaderIndex + 1, 0, ...changelogEntries); + const updatedChangelog = changelogLines.join('\n'); + await fs.writeFile(changelogFilename, updatedChangelog); + + console.log('CHANGELOG updated'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/development/auto-changelog.sh b/development/auto-changelog.sh deleted file mode 100755 index 26ab8e93f..000000000 --- a/development/auto-changelog.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -readonly URL='https://github.com/MetaMask/metamask-extension' - -git fetch --tags - -most_recent_tag="$(git describe --tags "$(git rev-list --tags --max-count=1)")" - -git rev-list "${most_recent_tag}"..HEAD | while read -r commit -do - subject="$(git show -s --format="%s" "$commit")" - - # Squash & Merge: the commit subject is parsed as ` (#)` - if grep -E -q '\(#[[:digit:]]+\)' <<< "$subject" - then - pr="$(awk '{print $NF}' <<< "$subject" | tr -d '()')" - prefix="[$pr]($URL/pull/${pr###}): " - description="$(awk '{NF--; print $0}' <<< "$subject")" - - # Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request - # # from `, and the description is assumed to be the first line of the body. - # If no body is found, the description is set to the commit subject - elif grep -E -q '#[[:digit:]]+\sfrom' <<< "$subject" - then - pr="$(awk '{print $4}' <<< "$subject")" - prefix="[$pr]($URL/pull/${pr###}): " - - first_line_of_body="$(git show -s --format="%b" "$commit" | head -n 1 | tr -d '\r')" - if [[ -z "$first_line_of_body" ]] - then - description="$subject" - else - description="$first_line_of_body" - fi - - # Normal commits: The commit subject is the description, and the PR ID is omitted. - else - pr='' - prefix='' - description="$subject" - fi - - # add entry to CHANGELOG - if [[ "$OSTYPE" == "linux-gnu" ]] - then - # shellcheck disable=SC1004 - sed -i'' '/## Current Develop Branch/a\ -- '"$prefix$description"''$'\n' CHANGELOG.md - else - # shellcheck disable=SC1004 - sed -i '' '/## Current Develop Branch/a\ -- '"$prefix$description"''$'\n' CHANGELOG.md - fi -done - -echo 'CHANGELOG updated' diff --git a/development/build/manifest.js b/development/build/manifest.js index 0dc8880b1..340db1272 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -8,10 +8,6 @@ const { createTask, composeSeries } = require('./task'); module.exports = createManifestTasks; -const scriptsToExcludeFromBackgroundDevBuild = { - 'bg-libs.js': true, -}; - function createManifestTasks({ browserPlatforms }) { // merge base manifest with per-platform manifests const prepPlatforms = async () => { @@ -35,29 +31,13 @@ function createManifestTasks({ browserPlatforms }) { ); }; - // dev: remove bg-libs, add chromereload, add perms + // dev: add perms const envDev = createTaskForModifyManifestForEnvironment((manifest) => { - const scripts = manifest.background.scripts.filter( - (scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName], - ); - scripts.push('chromereload.js'); - manifest.background = { - ...manifest.background, - scripts, - }; manifest.permissions = [...manifest.permissions, 'webRequestBlocking']; }); - // testDev: remove bg-libs, add perms + // testDev: add perms const envTestDev = createTaskForModifyManifestForEnvironment((manifest) => { - const scripts = manifest.background.scripts.filter( - (scriptName) => !scriptsToExcludeFromBackgroundDevBuild[scriptName], - ); - scripts.push('chromereload.js'); - manifest.background = { - ...manifest.background, - scripts, - }; manifest.permissions = [ ...manifest.permissions, 'webRequestBlocking', diff --git a/development/build/scripts.js b/development/build/scripts.js index f3dcc8de0..2bc5fe5c3 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -1,11 +1,9 @@ +const EventEmitter = require('events'); const gulp = require('gulp'); const watch = require('gulp-watch'); -const pify = require('pify'); -const pump = pify(require('pump')); const source = require('vinyl-source-stream'); const buffer = require('vinyl-buffer'); const log = require('fancy-log'); -const { assign } = require('lodash'); const watchify = require('watchify'); const browserify = require('browserify'); const envify = require('loose-envify/custom'); @@ -13,8 +11,11 @@ const sourcemaps = require('gulp-sourcemaps'); const terser = require('gulp-terser-js'); const babelify = require('babelify'); const brfs = require('brfs'); +const pify = require('pify'); +const endOfStream = pify(require('end-of-stream')); +const labeledStreamSplicer = require('labeled-stream-splicer').obj; -const conf = require('rc')('metamask', { +const metamaskrc = require('rc')('metamask', { INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID, SEGMENT_HOST: process.env.SEGMENT_HOST, SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY, @@ -67,10 +68,10 @@ function createScriptTasks({ browserPlatforms, livereload }) { }; const deps = { background: createTasksForBuildJsDeps({ - filename: 'bg-libs', + label: 'bg-libs', key: 'background', }), - ui: createTasksForBuildJsDeps({ filename: 'ui-libs', key: 'ui' }), + ui: createTasksForBuildJsDeps({ label: 'ui-libs', key: 'ui' }), }; // high level tasks @@ -83,15 +84,15 @@ function createScriptTasks({ browserPlatforms, livereload }) { return { prod, dev, testDev, test }; - function createTasksForBuildJsDeps({ key, filename }) { + function createTasksForBuildJsDeps({ key, label }) { return createTask( `scripts:deps:${key}`, - bundleTask({ - label: filename, - filename: `${filename}.js`, - buildLib: true, - dependenciesToBundle: externalDependenciesMap[key], + createNormalBundle({ + label, + destFilepath: `${label}.js`, + modulesToExpose: externalDependenciesMap[key], devMode: false, + browserPlatforms, }), ); } @@ -104,13 +105,18 @@ function createScriptTasks({ browserPlatforms, livereload }) { 'initSentry', ]; - const standardSubtasks = standardBundles.map((filename) => { + const standardSubtasks = standardBundles.map((label) => { + let extraEntries; + if (devMode && label === 'ui') { + extraEntries = ['./development/require-react-devtools.js']; + } return createTask( - `${taskPrefix}:${filename}`, + `${taskPrefix}:${label}`, createBundleTaskForBuildJsExtensionNormal({ - filename, + label, devMode, testing, + extraEntries, }), ); }); @@ -128,7 +134,7 @@ function createScriptTasks({ browserPlatforms, livereload }) { createTaskForBuildJsExtensionDisableConsole({ devMode }), ); - // task for initiating livereload + // task for initiating browser livereload const initiateLiveReload = async () => { if (devMode) { // trigger live reload when the bundles are updated @@ -156,29 +162,33 @@ function createScriptTasks({ browserPlatforms, livereload }) { } function createBundleTaskForBuildJsExtensionNormal({ - filename, + label, devMode, testing, + extraEntries, }) { - return bundleTask({ - label: filename, - filename: `${filename}.js`, - filepath: `./app/scripts/${filename}.js`, + return createNormalBundle({ + label, + entryFilepath: `./app/scripts/${label}.js`, + destFilepath: `${label}.js`, + extraEntries, externalDependencies: devMode ? undefined - : externalDependenciesMap[filename], + : externalDependenciesMap[label], devMode, testing, + browserPlatforms, }); } function createTaskForBuildJsExtensionDisableConsole({ devMode }) { - const filename = 'disable-console'; - return bundleTask({ - label: filename, - filename: `${filename}.js`, - filepath: `./app/scripts/${filename}.js`, + const label = 'disable-console'; + return createNormalBundle({ + label, + entryFilepath: `./app/scripts/${label}.js`, + destFilepath: `${label}.js`, devMode, + browserPlatforms, }); } @@ -186,188 +196,266 @@ function createScriptTasks({ browserPlatforms, livereload }) { const inpage = 'inpage'; const contentscript = 'contentscript'; return composeSeries( - bundleTask({ + createNormalBundle({ label: inpage, - filename: `${inpage}.js`, - filepath: `./app/scripts/${inpage}.js`, + entryFilepath: `./app/scripts/${inpage}.js`, + destFilepath: `${inpage}.js`, externalDependencies: devMode ? undefined : externalDependenciesMap[inpage], devMode, testing, + browserPlatforms, }), - bundleTask({ + createNormalBundle({ label: contentscript, - filename: `${contentscript}.js`, - filepath: `./app/scripts/${contentscript}.js`, + entryFilepath: `./app/scripts/${contentscript}.js`, + destFilepath: `${contentscript}.js`, externalDependencies: devMode ? undefined : externalDependenciesMap[contentscript], devMode, testing, + browserPlatforms, }), ); } +} - function bundleTask(opts) { - let bundler; - - return performBundle; +function createNormalBundle({ + destFilepath, + entryFilepath, + extraEntries = [], + modulesToExpose, + externalDependencies, + devMode, + testing, + browserPlatforms, +}) { + return async function () { + // create bundler setup and apply defaults + const buildConfiguration = createBuildConfiguration(); + const { bundlerOpts, events } = buildConfiguration; + + const envVars = getEnvironmentVariables({ devMode, testing }); + setupBundlerDefaults(buildConfiguration, { + devMode, + envVars, + }); - async function performBundle() { - // initialize bundler if not available yet - // dont create bundler until task is actually run - if (!bundler) { - bundler = generateBundler(opts, performBundle); - // output build logs to terminal - bundler.on('log', log); - } + // set bundle entries + bundlerOpts.entries = [...extraEntries]; + if (entryFilepath) { + bundlerOpts.entries.push(entryFilepath); + } - const buildPipeline = [ - bundler.bundle(), - // convert bundle stream to gulp vinyl stream - source(opts.filename), - // Initialize Source Maps - buffer(), - // loads map from browserify file - sourcemaps.init({ loadMaps: true }), - ]; - - // Minification - if (!opts.devMode) { - buildPipeline.push( - terser({ - mangle: { - reserved: ['MetamaskInpageProvider'], - }, - sourceMap: { - content: true, - }, - }), - ); - } + if (modulesToExpose) { + bundlerOpts.require = bundlerOpts.require.concat(modulesToExpose); + } - // Finalize Source Maps - if (opts.devMode) { - // Use inline source maps for development due to Chrome DevTools bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=931675 - // note: sourcemaps call arity is important - buildPipeline.push(sourcemaps.write()); - } else { - buildPipeline.push(sourcemaps.write('../sourcemaps')); - } + if (externalDependencies) { + // there doesnt seem to be a standard bify option for this + // so we'll put it here but manually call it after bundle + bundlerOpts.manualExternal = bundlerOpts.manualExternal.concat( + externalDependencies, + ); + } - // write completed bundles + // instrument pipeline + events.on('configurePipeline', ({ pipeline }) => { + // convert bundle stream to gulp vinyl stream + // and ensure file contents are buffered + pipeline.get('vinyl').push(source(destFilepath)); + pipeline.get('vinyl').push(buffer()); + // setup bundle destination browserPlatforms.forEach((platform) => { - const dest = `./dist/${platform}`; - buildPipeline.push(gulp.dest(dest)); + const dest = `./dist/${platform}/`; + pipeline.get('dest').push(gulp.dest(dest)); }); + }); - // process bundles - if (opts.devMode) { - try { - await pump(buildPipeline); - } catch (err) { - gracefulError(err); - } - } else { - await pump(buildPipeline); - } - } - } + await bundleIt(buildConfiguration); + }; +} - function generateBundler(opts, performBundle) { - const browserifyOpts = assign({}, watchify.args, { - plugin: [], - transform: [], - debug: true, - fullPaths: opts.devMode, - }); +function createBuildConfiguration() { + const events = new EventEmitter(); + const bundlerOpts = { + entries: [], + transform: [], + plugin: [], + require: [], + // not a standard bify option + manualExternal: [], + }; + return { bundlerOpts, events }; +} - if (!opts.buildLib) { - if (opts.devMode && opts.filename === 'ui.js') { - browserifyOpts.entries = [ - './development/require-react-devtools.js', - opts.filepath, - ]; - } else { - browserifyOpts.entries = [opts.filepath]; - } - } +function setupBundlerDefaults(buildConfiguration, { devMode, envVars }) { + const { bundlerOpts } = buildConfiguration; + // devMode options + const reloadOnChange = Boolean(devMode); + const minify = Boolean(devMode) === false; + + Object.assign(bundlerOpts, { + // source transforms + transform: [ + // transpile top-level code + babelify, + // inline `fs.readFileSync` files + brfs, + ], + // use entryFilepath for moduleIds, easier to determine origin file + fullPaths: devMode, + // for sourcemaps + debug: true, + }); + + // inject environment variables via node-style `process.env` + if (envVars) { + bundlerOpts.transform.push([envify(envVars), { global: true }]); + } - let bundler = browserify(browserifyOpts) - .transform(babelify) - .transform(brfs); + // setup reload on change + if (reloadOnChange) { + setupReloadOnChange(buildConfiguration); + } - if (opts.buildLib) { - bundler = bundler.require(opts.dependenciesToBundle); - } + if (minify) { + setupMinification(buildConfiguration); + } - if (opts.externalDependencies) { - bundler = bundler.external(opts.externalDependencies); - } + // setup source maps + setupSourcemaps(buildConfiguration, { devMode }); +} - const environment = getEnvironment({ - devMode: opts.devMode, - test: opts.testing, +function setupReloadOnChange({ bundlerOpts, events }) { + // add plugin to options + Object.assign(bundlerOpts, { + plugin: [...bundlerOpts.plugin, watchify], + // required by watchify + cache: {}, + packageCache: {}, + }); + // instrument pipeline + events.on('configurePipeline', ({ bundleStream }) => { + // handle build error to avoid breaking build process + // (eg on syntax error) + bundleStream.on('error', (err) => { + gracefulError(err); }); - if (environment === 'production' && !process.env.SENTRY_DSN) { - throw new Error('Missing SENTRY_DSN environment variable'); - } + }); +} - // Inject variables into bundle - bundler.transform( - envify({ - METAMASK_DEBUG: opts.devMode, - METAMASK_ENVIRONMENT: environment, - METAMASK_VERSION: baseManifest.version, - NODE_ENV: opts.devMode ? 'development' : 'production', - IN_TEST: opts.testing ? 'true' : false, - PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', - PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', - CONF: opts.devMode ? conf : {}, - SENTRY_DSN: process.env.SENTRY_DSN, - INFURA_PROJECT_ID: opts.testing - ? '00000000000000000000000000000000' - : conf.INFURA_PROJECT_ID, - SEGMENT_HOST: conf.SEGMENT_HOST, - // When we're in the 'production' environment we will use a specific key only set in CI - // Otherwise we'll use the key from .metamaskrc or from the environment variable. If - // the value of SEGMENT_WRITE_KEY that we envify is undefined then no events will be tracked - // in the build. This is intentional so that developers can contribute to MetaMask without - // inflating event volume. - SEGMENT_WRITE_KEY: - environment === 'production' - ? process.env.SEGMENT_PROD_WRITE_KEY - : conf.SEGMENT_WRITE_KEY, - SEGMENT_LEGACY_WRITE_KEY: - environment === 'production' - ? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY - : conf.SEGMENT_LEGACY_WRITE_KEY, +function setupMinification(buildConfiguration) { + const { events } = buildConfiguration; + events.on('configurePipeline', ({ pipeline }) => { + pipeline.get('minify').push( + terser({ + mangle: { + reserved: ['MetamaskInpageProvider'], + }, + sourceMap: { + content: true, + }, }), - { - global: true, - }, ); + }); +} - // Live reload - minimal rebundle on change - if (opts.devMode) { - bundler = watchify(bundler); - // on any file update, re-runs the bundler - bundler.on('update', () => { - performBundle(); - }); - } +function setupSourcemaps(buildConfiguration, { devMode }) { + const { events } = buildConfiguration; + events.on('configurePipeline', ({ pipeline }) => { + pipeline.get('sourcemaps:init').push(sourcemaps.init({ loadMaps: true })); + pipeline + .get('sourcemaps:write') + // Use inline source maps for development due to Chrome DevTools bug + // https://bugs.chromium.org/p/chromium/issues/detail?id=931675 + .push( + devMode + ? sourcemaps.write() + : sourcemaps.write('../sourcemaps', { addComment: false }), + ); + }); +} - return bundler; +async function bundleIt(buildConfiguration) { + const { bundlerOpts, events } = buildConfiguration; + const bundler = browserify(bundlerOpts); + // manually apply non-standard option + bundler.external(bundlerOpts.manualExternal); + // output build logs to terminal + bundler.on('log', log); + // forward update event (used by watchify) + bundler.on('update', () => performBundle()); + await performBundle(); + + async function performBundle() { + // this pipeline is created for every bundle + // the labels are all the steps you can hook into + const pipeline = labeledStreamSplicer([ + 'vinyl', + [], + 'sourcemaps:init', + [], + 'minify', + [], + 'sourcemaps:write', + [], + 'dest', + [], + ]); + const bundleStream = bundler.bundle(); + // trigger build pipeline instrumentations + events.emit('configurePipeline', { pipeline, bundleStream }); + // start bundle, send into pipeline + bundleStream.pipe(pipeline); + // nothing will consume pipeline, so let it flow + pipeline.resume(); + await endOfStream(pipeline); } } -function getEnvironment({ devMode, test }) { +function getEnvironmentVariables({ devMode, testing }) { + const environment = getEnvironment({ devMode, testing }); + if (environment === 'production' && !process.env.SENTRY_DSN) { + throw new Error('Missing SENTRY_DSN environment variable'); + } + return { + METAMASK_DEBUG: devMode, + METAMASK_ENVIRONMENT: environment, + METAMASK_VERSION: baseManifest.version, + NODE_ENV: devMode ? 'development' : 'production', + IN_TEST: testing ? 'true' : false, + PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', + PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', + CONF: devMode ? metamaskrc : {}, + SENTRY_DSN: process.env.SENTRY_DSN, + INFURA_PROJECT_ID: testing + ? '00000000000000000000000000000000' + : metamaskrc.INFURA_PROJECT_ID, + SEGMENT_HOST: metamaskrc.SEGMENT_HOST, + // When we're in the 'production' environment we will use a specific key only set in CI + // Otherwise we'll use the key from .metamaskrc or from the environment variable. If + // the value of SEGMENT_WRITE_KEY that we envify is undefined then no events will be tracked + // in the build. This is intentional so that developers can contribute to MetaMask without + // inflating event volume. + SEGMENT_WRITE_KEY: + environment === 'production' + ? process.env.SEGMENT_PROD_WRITE_KEY + : metamaskrc.SEGMENT_WRITE_KEY, + SEGMENT_LEGACY_WRITE_KEY: + environment === 'production' + ? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY + : metamaskrc.SEGMENT_LEGACY_WRITE_KEY, + }; +} + +function getEnvironment({ devMode, testing }) { // get environment slug if (devMode) { return 'development'; - } else if (test) { + } else if (testing) { return 'testing'; } else if (process.env.CIRCLE_BRANCH === 'master') { return 'production'; diff --git a/development/build/static.js b/development/build/static.js index d041d87ae..0adba6745 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -80,13 +80,31 @@ const copyTargetsDev = [ pattern: '/chromereload.js', dest: ``, }, + // empty files to suppress missing file errors + { + src: './development/empty.js', + dest: `bg-libs.js`, + }, + { + src: './development/empty.js', + dest: `ui-libs.js`, + }, +]; + +const copyTargetsProd = [ + ...copyTargets, + // empty files to suppress missing file errors + { + src: './development/empty.js', + dest: `chromereload.js`, + }, ]; function createStaticAssetTasks({ livereload, browserPlatforms }) { const prod = createTask( 'static:prod', composeSeries( - ...copyTargets.map((target) => { + ...copyTargetsProd.map((target) => { return async function copyStaticAssets() { await performCopy(target); }; diff --git a/development/build/styles.js b/development/build/styles.js index 52490f6fd..108561261 100644 --- a/development/build/styles.js +++ b/development/build/styles.js @@ -68,7 +68,7 @@ function createStyleTasks({ livereload }) { async function buildScssPipeline(src, dest, devMode, rtl) { if (!sass) { // eslint-disable-next-line node/global-require - sass = require('gulp-sass'); + sass = require('gulp-dart-sass'); // use our own compiler which runs sass in its own process // in order to not pollute the intrinsics // eslint-disable-next-line node/global-require diff --git a/development/build/task.js b/development/build/task.js index 2651b470a..bc6f92005 100644 --- a/development/build/task.js +++ b/development/build/task.js @@ -83,7 +83,7 @@ function runInChildProcess(task) { ); // await end of process await new Promise((resolve, reject) => { - childProcess.once('close', (errCode) => { + childProcess.once('exit', (errCode) => { if (errCode !== 0) { reject( new Error( diff --git a/app/scripts/chromereload.js b/development/chromereload.js similarity index 100% rename from app/scripts/chromereload.js rename to development/chromereload.js diff --git a/development/empty.js b/development/empty.js new file mode 100644 index 000000000..eb710729b --- /dev/null +++ b/development/empty.js @@ -0,0 +1 @@ +// this file intentionally left blank : ) diff --git a/development/generate-migration.sh b/development/generate-migration.sh index 5fb85feb5..0e08b9421 100755 --- a/development/generate-migration.sh +++ b/development/generate-migration.sh @@ -8,8 +8,8 @@ g-migration() { touch app/scripts/migrations/"$vnum".js cp app/scripts/migrations/template.js app/scripts/migrations/"$vnum".js - touch test/unit/migrations/"$vnum".js - cp test/unit/migrations/template-test.js test/unit/migrations/"$vnum"-test.js + touch app/scripts/migrations/"$vnum".test.js + cp app/scripts/migrations/template.test.js app/scripts/migrations/"$vnum".test.js } g-migration "$1" diff --git a/development/lib/runCommand.js b/development/lib/runCommand.js new file mode 100644 index 000000000..2d92ffe99 --- /dev/null +++ b/development/lib/runCommand.js @@ -0,0 +1,79 @@ +const spawn = require('cross-spawn'); + +/** + * Run a command to completion using the system shell. + * + * This will run a command with the specified arguments, and resolve when the + * process has exited. The STDOUT stream is monitored for output, which is + * returned after being split into lines. All output is expected to be UTF-8 + * encoded, and empty lines are removed from the output. + * + * Anything received on STDERR is assumed to indicate a problem, and is tracked + * as an error. + * + * @param {string} command - The command to run + * @param {Array} [args] - The arguments to pass to the command + * @returns {Array} Lines of output received via STDOUT + */ +async function runCommand(command, args) { + const output = []; + let mostRecentError; + let errorSignal; + let errorCode; + const internalError = new Error('Internal'); + try { + await new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { encoding: 'utf8' }); + childProcess.stdout.setEncoding('utf8'); + childProcess.stderr.setEncoding('utf8'); + + childProcess.on('error', (error) => { + mostRecentError = error; + }); + + childProcess.stdout.on('data', (message) => { + const nonEmptyLines = message.split('\n').filter((line) => line !== ''); + output.push(...nonEmptyLines); + }); + + childProcess.stderr.on('data', (message) => { + mostRecentError = new Error(message.trim()); + }); + + childProcess.once('exit', (code, signal) => { + if (code === 0) { + return resolve(); + } + errorCode = code; + errorSignal = signal; + return reject(internalError); + }); + }); + } catch (error) { + /** + * The error is re-thrown here in an `async` context to preserve the stack trace. If this was + * was thrown inside the Promise constructor, the stack trace would show a few frames of + * Node.js internals then end, without indicating where `runCommand` was called. + */ + if (error === internalError) { + let errorMessage; + if (errorCode !== null && errorSignal !== null) { + errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`; + } else if (errorSignal !== null) { + errorMessage = `Terminaled by signal '${errorSignal}'`; + } else if (errorCode === null) { + errorMessage = 'Exited with no code or signal'; + } else { + errorMessage = `Exited with code '${errorCode}'`; + } + const improvedError = new Error(errorMessage); + if (mostRecentError) { + improvedError.cause = mostRecentError; + } + throw improvedError; + } + } + return output; +} + +module.exports = runCommand; diff --git a/development/source-map-explorer.sh b/development/source-map-explorer.sh index a4130d0f0..ae9563f9f 100755 --- a/development/source-map-explorer.sh +++ b/development/source-map-explorer.sh @@ -5,11 +5,30 @@ set -e set -u set -o pipefail -mkdir -p build-artifacts/source-map-explorer -yarn source-map-explorer dist/chrome/inpage.js --html build-artifacts/source-map-explorer/inpage.html -yarn source-map-explorer dist/chrome/contentscript.js --html build-artifacts/source-map-explorer/contentscript.html -yarn source-map-explorer dist/chrome/background.js --html build-artifacts/source-map-explorer/background.html -yarn source-map-explorer dist/chrome/bg-libs.js --html build-artifacts/source-map-explorer/bg-libs.html -yarn source-map-explorer dist/chrome/ui.js --html build-artifacts/source-map-explorer/ui.html -yarn source-map-explorer dist/chrome/ui-libs.js --html build-artifacts/source-map-explorer/ui-libs.html -yarn source-map-explorer dist/chrome/phishing-detect.js --html build-artifacts/source-map-explorer/phishing-detect.html +function generate_sourcemap() { + local temp_dir="${1}"; shift + local module_name="${1}"; shift + + cp "dist/chrome/${module_name}.js" "${temp_dir}/" + cp "dist/sourcemaps/${module_name}.js.map" "${temp_dir}/" + printf '//# sourceMappingURL=%s.js.map' "${module_name}" >> "${temp_dir}/${module_name}.js" + yarn source-map-explorer "${temp_dir}/${module_name}.js" "${temp_dir}/${module_name}.js.map" --html "build-artifacts/source-map-explorer/${module_name}.html" +} + +function main() { + mkdir -p build-artifacts/source-map-explorer + + local temp_dir + temp_dir="$(mktemp -d)" + + for file in dist/sourcemaps/*.js.map; do + [[ -e $file ]] || (echo 'Failed to find any JavaScript modules' && exit 1) + local filename + filename="$(basename "${file}")" + local module_name + module_name="${filename%.js.map}" + generate_sourcemap "${temp_dir}" "${module_name}" + done +} + +main diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..d0e8e66fc --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["node:console"] +} diff --git a/lavamoat/node/policy.json b/lavamoat/node/policy.json index 123dee86c..e744f1175 100644 --- a/lavamoat/node/policy.json +++ b/lavamoat/node/policy.json @@ -959,6 +959,16 @@ "buffer-equal": true } }, + "are-we-there-yet": { + "builtin": { + "events.EventEmitter": true, + "util.inherits": true + }, + "packages": { + "delegates": true, + "readable-stream": true + } + }, "arr-diff": { "packages": { "arr-flatten": true, @@ -1311,6 +1321,7 @@ "anymatch": true, "async-each": true, "braces": true, + "fsevents": true, "glob-parent": true, "inherits": true, "is-binary-path": true, @@ -1562,6 +1573,16 @@ "through2": true } }, + "detect-libc": { + "builtin": { + "child_process.spawnSync": true, + "fs.readdirSync": true, + "os.platform": true + }, + "globals": { + "process.env": true + } + }, "detective": { "packages": { "acorn-node": true, @@ -2005,6 +2026,45 @@ "process.version": true } }, + "fsevents": { + "builtin": { + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true + }, + "native": true, + "packages": { + "node-pre-gyp": true + } + }, + "gauge": { + "builtin": { + "util.format": true + }, + "globals": { + "clearInterval": true, + "process": true, + "setImmediate": true, + "setInterval": true + }, + "packages": { + "aproba": true, + "console-control-strings": true, + "has-unicode": true, + "object-assign": true, + "signal-exit": true, + "string-width": true, + "strip-ansi": true, + "wide-align": true + } + }, "get-assigned-identifiers": { "builtin": { "assert.equal": true @@ -2026,8 +2086,6 @@ "fs.lstatSync": true, "fs.readdir": true, "fs.readdirSync": true, - "fs.realpath": true, - "fs.realpathSync": true, "fs.stat": true, "fs.statSync": true, "path.join": true, @@ -2214,6 +2272,29 @@ "vinyl-sourcemaps-apply": true } }, + "gulp-dart-sass": { + "builtin": { + "path.basename": true, + "path.dirname": true, + "path.extname": true, + "path.join": true, + "path.relative": true + }, + "globals": { + "process.cwd": true, + "process.stderr.write": true + }, + "packages": { + "chalk": true, + "lodash.clonedeep": true, + "plugin-error": true, + "replace-ext": true, + "sass": true, + "strip-ansi": true, + "through2": true, + "vinyl-sourcemaps-apply": true + } + }, "gulp-livereload": { "builtin": { "path.relative": true @@ -2247,29 +2328,6 @@ "vinyl-sourcemaps-apply": true } }, - "gulp-sass": { - "builtin": { - "path.basename": true, - "path.dirname": true, - "path.extname": true, - "path.join": true, - "path.relative": true - }, - "globals": { - "process.cwd": true, - "process.stderr.write": true - }, - "packages": { - "chalk": true, - "lodash": true, - "node-sass": true, - "plugin-error": true, - "replace-ext": true, - "strip-ansi": true, - "through2": true, - "vinyl-sourcemaps-apply": true - } - }, "gulp-sourcemaps": { "builtin": { "path.dirname": true, @@ -2387,6 +2445,16 @@ "process.argv": true } }, + "has-unicode": { + "builtin": { + "os.type": true + }, + "globals": { + "process.env.LANG": true, + "process.env.LC_ALL": true, + "process.env.LC_CTYPE": true + } + }, "has-value": { "packages": { "get-value": true, @@ -2540,6 +2608,11 @@ "is-plain-object": true } }, + "is-fullwidth-code-point": { + "packages": { + "number-is-nan": true + } + }, "is-glob": { "packages": { "is-extglob": true @@ -2924,43 +2997,54 @@ "setTimeout": true } }, - "node-sass": { + "node-pre-gyp": { "builtin": { + "events.EventEmitter": true, "fs.existsSync": true, "fs.readFileSync": true, - "fs.readdirSync": true, - "os.EOL": true, - "path.delimiter": true, + "fs.renameSync": true, + "path.dirname": true, + "path.existsSync": true, "path.join": true, - "path.resolve": true + "path.resolve": true, + "url.parse": true, + "url.resolve": true, + "util.inherits": true }, "globals": { "__dirname": true, "console.log": true, "process.arch": true, - "process.argv.slice": true, "process.cwd": true, - "process.env.SASS_BINARY_DIR": true, - "process.env.SASS_BINARY_NAME": true, - "process.env.SASS_BINARY_PATH": true, - "process.env.SASS_BINARY_SITE": true, - "process.env.SASS_PATH.split": true, - "process.env.hasOwnProperty": true, - "process.env.npm_config_cache": true, - "process.env.npm_config_sass_binary_cache": true, - "process.env.npm_config_sass_binary_dir": true, - "process.env.npm_config_sass_binary_name": true, - "process.env.npm_config_sass_binary_path": true, - "process.env.npm_config_sass_binary_site": true, - "process.execPath": true, + "process.env": true, "process.platform": true, - "process.sass": "write", - "process.versions.modules": true + "process.version.substr": true, + "process.versions": true }, "packages": { - "lodash": true, - "mkdirp": true, - "true-case-path": true + "detect-libc": true, + "nopt": true, + "npmlog": true, + "rimraf": true, + "semver": true + } + }, + "nopt": { + "builtin": { + "path": true, + "stream.Stream": true, + "url": true + }, + "globals": { + "console": true, + "process.argv": true, + "process.env.DEBUG_NOPT": true, + "process.env.NOPT_DEBUG": true, + "process.platform": true + }, + "packages": { + "abbrev": true, + "osenv": true } }, "normalize-path": { @@ -2978,6 +3062,22 @@ "once": true } }, + "npmlog": { + "builtin": { + "events.EventEmitter": true, + "util": true + }, + "globals": { + "process.nextTick": true, + "process.stderr": true + }, + "packages": { + "are-we-there-yet": true, + "console-control-strings": true, + "gauge": true, + "set-blocking": true + } + }, "object-copy": { "packages": { "copy-descriptor": true, @@ -3045,6 +3145,54 @@ "readable-stream": true } }, + "os-homedir": { + "builtin": { + "os.homedir": true + }, + "globals": { + "process.env": true, + "process.getuid": true, + "process.platform": true + } + }, + "os-tmpdir": { + "globals": { + "process.env.SystemRoot": true, + "process.env.TEMP": true, + "process.env.TMP": true, + "process.env.TMPDIR": true, + "process.env.windir": true, + "process.platform": true + } + }, + "osenv": { + "builtin": { + "child_process.exec": true, + "path": true + }, + "globals": { + "process.env.COMPUTERNAME": true, + "process.env.ComSpec": true, + "process.env.EDITOR": true, + "process.env.HOSTNAME": true, + "process.env.PATH": true, + "process.env.PROMPT": true, + "process.env.PS1": true, + "process.env.Path": true, + "process.env.SHELL": true, + "process.env.USER": true, + "process.env.USERDOMAIN": true, + "process.env.USERNAME": true, + "process.env.VISUAL": true, + "process.env.path": true, + "process.nextTick": true, + "process.platform": true + }, + "packages": { + "os-homedir": true, + "os-tmpdir": true + } + }, "parent-module": { "packages": { "callsites": true @@ -3579,6 +3727,7 @@ "readline": true }, "globals": { + "Buffer": true, "HTMLElement": true, "InternalError": true, "TextDecoder": true, @@ -3597,6 +3746,7 @@ "navigator": true, "print": true, "process": true, + "setImmediate": true, "setTimeout": true, "version": true }, @@ -3625,6 +3775,12 @@ "process": true } }, + "set-blocking": { + "globals": { + "process.stderr": true, + "process.stdout": true + } + }, "set-value": { "packages": { "extend-shallow": true, @@ -3818,6 +3974,7 @@ }, "string-width": { "packages": { + "code-point-at": true, "emoji-regex": true, "is-fullwidth-code-point": true, "strip-ansi": true @@ -4066,18 +4223,6 @@ "through2": true } }, - "true-case-path": { - "builtin": { - "path.normalize": true, - "path.parse": true - }, - "globals": { - "process.platform": true - }, - "packages": { - "glob": true - } - }, "typedarray-to-buffer": { "globals": { "Buffer.from": true @@ -4386,6 +4531,11 @@ "isexe": true } }, + "wide-align": { + "packages": { + "string-width": true + } + }, "write": { "builtin": { "fs.createWriteStream": true, diff --git a/package.json b/package.json index 50d265a9e..7d0928820 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010", "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", "sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080", - "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.test.js\" \"ui/app/**/*.test.js\" \"shared/**/*.test.js\"", + "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive './{ui,app,shared}/**/*.test.js'", "test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js", - "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/{,**/!(permissions)}/*.test.js\" \"ui/app/**/*.test.js\" \"shared/**/*.test.js\"", - "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/permissions/*.test.js\"", + "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --recursive './{ui,app,shared}/**/*.test.js'", + "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'", "test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive", "test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh", "test:e2e:chrome:metrics": "SELENIUM_BROWSER=chrome mocha test/e2e/metrics.spec.js", @@ -34,8 +34,8 @@ "test:coverage:path": "nyc --check-coverage yarn test:unit:path", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", - "lint": "prettier --check ./**/*.json && eslint . --ext js && yarn lint:styles", - "lint:fix": "prettier --write ./**/*.json && eslint . --ext js --fix", + "lint": "prettier --check ./**/*.json && eslint . --ext js --cache && yarn lint:styles", + "lint:fix": "prettier --write ./**/*.json && eslint . --ext js --cache --fix", "lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint", "lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix", "lint:shellcheck": "./development/shellcheck.sh", @@ -53,12 +53,13 @@ "storybook": "start-storybook -p 6006 -c .storybook --static-dir ./app ./storybook/images", "storybook:build": "build-storybook -c .storybook -o storybook-build --static-dir ./app ./storybook/images", "storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master", - "update-changelog": "./development/auto-changelog.sh", + "update-changelog": "node ./development/auto-changelog.js", "generate:migration": "./development/generate-migration.sh", "lavamoat:auto": "lavamoat ./development/build/index.js --writeAutoPolicy", "lavamoat:debug": "lavamoat ./development/build/index.js --writeAutoPolicyDebug" }, "resolutions": { + "**/regenerator-runtime": "^0.13.7", "**/configstore/dot-prop": "^5.1.1", "**/ethers/elliptic": "^6.5.4", "**/knex/minimist": "^1.2.5", @@ -84,7 +85,7 @@ "@lavamoat/preinstall-always-fail": "^1.0.0", "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.22.0", - "@metamask/controllers": "^5.1.0", + "@metamask/controllers": "^6.2.1", "@metamask/eth-ledger-bridge-keyring": "^0.3.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.0.0", @@ -108,9 +109,8 @@ "currency-formatter": "^1.4.2", "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", - "dnode": "^1.2.2", "end-of-stream": "^1.4.4", - "eth-block-tracker": "^4.4.2", + "eth-block-tracker": "^5.0.1", "eth-ens-namehash": "^2.0.8", "eth-json-rpc-filters": "^4.2.1", "eth-json-rpc-infura": "^5.1.0", @@ -135,6 +135,7 @@ "extension-port-stream": "^2.0.0", "extensionizer": "^1.0.1", "fast-json-patch": "^2.0.4", + "fast-safe-stringify": "^2.0.7", "fuse.js": "^3.2.0", "globalthis": "^1.0.1", "human-standard-token-abi": "^2.0.0", @@ -142,6 +143,7 @@ "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^2.1.1", "jsonschema": "^1.2.4", + "labeled-stream-splicer": "^2.0.2", "localforage": "^1.9.0", "lodash": "^4.17.19", "loglevel": "^1.4.1", @@ -176,7 +178,7 @@ "reselect": "^3.0.1", "rpc-cap": "^3.2.1", "safe-event-emitter": "^1.0.1", - "safe-json-stringify": "^1.2.0", + "ses": "^0.12.4", "single-call-balance-checker-abi": "^1.0.0", "swappable-obj-proxy": "^1.1.0", "textarea-caret": "^3.0.1", @@ -187,7 +189,7 @@ }, "devDependencies": { "@babel/core": "^7.12.1", - "@babel/eslint-parser": "^7.12.1", + "@babel/eslint-parser": "^7.13.14", "@babel/eslint-plugin": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", @@ -225,13 +227,14 @@ "del": "^3.0.0", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", - "eslint": "^7.7.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-mocha": "^8.0.0", + "eslint": "^7.23.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-mocha": "^8.1.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "~7.20.0", - "eslint-plugin-react-hooks": "^4.0.4", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0", "fancy-log": "^1.3.3", "fast-glob": "^3.2.2", "fs-extra": "^8.1.0", @@ -241,10 +244,10 @@ "get-port": "^5.1.0", "gulp": "^4.0.2", "gulp-autoprefixer": "^5.0.0", + "gulp-dart-sass": "^1.0.2", "gulp-livereload": "4.0.0", "gulp-rename": "^2.0.0", "gulp-rtlcss": "^1.4.0", - "gulp-sass": "^4.1.0", "gulp-sourcemaps": "^2.6.0", "gulp-stylelint": "^13.0.0", "gulp-terser-js": "^5.2.2", @@ -260,9 +263,9 @@ "nock": "^9.0.14", "node-fetch": "^2.6.1", "nyc": "^15.0.0", - "patch-package": "^6.2.2", + "patch-package": "^6.4.7", "polyfill-crypto.getrandomvalues": "^1.0.0", - "prettier": "^2.1.1", + "prettier": "^2.2.1", "prettier-plugin-sort-json": "^0.0.1", "proxyquire": "^2.1.3", "randomcolor": "^0.5.4", @@ -271,13 +274,11 @@ "read-installed": "^4.0.3", "redux-mock-store": "^1.5.4", "remote-redux-devtools": "^0.5.16", - "remotedev-server": "^0.3.1", "resolve-url-loader": "^3.1.2", "sass": "^1.32.4", "sass-loader": "^10.1.1", "selenium-webdriver": "4.0.0-alpha.7", "serve-handler": "^6.1.2", - "ses": "0.11.0", "sinon": "^9.0.0", "source-map": "^0.7.2", "source-map-explorer": "^2.4.2", @@ -297,12 +298,10 @@ }, "lavamoat": { "allowScripts": { - "node-sass": true, "chromedriver": true, "geckodriver": true, "@sentry/cli": true, "electron": true, - "sqlite3": true, "core-js": false, "core-js-pure": false, "keccak": false, @@ -311,14 +310,13 @@ "sha3": false, "bufferutil": false, "utf-8-validate": false, - "ejs": false, - "sc-uws": false, "leveldown": false, "ursa-optional": false, "gc-stats": false, "github:assemblyscript/assemblyscript": false, "tiny-secp256k1": false, - "@lavamoat/preinstall-always-fail": false + "@lavamoat/preinstall-always-fail": false, + "fsevents": false } } } diff --git a/patches/@reduxjs+toolkit+1.5.0.patch b/patches/@reduxjs+toolkit+1.5.0.patch new file mode 100644 index 000000000..5a4a003cd --- /dev/null +++ b/patches/@reduxjs+toolkit+1.5.0.patch @@ -0,0 +1,74 @@ +diff --git a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.development.js b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.development.js +index 96ead94..c5492d3 100644 +--- a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.development.js ++++ b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.development.js +@@ -204,7 +204,7 @@ function (_Array) { + + var _proto = MiddlewareArray.prototype; + +- _proto.concat = function concat() { ++ Object.defineProperty(_proto, 'concat', { value: function concat() { + var _Array$prototype$conc; + + for (var _len = arguments.length, arr = new Array(_len), _key = 0; _key < _len; _key++) { +@@ -212,7 +212,7 @@ function (_Array) { + } + + return _construct(MiddlewareArray, (_Array$prototype$conc = _Array.prototype.concat).call.apply(_Array$prototype$conc, [this].concat(arr))); +- }; ++ }}) + + _proto.prepend = function prepend() { + for (var _len2 = arguments.length, arr = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { +diff --git a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.production.min.js b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.production.min.js +index eb2dd1c..d44d517 100644 +--- a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.production.min.js ++++ b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.cjs.production.min.js +@@ -1,2 +1,2 @@ +-"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}var t=require("immer"),r=e(t),n=require("redux"),o=require("reselect"),i=e(require("redux-thunk")),u=function(){var e=o.createSelector.apply(void 0,arguments),r=function(r){for(var n=arguments.length,o=new Array(n>1?n-1:0),i=1;i-1}function O(e){var t,r={},n=[],o={addCase:function(e,t){var n="string"==typeof e?e:e.type;if(n in r)throw new Error("addCase cannot be called with two reducers for the same action type");return r[n]=t,o},addMatcher:function(e,t){return n.push({matcher:e,reducer:t}),o},addDefaultCase:function(e){return t=e,o}};return e(o),[r,n,t]}function j(e,n,o,i){void 0===o&&(o=[]);var u="function"==typeof n?O(n):[n,o,i],a=u[0],c=u[1],f=u[2];return function(n,o){void 0===n&&(n=e);var i=[a[o.type]].concat(c.filter((function(e){return(0,e.matcher)(o)})).map((function(e){return e.reducer})));return 0===i.filter((function(e){return!!e})).length&&(i=[f]),i.reduce((function(e,n){if(n){if(t.isDraft(e)){var i=n(e,o);return void 0===i?e:i}if(t.isDraftable(e))return r(e,(function(e){return n(e,o)}));var u=n(e,o);if(void 0===u){if(null===e)return e;throw Error("A case reducer on a non-draftable value must not return undefined")}return u}return e}),n)}}function A(e){return function(n,o){var i=function(t){!function(e){return y(t=e)&&"string"==typeof t.type&&Object.keys(t).every(g);var t}(o)?e(o,t):e(o.payload,t)};return t.isDraft(n)?(i(n),n):r(n,i)}}function x(e,t){return t(e)}function S(e){function t(t,r){var n=x(t,e);n in r.entities||(r.ids.push(n),r.entities[n]=t)}function r(e,r){Array.isArray(e)||(e=Object.values(e));var n=e,o=Array.isArray(n),i=0;for(n=o?n:n[Symbol.iterator]();;){var u;if(o){if(i>=n.length)break;u=n[i++]}else{if((i=n.next()).done)break;u=i.value}t(u,r)}}function n(e,t){var r=!1;e.forEach((function(e){e in t.entities&&(delete t.entities[e],r=!0)})),r&&(t.ids=t.ids.filter((function(e){return e in t.entities})))}function o(t,r){var n={},o={};t.forEach((function(e){e.id in r.entities&&(o[e.id]={id:e.id,changes:a({},o[e.id]?o[e.id].changes:null,{},e.changes)})})),(t=Object.values(o)).length>0&&t.filter((function(t){return function(t,r,n){var o=Object.assign({},n.entities[r.id],r.changes),i=x(o,e),u=i!==r.id;return u&&(t[r.id]=i,delete n.entities[r.id]),n.entities[i]=o,u}(n,t,r)})).length>0&&(r.ids=r.ids.map((function(e){return n[e]||e})))}function i(t,n){Array.isArray(t)||(t=Object.values(t));var i=[],u=[],a=t,c=Array.isArray(a),f=0;for(a=c?a:a[Symbol.iterator]();;){var s;if(c){if(f>=a.length)break;s=a[f++]}else{if((f=a.next()).done)break;s=f.value}var l=s,d=x(l,e);d in n.entities?u.push({id:d,changes:l}):i.push(l)}o(u,n),r(i,n)}return{removeAll:(u=function(e){Object.assign(e,{ids:[],entities:{}})},c=A((function(e,t){return u(t)})),function(e){return c(e,void 0)}),addOne:A(t),addMany:A(r),setAll:A((function(e,t){Array.isArray(e)||(e=Object.values(e)),t.ids=[],t.entities={},r(e,t)})),updateOne:A((function(e,t){return o([e],t)})),updateMany:A(o),upsertOne:A((function(e,t){return i([e],t)})),upsertMany:A(i),removeOne:A((function(e,t){return n([e],t)})),removeMany:A(n)};var u,c}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator")));var w=function(e){void 0===e&&(e=21);for(var t="",r=e;r--;)t+="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW"[64*Math.random()|0];return t},E=["name","message","stack","code"],P=function(e){this.payload=e,this.name="RejectWithValue",this.message="Rejected"},_=function(e){if("object"==typeof e&&null!==e){var t={},r=E,n=Array.isArray(r),o=0;for(r=n?r:r[Symbol.iterator]();;){var i;if(n){if(o>=r.length)break;i=r[o++]}else{if((o=r.next()).done)break;i=o.value}"string"==typeof e[i]&&(t[i]=e[i])}return t}return{message:String(e)}},k=function(e,t){return(r=e)&&"function"==typeof r.match?e.match(t):e(t);var r};function I(){for(var e=arguments.length,t=new Array(e),r=0;r-1;return r&&n}function q(e){return"function"==typeof e[0]&&"pending"in e[0]&&"fulfilled"in e[0]&&"rejected"in e[0]}function D(){for(var e=arguments.length,t=new Array(e),r=0;r=a.length)break;s=a[f++]}else{if((f=a.next()).done)break;s=f.value}var l=s,d=x(l,e);d in r.entities?u.push({id:d,changes:l}):i.push(l)}o(u,r),n(i,r)}function u(r,n){r.sort(t),r.forEach((function(t){n.entities[e(t)]=t}));var o=Object.values(n.entities);o.sort(t);var i=o.map(e);(function(e,t){if(e.length!==t.length)return!1;for(var r=0;r",value:t};if("object"!=typeof t||null===t)return!1;var a=null!=o?o(t):Object.entries(t),c=i.length>0,f=a,s=Array.isArray(f),l=0;for(f=s?f:f[Symbol.iterator]();;){var d;if(s){if(l>=f.length)break;d=f[l++]}else{if((l=f.next()).done)break;d=l.value}var p=d[1],y=r.concat(d[0]);if(!(c&&i.indexOf(y.join("."))>=0)){if(!n(p))return{keyPath:y.join("."),value:p};if("object"==typeof p&&(u=e(p,y,n,o,i)))return u}}return!1},exports.getDefaultMiddleware=b,exports.getType=function(e){return""+e},exports.isAllOf=R,exports.isAnyOf=I,exports.isAsyncThunkAction=function e(){for(var t=arguments.length,r=new Array(t),n=0;n=n.length)break;u=n[i++]}else{if((i=n.next()).done)break;u=i.value}t.push(u.pending,u.rejected,u.fulfilled)}return I.apply(void 0,t)(e)}:e()(r[0])},exports.isFulfilled=function e(){for(var t=arguments.length,r=new Array(t),n=0;n1?n-1:0),i=1;i-1}function O(e){var t,r={},n=[],o={addCase:function(e,t){var n="string"==typeof e?e:e.type;if(n in r)throw new Error("addCase cannot be called with two reducers for the same action type");return r[n]=t,o},addMatcher:function(e,t){return n.push({matcher:e,reducer:t}),o},addDefaultCase:function(e){return t=e,o}};return e(o),[r,n,t]}function j(e,n,o,i){void 0===o&&(o=[]);var u="function"==typeof n?O(n):[n,o,i],a=u[0],c=u[1],f=u[2];return function(n,o){void 0===n&&(n=e);var i=[a[o.type]].concat(c.filter((function(e){return(0,e.matcher)(o)})).map((function(e){return e.reducer})));return 0===i.filter((function(e){return!!e})).length&&(i=[f]),i.reduce((function(e,n){if(n){if(t.isDraft(e)){var i=n(e,o);return void 0===i?e:i}if(t.isDraftable(e))return r(e,(function(e){return n(e,o)}));var u=n(e,o);if(void 0===u){if(null===e)return e;throw Error("A case reducer on a non-draftable value must not return undefined")}return u}return e}),n)}}function A(e){return function(n,o){var i=function(t){!function(e){return y(t=e)&&"string"==typeof t.type&&Object.keys(t).every(g);var t}(o)?e(o,t):e(o.payload,t)};return t.isDraft(n)?(i(n),n):r(n,i)}}function x(e,t){return t(e)}function S(e){function t(t,r){var n=x(t,e);n in r.entities||(r.ids.push(n),r.entities[n]=t)}function r(e,r){Array.isArray(e)||(e=Object.values(e));var n=e,o=Array.isArray(n),i=0;for(n=o?n:n[Symbol.iterator]();;){var u;if(o){if(i>=n.length)break;u=n[i++]}else{if((i=n.next()).done)break;u=i.value}t(u,r)}}function n(e,t){var r=!1;e.forEach((function(e){e in t.entities&&(delete t.entities[e],r=!0)})),r&&(t.ids=t.ids.filter((function(e){return e in t.entities})))}function o(t,r){var n={},o={};t.forEach((function(e){e.id in r.entities&&(o[e.id]={id:e.id,changes:a({},o[e.id]?o[e.id].changes:null,{},e.changes)})})),(t=Object.values(o)).length>0&&t.filter((function(t){return function(t,r,n){var o=Object.assign({},n.entities[r.id],r.changes),i=x(o,e),u=i!==r.id;return u&&(t[r.id]=i,delete n.entities[r.id]),n.entities[i]=o,u}(n,t,r)})).length>0&&(r.ids=r.ids.map((function(e){return n[e]||e})))}function i(t,n){Array.isArray(t)||(t=Object.values(t));var i=[],u=[],a=t,c=Array.isArray(a),f=0;for(a=c?a:a[Symbol.iterator]();;){var s;if(c){if(f>=a.length)break;s=a[f++]}else{if((f=a.next()).done)break;s=f.value}var l=s,d=x(l,e);d in n.entities?u.push({id:d,changes:l}):i.push(l)}o(u,n),r(i,n)}return{removeAll:(u=function(e){Object.assign(e,{ids:[],entities:{}})},c=A((function(e,t){return u(t)})),function(e){return c(e,void 0)}),addOne:A(t),addMany:A(r),setAll:A((function(e,t){Array.isArray(e)||(e=Object.values(e)),t.ids=[],t.entities={},r(e,t)})),updateOne:A((function(e,t){return o([e],t)})),updateMany:A(o),upsertOne:A((function(e,t){return i([e],t)})),upsertMany:A(i),removeOne:A((function(e,t){return n([e],t)})),removeMany:A(n)};var u,c}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator")));var w=function(e){void 0===e&&(e=21);for(var t="",r=e;r--;)t+="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW"[64*Math.random()|0];return t},E=["name","message","stack","code"],P=function(e){this.payload=e,this.name="RejectWithValue",this.message="Rejected"},_=function(e){if("object"==typeof e&&null!==e){var t={},r=E,n=Array.isArray(r),o=0;for(r=n?r:r[Symbol.iterator]();;){var i;if(n){if(o>=r.length)break;i=r[o++]}else{if((o=r.next()).done)break;i=o.value}"string"==typeof e[i]&&(t[i]=e[i])}return t}return{message:String(e)}},k=function(e,t){return(r=e)&&"function"==typeof r.match?e.match(t):e(t);var r};function I(){for(var e=arguments.length,t=new Array(e),r=0;r-1;return r&&n}function q(e){return"function"==typeof e[0]&&"pending"in e[0]&&"fulfilled"in e[0]&&"rejected"in e[0]}function D(){for(var e=arguments.length,t=new Array(e),r=0;r=a.length)break;s=a[f++]}else{if((f=a.next()).done)break;s=f.value}var l=s,d=x(l,e);d in r.entities?u.push({id:d,changes:l}):i.push(l)}o(u,r),n(i,r)}function u(r,n){r.sort(t),r.forEach((function(t){n.entities[e(t)]=t}));var o=Object.values(n.entities);o.sort(t);var i=o.map(e);(function(e,t){if(e.length!==t.length)return!1;for(var r=0;r",value:t};if("object"!=typeof t||null===t)return!1;var a=null!=o?o(t):Object.entries(t),c=i.length>0,f=a,s=Array.isArray(f),l=0;for(f=s?f:f[Symbol.iterator]();;){var d;if(s){if(l>=f.length)break;d=f[l++]}else{if((l=f.next()).done)break;d=l.value}var p=d[1],y=r.concat(d[0]);if(!(c&&i.indexOf(y.join("."))>=0)){if(!n(p))return{keyPath:y.join("."),value:p};if("object"==typeof p&&(u=e(p,y,n,o,i)))return u}}return!1},exports.getDefaultMiddleware=b,exports.getType=function(e){return""+e},exports.isAllOf=R,exports.isAnyOf=I,exports.isAsyncThunkAction=function e(){for(var t=arguments.length,r=new Array(t),n=0;n=n.length)break;u=n[i++]}else{if((i=n.next()).done)break;u=i.value}t.push(u.pending,u.rejected,u.fulfilled)}return I.apply(void 0,t)(e)}:e()(r[0])},exports.isFulfilled=function e(){for(var t=arguments.length,r=new Array(t),n=0;n err.message); - const errorMessage = `Errors found in browser console:\n${errorReports.join( - '\n', - )}`; - console.error(new Error(errorMessage)); - } - } - if (this.currentTest.state === 'failed') { - await driver.verboseReportOnFailure(this.currentTest.title); - } - }); - - after(async function () { - await ganacheServer.quit(); - await driver.quit(); - }); - - describe('Going through the first time flow', function () { - it('clicks the continue button on the welcome screen', async function () { - await driver.findElement(By.css('.welcome-page__header')); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`, - ), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "Create New Wallet" option', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Create a Wallet')]`), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "No thanks" option on the metametrics opt-in screen', async function () { - await driver.clickElement(By.css('.btn-default')); - await driver.delay(largeDelayMs); - }); - - it('accepts a secure password', async function () { - const passwordBox = await driver.findElement( - By.css('.first-time-flow__form #create-password'), - ); - const passwordBoxConfirm = await driver.findElement( - By.css('.first-time-flow__form #confirm-password'), - ); - - await passwordBox.sendKeys('correct horse battery staple'); - await passwordBoxConfirm.sendKeys('correct horse battery staple'); - - await driver.clickElement(By.css('.first-time-flow__checkbox')); - await driver.clickElement(By.css('.first-time-flow__form button')); - await driver.delay(regularDelayMs); - }); - - let seedPhrase; - - it('reveals the seed phrase', async function () { - const byRevealButton = By.css( - '.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button', - ); - await driver.clickElement(byRevealButton); - await driver.delay(regularDelayMs); - - const revealedSeedPhrase = await driver.findElement( - By.css('.reveal-seed-phrase__secret-words'), - ); - seedPhrase = await revealedSeedPhrase.getText(); - assert.equal(seedPhrase.split(' ').length, 12); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.next.message}')]`, - ), - ); - await driver.delay(regularDelayMs); - }); - - async function clickWordAndWait(word) { - await driver.clickElement( - By.css( - `[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`, - ), - ); - await driver.delay(tinyDelayMs); - } - - it('can retype the seed phrase', async function () { - const words = seedPhrase.split(' '); - - for (const word of words) { - await clickWordAndWait(word); - } - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('clicks through the success screen', async function () { - await driver.findElement( - By.xpath(`//div[contains(text(), 'Congratulations')]`), - ); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`, - ), - ); - await driver.delay(regularDelayMs); - }); - }); - - describe('Import seed phrase', function () { - it('logs out of the vault', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - const lockButton = await driver.findClickableElement( - By.css('.account-menu__lock-button'), - ); - assert.equal(await lockButton.getText(), 'Lock'); - await lockButton.click(); - await driver.delay(regularDelayMs); - }); - - it('imports seed phrase', async function () { - const restoreSeedLink = await driver.findClickableElement( - By.css('.unlock-page__link--import'), - ); - assert.equal( - await restoreSeedLink.getText(), - 'Import using account seed phrase', - ); - await restoreSeedLink.click(); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.import-account__checkbox-container')); - - const seedTextArea = await driver.findElement(By.css('textarea')); - await seedTextArea.sendKeys(testSeedPhrase); - await driver.delay(regularDelayMs); - - const passwordInputs = await driver.findElements(By.css('input')); - await driver.delay(regularDelayMs); - - await passwordInputs[0].sendKeys('correct horse battery staple'); - await passwordInputs[1].sendKeys('correct horse battery staple'); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.restore.message}')]`, - ), - ); - await driver.delay(regularDelayMs); - }); - - it('balance renders', async function () { - const balance = await driver.findElement( - By.css('[data-testid="wallet-balance"] .list-item__heading'), - ); - await driver.wait(until.elementTextMatches(balance, /25\s*ETH/u)); - await driver.delay(regularDelayMs); - }); - }); - - describe('Adds an entry to the address book and sends eth to that address', function () { - it('starts a send transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); - await driver.delay(regularDelayMs); - - const inputAddress = await driver.findElement( - By.css('input[placeholder="Search, public address (0x), or ENS"]'), - ); - await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.dialog.send__dialog.dialog--message')); - - const addressBookAddModal = await driver.findElement( - By.css('span .modal'), - ); - await driver.findElement(By.css('.add-to-address-book-modal')); - const addressBookInput = await driver.findElement( - By.css('.add-to-address-book-modal__input'), - ); - await addressBookInput.sendKeys('Test Name 1'); - await driver.delay(tinyDelayMs); - await driver.clickElement( - By.css('.add-to-address-book-modal__footer .btn-primary'), - ); - - await driver.wait(until.stalenessOf(addressBookAddModal)); - - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('1'); - - const inputValue = await inputAmount.getAttribute('value'); - assert.equal(inputValue, '1'); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.delay(regularDelayMs); - }); - - it('confirms the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); - await driver.delay(largeDelayMs * 2); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), - ); - return confirmedTxes.length === 1; - }, 10000); - - const txValues = await driver.findElement( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); - }); - }); - - describe('Sends to an address book entry', function () { - it('starts a send transaction by clicking address book entry', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); - await driver.delay(regularDelayMs); - - const recipientRowTitle = await driver.findElement( - By.css('.send__select-recipient-wrapper__group-item__title'), - ); - const recipientRowTitleString = await recipientRowTitle.getText(); - assert.equal(recipientRowTitleString, 'Test Name 1'); - - await driver.clickElement( - By.css('.send__select-recipient-wrapper__group-item'), - ); - - await driver.delay(regularDelayMs); - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('2'); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.delay(regularDelayMs); - }); - - it('confirms the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); - await driver.delay(largeDelayMs * 2); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), - ); - return confirmedTxes.length === 2; - }, 10000); - - const txValues = await driver.findElement( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/u), 10000); - }); - }); -}); diff --git a/test/e2e/ethereum-on.spec.js b/test/e2e/ethereum-on.spec.js deleted file mode 100644 index dfe1a5d1a..000000000 --- a/test/e2e/ethereum-on.spec.js +++ /dev/null @@ -1,201 +0,0 @@ -const assert = require('assert'); -const webdriver = require('selenium-webdriver'); - -const { By, until } = webdriver; -const enLocaleMessages = require('../../app/_locales/en/messages.json'); -const { regularDelayMs, largeDelayMs } = require('./helpers'); -const { buildWebDriver } = require('./webdriver'); -const Ganache = require('./ganache'); - -const ganacheServer = new Ganache(); - -describe('MetaMask', function () { - let driver; - let publicAddress; - - this.timeout(0); - this.bail(true); - - before(async function () { - await ganacheServer.start({ - accounts: [ - { - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - balance: 25000000000000000000, - }, - ], - }); - const result = await buildWebDriver(); - driver = result.driver; - await driver.navigate(); - }); - - afterEach(async function () { - if (process.env.SELENIUM_BROWSER === 'chrome') { - const errors = await driver.checkBrowserForConsoleErrors(driver); - if (errors.length) { - const errorReports = errors.map((err) => err.message); - const errorMessage = `Errors found in browser console:\n${errorReports.join( - '\n', - )}`; - console.error(new Error(errorMessage)); - } - } - if (this.currentTest.state === 'failed') { - await driver.verboseReportOnFailure(this.currentTest.title); - } - }); - - after(async function () { - await ganacheServer.quit(); - await driver.quit(); - }); - - describe('Going through the first time flow, but skipping the seed phrase challenge', function () { - it('clicks the continue button on the welcome screen', async function () { - await driver.findElement(By.css('.welcome-page__header')); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`, - ), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "Create New Wallet" option', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Create a Wallet')]`), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "No thanks" option on the metametrics opt-in screen', async function () { - await driver.clickElement(By.css('.btn-default')); - await driver.delay(largeDelayMs); - }); - - it('accepts a secure password', async function () { - const passwordBox = await driver.findElement( - By.css('.first-time-flow__form #create-password'), - ); - const passwordBoxConfirm = await driver.findElement( - By.css('.first-time-flow__form #confirm-password'), - ); - - await passwordBox.sendKeys('correct horse battery staple'); - await passwordBoxConfirm.sendKeys('correct horse battery staple'); - - await driver.clickElement(By.css('.first-time-flow__checkbox')); - await driver.clickElement(By.css('.first-time-flow__form button')); - await driver.delay(largeDelayMs); - }); - - it('skips the seed phrase challenge', async function () { - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`, - ), - ); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__account-details"]'), - ); - }); - - it('gets the current accounts address', async function () { - const addressInput = await driver.findElement( - By.css('.readonly-input__input'), - ); - publicAddress = await addressInput.getAttribute('value'); - const accountModal = await driver.findElement(By.css('span .modal')); - - await driver.clickElement(By.css('.account-modal__close')); - - await driver.wait(until.stalenessOf(accountModal)); - await driver.delay(regularDelayMs); - }); - }); - - describe('provider listening for events', function () { - let extension; - let popup; - let dapp; - - it('connects to the dapp', async function () { - await driver.openNewPage('http://127.0.0.1:8080/'); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Connect')]`), - ); - - await driver.delay(regularDelayMs); - - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - - extension = windowHandles[0]; - dapp = await driver.switchToWindowWithTitle( - 'E2E Test Dapp', - windowHandles, - ); - popup = windowHandles.find( - (handle) => handle !== extension && handle !== dapp, - ); - - await driver.switchToWindow(popup); - - await driver.delay(regularDelayMs); - - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Connect')]`), - ); - - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - }); - - it('has the ganache network id within the dapp', async function () { - const networkDiv = await driver.findElement(By.css('#network')); - await driver.delay(regularDelayMs); - assert.equal(await networkDiv.getText(), '1337'); - }); - - it('changes the network', async function () { - await driver.switchToWindow(extension); - - await driver.clickElement(By.css('.network-display')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Ropsten')]`), - ); - await driver.delay(largeDelayMs); - }); - - it('sets the network div within the dapp', async function () { - await driver.switchToWindow(dapp); - const networkDiv = await driver.findElement(By.css('#network')); - assert.equal(await networkDiv.getText(), '3'); - }); - - it('sets the chainId div within the dapp', async function () { - await driver.switchToWindow(dapp); - const chainIdDiv = await driver.findElement(By.css('#chainId')); - assert.equal(await chainIdDiv.getText(), '0x3'); - }); - - it('sets the account div within the dapp', async function () { - await driver.switchToWindow(dapp); - const accountsDiv = await driver.findElement(By.css('#accounts')); - assert.equal(await accountsDiv.getText(), publicAddress.toLowerCase()); - }); - }); -}); diff --git a/test/e2e/fixtures/address-entry/state.json b/test/e2e/fixtures/address-entry/state.json new file mode 100644 index 000000000..26da39502 --- /dev/null +++ b/test/e2e/fixtures/address-entry/state.json @@ -0,0 +1,136 @@ +{ + "data": { + "AddressBookController": { + "addressBook": { + "0x539": { + "0x2f318C334780961FB129D2a6c30D0763d9a5C970": { + "address": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", + "chainId": "0x539", + "isEns": false, + "memo": "", + "name": "Test Name 1" + } + } + } + }, + "AppStateController": { + "mkrMigrationReminderTimestamp": null, + "swapsWelcomeMessageHasBeenShown": true + }, + "CachedBalancesController": { + "cachedBalances": { + "4": {} + } + }, + "CurrencyController": { + "conversionDate": 1575697244.188, + "conversionRate": 149.61, + "currentCurrency": "usd", + "nativeCurrency": "ETH" + }, + "IncomingTransactionsController": { + "incomingTransactions": {}, + "incomingTxLastFetchedBlocksByNetwork": { + "goerli": null, + "kovan": null, + "mainnet": null, + "rinkeby": 5570536 + } + }, + "KeyringController": { + "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" + }, + "NetworkController": { + "network": "1337", + "provider": { + "nickname": "Localhost 8545", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "type": "rpc" + } + }, + "OnboardingController": { + "onboardingTabs": {}, + "seedPhraseBackedUp": false + }, + "PermissionsMetadata": { + "domainMetadata": { + "metamask.github.io": { + "icon": null, + "name": "M E T A M A S K M E S H T E S T" + } + }, + "permissionsHistory": {}, + "permissionsLog": [ + { + "id": 746677923, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "metamask.github.io", + "request": { + "id": 746677923, + "jsonrpc": "2.0", + "method": "eth_accounts", + "origin": "metamask.github.io", + "params": [] + }, + "requestTime": 1575697241368, + "response": { + "id": 746677923, + "jsonrpc": "2.0", + "result": [] + }, + "responseTime": 1575697241370, + "success": true + } + ] + }, + "PreferencesController": { + "accountTokens": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "rinkeby": [], + "ropsten": [] + } + }, + "assetImages": {}, + "completedOnboarding": true, + "currentLocale": "en", + "featureFlags": { + "showIncomingTransactions": true, + "transactionTime": false + }, + "firstTimeFlowType": "create", + "forgottenPassword": false, + "frequentRpcListDetail": [], + "identities": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "name": "Account 1" + } + }, + "knownMethodData": {}, + "lostIdentities": {}, + "metaMetricsId": null, + "metaMetricsSendCount": 0, + "participateInMetaMetrics": false, + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "suggestedTokens": {}, + "tokens": [], + "useBlockie": false, + "useNonceField": false, + "usePhishDetect": true + }, + "config": {}, + "firstTimeInfo": { + "date": 1575697234195, + "version": "7.7.0" + } + }, + "meta": { + "version": 40 + } +} diff --git a/test/e2e/fixtures/personal-sign/state.json b/test/e2e/fixtures/connected-state/state.json similarity index 100% rename from test/e2e/fixtures/personal-sign/state.json rename to test/e2e/fixtures/connected-state/state.json diff --git a/test/e2e/fixtures/send-edit/state.json b/test/e2e/fixtures/send-edit/state.json new file mode 100644 index 000000000..77fd20720 --- /dev/null +++ b/test/e2e/fixtures/send-edit/state.json @@ -0,0 +1,172 @@ +{ + "data": { + "AppStateController": { + "mkrMigrationReminderTimestamp": null, + "swapsWelcomeMessageHasBeenShown": true + }, + "CachedBalancesController": { + "cachedBalances": { + "4": {} + } + }, + "CurrencyController": { + "conversionDate": 1575697244.188, + "conversionRate": 149.61, + "currentCurrency": "usd", + "nativeCurrency": "ETH" + }, + "IncomingTransactionsController": { + "incomingTransactions": {}, + "incomingTxLastFetchedBlocksByNetwork": { + "goerli": null, + "kovan": null, + "mainnet": null, + "rinkeby": 5570536 + } + }, + "KeyringController": { + "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" + }, + "NetworkController": { + "network": "1337", + "provider": { + "nickname": "Localhost 8545", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "type": "rpc" + } + }, + "OnboardingController": { + "onboardingTabs": {}, + "seedPhraseBackedUp": false + }, + "PermissionsMetadata": { + "domainMetadata": { + "metamask.github.io": { + "icon": null, + "name": "M E T A M A S K M E S H T E S T" + } + }, + "permissionsHistory": {}, + "permissionsLog": [ + { + "id": 746677923, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "metamask.github.io", + "request": { + "id": 746677923, + "jsonrpc": "2.0", + "method": "eth_accounts", + "origin": "metamask.github.io", + "params": [] + }, + "requestTime": 1575697241368, + "response": { + "id": 746677923, + "jsonrpc": "2.0", + "result": [] + }, + "responseTime": 1575697241370, + "success": true + } + ] + }, + "PreferencesController": { + "accountTokens": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "rinkeby": [], + "ropsten": [] + } + }, + "assetImages": {}, + "completedOnboarding": true, + "currentLocale": "en", + "featureFlags": { + "showIncomingTransactions": true, + "transactionTime": false + }, + "firstTimeFlowType": "create", + "forgottenPassword": false, + "frequentRpcListDetail": [], + "identities": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "name": "Account 1" + } + }, + "knownMethodData": {}, + "lostIdentities": {}, + "metaMetricsId": null, + "metaMetricsSendCount": 0, + "participateInMetaMetrics": false, + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "suggestedTokens": {}, + "tokens": [], + "useBlockie": false, + "useNonceField": false, + "usePhishDetect": true + }, + "TransactionController": { + "transactions": { + "4046084157914634": { + "chainId": "0x539", + "history": [ + { + "chainId": "0x539", + "id": 4046084157914634, + "loadingDefaults": true, + "metamaskNetworkId": "1337", + "origin": "metamask", + "status": "unapproved", + "time": 1617228030067, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x61a8", + "gasPrice": "0x2540be400", + "to": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", + "value": "0xde0b6b3a7640000" + }, + "type": "sentEther" + }, + [ + { + "note": "Added new unapproved transaction.", + "op": "replace", + "path": "/loadingDefaults", + "timestamp": 1617228030069, + "value": false + } + ] + ], + "id": 4046084157914634, + "loadingDefaults": false, + "metamaskNetworkId": "1337", + "origin": "metamask", + "status": "unapproved", + "time": 1617228030067, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x61a8", + "gasPrice": "0x2540be400", + "to": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", + "value": "0xde0b6b3a7640000" + }, + "type": "sentEther" + } + } + }, + "config": {}, + "firstTimeInfo": { + "date": 1575697234195, + "version": "7.7.0" + } + }, + "meta": { + "version": 40 + } +} diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 0e604a6bb..15f9e9e7b 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -55,19 +55,6 @@ retry concurrently --kill-others \ --success first \ 'mocha test/e2e/from-import-ui.spec' -retry concurrently --kill-others \ - --names 'e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'mocha test/e2e/send-edit.spec' - -retry concurrently --kill-others \ - --names 'dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/ethereum-on.spec' - retry concurrently --kill-others \ --names 'dapp,e2e' \ --prefix '[{time}][{name}]' \ @@ -82,13 +69,6 @@ retry concurrently --kill-others \ 'yarn sendwithprivatedapp' \ 'mocha test/e2e/incremental-security.spec' -retry concurrently --kill-others \ - --names 'dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/address-book.spec' - retry concurrently --kill-others \ --names '3box,dapp,e2e' \ --prefix '[{time}][{name}]' \ diff --git a/test/e2e/send-edit.spec.js b/test/e2e/send-edit.spec.js deleted file mode 100644 index 9ae21e89b..000000000 --- a/test/e2e/send-edit.spec.js +++ /dev/null @@ -1,254 +0,0 @@ -const assert = require('assert'); -const webdriver = require('selenium-webdriver'); - -const { By, until } = webdriver; -const enLocaleMessages = require('../../app/_locales/en/messages.json'); -const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers'); -const { buildWebDriver } = require('./webdriver'); -const Ganache = require('./ganache'); - -const ganacheServer = new Ganache(); - -describe('Using MetaMask with an existing account', function () { - let driver; - - const testSeedPhrase = - 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'; - - this.timeout(0); - this.bail(true); - - before(async function () { - await ganacheServer.start({ - accounts: [ - { - secretKey: - '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', - balance: 25000000000000000000, - }, - ], - }); - const result = await buildWebDriver(); - driver = result.driver; - await driver.navigate(); - }); - - afterEach(async function () { - if (process.env.SELENIUM_BROWSER === 'chrome') { - const errors = await driver.checkBrowserForConsoleErrors(driver); - if (errors.length) { - const errorReports = errors.map((err) => err.message); - const errorMessage = `Errors found in browser console:\n${errorReports.join( - '\n', - )}`; - console.error(new Error(errorMessage)); - } - } - if (this.currentTest.state === 'failed') { - await driver.verboseReportOnFailure(this.currentTest.title); - } - }); - - after(async function () { - await ganacheServer.quit(); - await driver.quit(); - }); - - describe('First time flow starting from an existing seed phrase', function () { - it('clicks the continue button on the welcome screen', async function () { - await driver.findElement(By.css('.welcome-page__header')); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`, - ), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "Import Wallet" option', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Import wallet')]`), - ); - await driver.delay(largeDelayMs); - }); - - it('clicks the "No thanks" option on the metametrics opt-in screen', async function () { - await driver.clickElement(By.css('.btn-default')); - await driver.delay(largeDelayMs); - }); - - it('imports a seed phrase', async function () { - const [seedTextArea] = await driver.findElements( - By.css('input[placeholder="Paste seed phrase from clipboard"]'), - ); - await seedTextArea.sendKeys(testSeedPhrase); - await driver.delay(regularDelayMs); - - const [password] = await driver.findElements(By.id('password')); - await password.sendKeys('correct horse battery staple'); - const [confirmPassword] = await driver.findElements( - By.id('confirm-password'), - ); - confirmPassword.sendKeys('correct horse battery staple'); - - await driver.clickElement(By.css('.first-time-flow__terms')); - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Import')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('clicks through the success screen', async function () { - await driver.findElement( - By.xpath(`//div[contains(text(), 'Congratulations')]`), - ); - await driver.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`, - ), - ); - await driver.delay(regularDelayMs); - }); - }); - - describe('Send ETH from inside MetaMask', function () { - it('starts a send transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); - await driver.delay(regularDelayMs); - - const inputAddress = await driver.findElement( - By.css('input[placeholder="Search, public address (0x), or ENS"]'), - ); - await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); - - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('1'); - - // Set the gas limit - await driver.clickElement(By.css('.advanced-gas-options-btn')); - await driver.delay(regularDelayMs); - - const gasModal = await driver.findElement(By.css('span .modal')); - - const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), - ); - - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.sendKeys('10'); - await driver.delay(50); - await driver.delay(tinyDelayMs); - await driver.delay(50); - - await gasLimitInput.clear(); - await driver.delay(50); - await gasLimitInput.sendKeys('25000'); - - await driver.delay(1000); - - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.wait(until.stalenessOf(gasModal)); - await driver.delay(regularDelayMs); - - // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.delay(regularDelayMs); - }); - - it('has correct value and fee on the confirm screen the transaction', async function () { - const transactionAmounts = await driver.findElements( - By.css('.currency-display-component__text'), - ); - const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '1'); - - const transactionFee = transactionAmounts[1]; - assert.equal(await transactionFee.getText(), '0.00025'); - }); - - it('edits the transaction', async function () { - await driver.clickElement( - By.css('.confirm-page-container-header__back-button'), - ); - - await driver.delay(regularDelayMs); - - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - - await inputAmount.clear(); - await driver.delay(50); - await inputAmount.sendKeys('2.2'); - - await driver.clickElement(By.css('.advanced-gas-options-btn')); - await driver.delay(regularDelayMs); - - const gasModal = await driver.findElement(By.css('span .modal')); - - const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), - ); - - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.sendKeys('8'); - await driver.delay(50); - await driver.delay(tinyDelayMs); - await driver.delay(50); - - await gasLimitInput.clear(); - await driver.delay(50); - await gasLimitInput.sendKeys('100000'); - - await driver.delay(1000); - - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.wait(until.stalenessOf(gasModal)); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.delay(regularDelayMs); - }); - - it('has correct updated value on the confirm screen the transaction', async function () { - const transactionAmounts = await driver.findElements( - By.css('.currency-display-component__text'), - ); - const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '2.2'); - - const transactionFee = transactionAmounts[1]; - assert.equal(await transactionFee.getText(), '0.0008'); - }); - - it('confirms the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), - ); - return confirmedTxes.length === 1; - }, 10000); - - const txValues = await driver.findElements( - By.css('.transaction-list-item__primary-currency'), - ); - assert.equal(txValues.length, 1); - assert.ok(/-2.2\s*ETH/u.test(await txValues[0].getText())); - }); - }); -}); diff --git a/test/e2e/tests/address-book.spec.js b/test/e2e/tests/address-book.spec.js new file mode 100644 index 000000000..c8581cfee --- /dev/null +++ b/test/e2e/tests/address-book.spec.js @@ -0,0 +1,147 @@ +const { strict: assert } = require('assert'); +const { By, Key, until } = require('selenium-webdriver'); +const { withFixtures } = require('../helpers'); + +describe('Address Book', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('Adds an entry to the address book and sends eth to that address', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + const passwordField = await driver.findElement(By.css('#password')); + await passwordField.sendKeys('correct horse battery staple'); + await passwordField.sendKeys(Key.ENTER); + + await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + + const inputAddress = await driver.findElement( + By.css('input[placeholder="Search, public address (0x), or ENS"]'), + ); + await inputAddress.sendKeys( + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + + await driver.clickElement( + By.css('.dialog.send__dialog.dialog--message'), + ); + + const addressBookAddModal = await driver.findElement( + By.css('span .modal'), + ); + await driver.findElement(By.css('.add-to-address-book-modal')); + const addressBookInput = await driver.findElement( + By.css('.add-to-address-book-modal__input'), + ); + await addressBookInput.sendKeys('Test Name 1'); + await driver.clickElement( + By.css('.add-to-address-book-modal__footer .btn-primary'), + ); + await driver.wait(until.stalenessOf(addressBookAddModal)); + + const inputAmount = await driver.findElement( + By.css('.unit-input__input'), + ); + await inputAmount.sendKeys('1'); + + const inputValue = await inputAmount.getAttribute('value'); + assert.equal(inputValue, '1'); + + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Next')]`), + ); + + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Confirm')]`), + ); + + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + By.css( + '.transaction-list__completed-transactions .transaction-list-item', + ), + ); + return confirmedTxes.length === 1; + }, 10000); + + const txValues = await driver.findElement( + By.css('.transaction-list-item__primary-currency'), + ); + await driver.wait( + until.elementTextMatches(txValues, /-1\s*ETH/u), + 10000, + ); + }, + ); + }); + it('Sends to an address book entry', async function () { + await withFixtures( + { + fixtures: 'address-entry', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + const passwordField = await driver.findElement(By.css('#password')); + await passwordField.sendKeys('correct horse battery staple'); + await passwordField.sendKeys(Key.ENTER); + + await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + const recipientRowTitle = await driver.findElement( + By.css('.send__select-recipient-wrapper__group-item__title'), + ); + const recipientRowTitleString = await recipientRowTitle.getText(); + assert.equal(recipientRowTitleString, 'Test Name 1'); + await driver.clickElement( + By.css('.send__select-recipient-wrapper__group-item'), + ); + + const inputAmount = await driver.findElement( + By.css('.unit-input__input'), + ); + await inputAmount.sendKeys('2'); + + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Next')]`), + ); + + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Confirm')]`), + ); + + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + By.css( + '.transaction-list__completed-transactions .transaction-list-item', + ), + ); + return confirmedTxes.length === 1; + }, 10000); + + const txValues = await driver.findElement( + By.css('.transaction-list-item__primary-currency'), + ); + await driver.wait( + until.elementTextMatches(txValues, /-2\s*ETH/u), + 10000, + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/personal-sign.spec.js b/test/e2e/tests/personal-sign.spec.js index e9efc5ea4..ef4c0be7b 100644 --- a/test/e2e/tests/personal-sign.spec.js +++ b/test/e2e/tests/personal-sign.spec.js @@ -16,7 +16,7 @@ describe('Personal sign', function () { await withFixtures( { dapp: true, - fixtures: 'personal-sign', + fixtures: 'connected-state', ganacheOptions, title: this.test.title, }, diff --git a/test/e2e/tests/provider-events.spec.js b/test/e2e/tests/provider-events.spec.js new file mode 100644 index 000000000..417eb021e --- /dev/null +++ b/test/e2e/tests/provider-events.spec.js @@ -0,0 +1,59 @@ +const { strict: assert } = require('assert'); +const { By, Key } = require('selenium-webdriver'); +const { withFixtures, regularDelayMs } = require('../helpers'); + +describe('MetaMask', function () { + it('provider should inform dapp when switching networks', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + await withFixtures( + { + dapp: true, + fixtures: 'connected-state', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + const passwordField = await driver.findElement(By.css('#password')); + await passwordField.sendKeys('correct horse battery staple'); + await passwordField.sendKeys(Key.ENTER); + + await driver.openNewPage('http://127.0.0.1:8080/'); + const networkDiv = await driver.findElement(By.css('#network')); + const chainIdDiv = await driver.findElement(By.css('#chainId')); + await driver.delay(regularDelayMs); + assert.equal(await networkDiv.getText(), '1337'); + assert.equal(await chainIdDiv.getText(), '0x539'); + + const windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindow(windowHandles[0]); + + await driver.clickElement(By.css('.network-display')); + await driver.clickElement( + By.xpath(`//span[contains(text(), 'Ropsten')]`), + ); + await driver.delay(regularDelayMs); + + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + const switchedNetworkDiv = await driver.findElement(By.css('#network')); + const switchedChainIdDiv = await driver.findElement(By.css('#chainId')); + const accountsDiv = await driver.findElement(By.css('#accounts')); + + assert.equal(await switchedNetworkDiv.getText(), '3'); + assert.equal(await switchedChainIdDiv.getText(), '0x3'); + assert.equal( + await accountsDiv.getText(), + '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/send-edit.spec.js b/test/e2e/tests/send-edit.spec.js new file mode 100644 index 000000000..3f9846060 --- /dev/null +++ b/test/e2e/tests/send-edit.spec.js @@ -0,0 +1,110 @@ +const { strict: assert } = require('assert'); +const { By, Key, until } = require('selenium-webdriver'); +const { + withFixtures, + tinyDelayMs, + regularDelayMs, + largeDelayMs, +} = require('../helpers'); + +describe('Editing Confirm Transaction', function () { + it('goes back from confirm page to edit eth value, gas price and gas limit', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + await withFixtures( + { + fixtures: 'send-edit', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + const passwordField = await driver.findElement(By.css('#password')); + await passwordField.sendKeys('correct horse battery staple'); + await passwordField.sendKeys(Key.ENTER); + + const transactionAmounts = await driver.findElements( + By.css('.currency-display-component__text'), + ); + const transactionAmount = transactionAmounts[0]; + assert.equal(await transactionAmount.getText(), '1'); + + const transactionFee = transactionAmounts[1]; + assert.equal(await transactionFee.getText(), '0.00025'); + + await driver.clickElement( + By.css('.confirm-page-container-header__back-button'), + ); + const inputAmount = await driver.findElement( + By.css('.unit-input__input'), + ); + await inputAmount.clear(); + await inputAmount.sendKeys('2.2'); + + await driver.clickElement(By.css('.advanced-gas-options-btn')); + await driver.delay(regularDelayMs); + + const gasModal = await driver.findElement(By.css('span .modal')); + + const [gasPriceInput, gasLimitInput] = await driver.findElements( + By.css('.advanced-gas-inputs__gas-edit-row__input'), + ); + + await gasPriceInput.clear(); + await gasPriceInput.sendKeys('8'); + await driver.delay(tinyDelayMs); + + await gasLimitInput.clear(); + await gasLimitInput.sendKeys('100000'); + await driver.delay(largeDelayMs); + + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Save')]`), + ); + await driver.wait(until.stalenessOf(gasModal)); + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Next')]`), + ); + + // has correct updated value on the confirm screen the transaction + const editedTransactionAmounts = await driver.findElements( + By.css('.currency-display-component__text'), + ); + const editedTransactionAmount = editedTransactionAmounts[0]; + assert.equal(await editedTransactionAmount.getText(), '2.2'); + + const editedTransactionFee = editedTransactionAmounts[1]; + assert.equal(await editedTransactionFee.getText(), '0.0008'); + + // confirms the transaction + await driver.clickElement( + By.xpath(`//button[contains(text(), 'Confirm')]`), + ); + await driver.delay(regularDelayMs); + + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + By.css( + '.transaction-list__completed-transactions .transaction-list-item', + ), + ); + return confirmedTxes.length === 1; + }, 10000); + + const txValues = await driver.findElements( + By.css('.transaction-list-item__primary-currency'), + ); + assert.equal(txValues.length, 1); + assert.ok(/-2.2\s*ETH/u.test(await txValues[0].getText())); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index f1da84a21..7787468ba 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1,6 +1,6 @@ const { promises: fs } = require('fs'); const { strict: assert } = require('assert'); -const { until, error: webdriverError } = require('selenium-webdriver'); +const { until, error: webdriverError, By } = require('selenium-webdriver'); class Driver { /** @@ -15,6 +15,30 @@ class Driver { this.timeout = timeout; } + buildLocator(locator) { + if (typeof locator === 'string') { + // If locator is a string we assume its a css selector + return By.css(locator); + } else if (locator.value) { + // For backwards compatibility, checking if the locator has a value prop + // tells us this is a Selenium locator + return locator; + } else if (locator.xpath) { + // Providing an xpath prop to the object will consume the locator as an + // xpath locator. + return By.xpath(locator.xpath); + } else if (locator.text) { + // Providing a text prop, and optionally a tag, will use xpath to look + // for an element with the tag that has matching text. + return By.xpath( + `//${locator.tag ?? '*'}[contains(text(), '${locator.text}')]`, + ); + } + throw new Error( + `The locator '${locator}' is not supported by the E2E test driver`, + ); + } + async delay(time) { await new Promise((resolve) => setTimeout(resolve, time)); } @@ -29,17 +53,20 @@ class Driver { // Element interactions - async findElement(locator) { + async findElement(rawLocator) { + const locator = this.buildLocator(rawLocator); return await this.driver.wait(until.elementLocated(locator), this.timeout); } - async findVisibleElement(locator) { + async findVisibleElement(rawLocator) { + const locator = this.buildLocator(rawLocator); const element = await this.findElement(locator); await this.driver.wait(until.elementIsVisible(element), this.timeout); return element; } - async findClickableElement(locator) { + async findClickableElement(rawLocator) { + const locator = this.buildLocator(rawLocator); const element = await this.findElement(locator); await Promise.all([ this.driver.wait(until.elementIsVisible(element), this.timeout), @@ -48,11 +75,13 @@ class Driver { return element; } - async findElements(locator) { + async findElements(rawLocator) { + const locator = this.buildLocator(rawLocator); return await this.driver.wait(until.elementsLocated(locator), this.timeout); } - async findClickableElements(locator) { + async findClickableElements(rawLocator) { + const locator = this.buildLocator(rawLocator); const elements = await this.findElements(locator); await Promise.all( elements.reduce((acc, element) => { @@ -66,12 +95,14 @@ class Driver { return elements; } - async clickElement(locator) { + async clickElement(rawLocator) { + const locator = this.buildLocator(rawLocator); const element = await this.findClickableElement(locator); await element.click(); } - async clickPoint(locator, x, y) { + async clickPoint(rawLocator, x, y) { + const locator = this.buildLocator(rawLocator); const element = await this.findElement(locator); await this.driver .actions() @@ -87,7 +118,8 @@ class Driver { ); } - async assertElementNotPresent(locator) { + async assertElementNotPresent(rawLocator) { + const locator = this.buildLocator(rawLocator); let dataTab; try { dataTab = await this.findElement(locator); diff --git a/test/unit/app/controllers/permissions/helpers.js b/test/helpers/permission-controller-helpers.js similarity index 94% rename from test/unit/app/controllers/permissions/helpers.js rename to test/helpers/permission-controller-helpers.js index d0e4f57dd..bc57f73d2 100644 --- a/test/unit/app/controllers/permissions/helpers.js +++ b/test/helpers/permission-controller-helpers.js @@ -1,6 +1,7 @@ import { strict as assert } from 'assert'; +import stringify from 'fast-safe-stringify'; -import { noop } from './mocks'; +import { noop } from '../mocks/permission-controller'; /** * Grants the given permissions to the given origin, using the given permissions @@ -33,7 +34,7 @@ export function getRequestUserApprovalHelper(permController) { */ return (id, origin = 'defaultOrigin') => { return permController.permissions.requestUserApproval({ - metadata: { id, origin }, + metadata: { id, origin, type: 'NO_TYPE' }, }); }; } @@ -84,9 +85,9 @@ function _validateActivityEntry(entry, req, res, methodType, success) { assert.equal(entry.method, req.method); assert.equal(entry.origin, req.origin); assert.equal(entry.methodType, methodType); - assert.deepEqual( + assert.equal( entry.request, - req, + stringify(req, null, 2), 'entry.request should equal the request', ); @@ -104,7 +105,7 @@ function _validateActivityEntry(entry, req, res, methodType, success) { assert.equal(entry.success, success); assert.deepEqual( entry.response, - res, + stringify(res, null, 2), 'entry.response should equal the response', ); } else { diff --git a/test/helper.js b/test/helpers/setup-helper.js similarity index 100% rename from test/helper.js rename to test/helpers/setup-helper.js diff --git a/test/unit/app/controllers/permissions/mocks.js b/test/mocks/permission-controller.js similarity index 98% rename from test/unit/app/controllers/permissions/mocks.js rename to test/mocks/permission-controller.js index ddbf9ff41..1a2ee0d20 100644 --- a/test/unit/app/controllers/permissions/mocks.js +++ b/test/mocks/permission-controller.js @@ -3,13 +3,13 @@ import deepFreeze from 'deep-freeze-strict'; import { ApprovalController } from '@metamask/controllers'; -import _getRestrictedMethods from '../../../../../app/scripts/controllers/permissions/restrictedMethods'; +import _getRestrictedMethods from '../../app/scripts/controllers/permissions/restrictedMethods'; -import { CAVEAT_NAMES } from '../../../../../shared/constants/permissions'; +import { CAVEAT_NAMES } from '../../shared/constants/permissions'; import { CAVEAT_TYPES, NOTIFICATION_NAMES, -} from '../../../../../app/scripts/controllers/permissions/enums'; +} from '../../app/scripts/controllers/permissions/enums'; /** * README @@ -71,7 +71,6 @@ export function getPermControllerOpts() { return { approvals: new ApprovalController({ showApprovalRequest: noop, - defaultApprovalType: 'NO_TYPE', }), getKeyringAccounts: async () => [...keyringAccounts], getUnlockPromise: () => Promise.resolve(), diff --git a/test/setup.js b/test/setup.js index a4795b61b..57e79a557 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,6 +1,6 @@ require('@babel/register'); -require('./helper'); +require('./helpers/setup-helper'); window.SVGPathElement = window.SVGPathElement || { prototype: {} }; global.indexedDB = {}; diff --git a/test/unit/app/controllers/network/stubs.js b/test/stub/tx-meta-stub.js similarity index 94% rename from test/unit/app/controllers/network/stubs.js rename to test/stub/tx-meta-stub.js index 421e125a8..0af67dd20 100644 --- a/test/unit/app/controllers/network/stubs.js +++ b/test/stub/tx-meta-stub.js @@ -1,8 +1,7 @@ import { - TRANSACTION_CATEGORIES, TRANSACTION_STATUSES, TRANSACTION_TYPES, -} from '../../../../../shared/constants/transaction'; +} from '../../shared/constants/transaction'; export const txMetaStub = { firstRetryBlockNumber: '0x51a402', @@ -14,7 +13,7 @@ export const txMetaStub = { metamaskNetworkId: '4', status: TRANSACTION_STATUSES.UNAPPROVED, time: 1572395156620, - transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER, + type: TRANSACTION_TYPES.SENT_ETHER, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', gas: '0x5208', @@ -22,7 +21,6 @@ export const txMetaStub = { to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', value: '0x0', }, - type: TRANSACTION_TYPES.STANDARD, }, [ { @@ -196,7 +194,7 @@ export const txMetaStub = { status: TRANSACTION_STATUSES.SUBMITTED, submittedTime: 1572395158570, time: 1572395156620, - transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER, + type: TRANSACTION_TYPES.SENT_ETHER, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', gas: '0x5208', @@ -205,6 +203,5 @@ export const txMetaStub = { to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', value: '0x0', }, - type: TRANSACTION_TYPES.STANDARD, v: '0x2c', }; diff --git a/test/unit/balance-formatter.test.js b/test/unit-global/balance-formatter.test.js similarity index 100% rename from test/unit/balance-formatter.test.js rename to test/unit-global/balance-formatter.test.js diff --git a/test/unit/actions/config.test.js b/test/unit/actions/config.test.js deleted file mode 100644 index 7b31cd84f..000000000 --- a/test/unit/actions/config.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import assert from 'assert'; -import freeze from 'deep-freeze-strict'; -import reducers from '../../../ui/app/ducks'; -import * as actionConstants from '../../../ui/app/store/actionConstants'; -import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; - -describe('config view actions', function () { - const initialState = { - metamask: { - rpcUrl: 'foo', - frequentRpcList: [], - }, - appState: { - currentView: { - name: 'accounts', - }, - }, - }; - freeze(initialState); - - describe('SET_RPC_TARGET', function () { - it('sets the state.metamask.rpcUrl property of the state to the action.value', function () { - const action = { - type: actionConstants.SET_RPC_TARGET, - value: 'foo', - }; - - const result = reducers(initialState, action); - assert.equal(result.metamask.provider.type, NETWORK_TYPE_RPC); - assert.equal(result.metamask.provider.rpcUrl, 'foo'); - }); - }); -}); diff --git a/test/unit/actions/set_account_label.test.js b/test/unit/actions/set_account_label.test.js deleted file mode 100644 index 7bdb0d22c..000000000 --- a/test/unit/actions/set_account_label.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'assert'; -import freeze from 'deep-freeze-strict'; -import reducers from '../../../ui/app/ducks'; -import * as actionConstants from '../../../ui/app/store/actionConstants'; - -describe('SET_ACCOUNT_LABEL', function () { - it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { - const initialState = { - metamask: { - identities: { - foo: { - name: 'bar', - }, - }, - }, - }; - freeze(initialState); - - const action = { - type: actionConstants.SET_ACCOUNT_LABEL, - value: { - account: 'foo', - label: 'baz', - }, - }; - freeze(action); - - const resultingState = reducers(initialState, action); - assert.equal( - resultingState.metamask.identities.foo.name, - action.value.label, - ); - }); -}); diff --git a/test/unit/actions/set_selected_account.test.js b/test/unit/actions/set_selected_account.test.js deleted file mode 100644 index 9f9a79036..000000000 --- a/test/unit/actions/set_selected_account.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import assert from 'assert'; -import freeze from 'deep-freeze-strict'; -import reducers from '../../../ui/app/ducks'; -import * as actionConstants from '../../../ui/app/store/actionConstants'; - -describe('SHOW_ACCOUNT_DETAIL', function () { - it('updates metamask state', function () { - const initialState = { - metamask: { - selectedAddress: 'foo', - }, - }; - freeze(initialState); - - const action = { - type: actionConstants.SHOW_ACCOUNT_DETAIL, - value: 'bar', - }; - freeze(action); - - const resultingState = reducers(initialState, action); - assert.equal(resultingState.metamask.selectedAddress, action.value); - }); -}); diff --git a/test/unit/actions/tx.test.js b/test/unit/actions/tx.test.js deleted file mode 100644 index 120fcdb73..000000000 --- a/test/unit/actions/tx.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import assert from 'assert'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import * as actions from '../../../ui/app/store/actions'; -import * as actionConstants from '../../../ui/app/store/actionConstants'; -import { ROPSTEN_CHAIN_ID } from '../../../shared/constants/network'; - -const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); - -describe('tx confirmation screen', function () { - const txId = 1457634084250832; - const initialState = { - appState: {}, - metamask: { - unapprovedTxs: { - [txId]: { - id: txId, - status: 'unconfirmed', - time: 1457634084250, - }, - }, - provider: { - chainId: ROPSTEN_CHAIN_ID, - }, - }, - }; - - const store = mockStore(initialState); - - describe('cancelTx', function () { - it('creates COMPLETED_TX with the cancelled transaction ID', async function () { - actions._setBackgroundConnection({ - approveTransaction(_, cb) { - cb(new Error('An error!')); - }, - cancelTransaction(_, cb) { - cb(); - }, - getState(cb) { - cb(null, {}); - }, - }); - - await store.dispatch(actions.cancelTx({ id: txId })); - const storeActions = store.getActions(); - const completedTxAction = storeActions.find( - ({ type }) => type === actionConstants.COMPLETED_TX, - ); - const { id } = completedTxAction.value; - assert.equal(id, txId); - }); - }); -}); diff --git a/test/unit/actions/warning.test.js b/test/unit/actions/warning.test.js deleted file mode 100644 index 2ec35525b..000000000 --- a/test/unit/actions/warning.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import assert from 'assert'; -import freeze from 'deep-freeze-strict'; -import * as actions from '../../../ui/app/store/actions'; -import reducers from '../../../ui/app/ducks'; - -describe('action DISPLAY_WARNING', function () { - it('sets appState.warning to provided value', function () { - const initialState = { - appState: {}, - }; - freeze(initialState); - - const warningText = 'This is a sample warning message'; - - const action = actions.displayWarning(warningText); - const resultingState = reducers(initialState, action); - - assert.equal( - resultingState.appState.warning, - warningText, - 'warning text set', - ); - }); -}); diff --git a/test/unit/localhostState.js b/test/unit/localhostState.js deleted file mode 100644 index b758b9120..000000000 --- a/test/unit/localhostState.js +++ /dev/null @@ -1,23 +0,0 @@ -import { NETWORK_TYPE_RPC } from '../../shared/constants/network'; - -/** - * @typedef {Object} FirstTimeState - * @property {Object} config Initial configuration parameters - * @property {Object} NetworkController Network controller state - */ - -/** - * @type {FirstTimeState} - */ -const initialState = { - config: {}, - NetworkController: { - provider: { - type: NETWORK_TYPE_RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - }, - }, -}; - -export default initialState; diff --git a/test/unit/responsive/components/dropdown.test.js b/test/unit/responsive/components/dropdown.test.js deleted file mode 100644 index a7f83b07d..000000000 --- a/test/unit/responsive/components/dropdown.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { fireEvent } from '@testing-library/react'; -import sinon from 'sinon'; -import { renderWithProvider } from '../../../lib/render-helpers'; -import { Dropdown } from '../../../../ui/app/components/app/dropdowns/components/dropdown'; - -describe('Dropdown components', function () { - const mockState = { - metamask: {}, - }; - - const props = { - isOpen: true, - zIndex: 11, - onClickOutside: sinon.spy(), - style: { - position: 'absolute', - right: 0, - top: '36px', - }, - innerStyle: {}, - }; - - it('invokes click handler when item clicked', function () { - const store = configureMockStore()(mockState); - - const onClickSpy = sinon.spy(); - - const { getByText } = renderWithProvider( - -
  • Item 1
  • -
  • Item 2
  • -
    , - store, - ); - - const item1 = getByText(/Item 1/u); - fireEvent.click(item1); - - assert.ok(onClickSpy.calledOnce); - }); -}); diff --git a/ui/app/components/app/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/app/account-list-item/account-list-item-component.test.js similarity index 97% rename from ui/app/components/app/account-list-item/tests/account-list-item-component.test.js rename to ui/app/components/app/account-list-item/account-list-item-component.test.js index 558f9cebe..e4d894c58 100644 --- a/ui/app/components/app/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/components/app/account-list-item/account-list-item-component.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import * as utils from '../../../../helpers/utils/util'; -import Identicon from '../../../ui/identicon'; -import AccountListItem from '../account-list-item'; +import * as utils from '../../../helpers/utils/util'; +import Identicon from '../../ui/identicon'; +import AccountListItem from './account-list-item'; describe('AccountListItem Component', function () { let wrapper, propsMethodSpies, checksumAddressStub; diff --git a/ui/app/components/app/account-menu/tests/account-menu.test.js b/ui/app/components/app/account-menu/account-menu.test.js similarity index 98% rename from ui/app/components/app/account-menu/tests/account-menu.test.js rename to ui/app/components/app/account-menu/account-menu.test.js index 147cbb09e..bbc4675d3 100644 --- a/ui/app/components/app/account-menu/tests/account-menu.test.js +++ b/ui/app/components/app/account-menu/account-menu.test.js @@ -3,8 +3,8 @@ import React from 'react'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import AccountMenu from '..'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import AccountMenu from '.'; describe('Account Menu', function () { let wrapper; diff --git a/ui/app/components/app/alerts/unconnected-account-alert/tests/unconnected-account-alert.test.js b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js similarity index 94% rename from ui/app/components/app/alerts/unconnected-account-alert/tests/unconnected-account-alert.test.js rename to ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js index 57cae337e..e9219a145 100644 --- a/ui/app/components/app/alerts/unconnected-account-alert/tests/unconnected-account-alert.test.js +++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.test.js @@ -7,11 +7,11 @@ import thunk from 'redux-thunk'; import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../../../../test/lib/render-helpers'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; -import * as actions from '../../../../../store/actions'; -import UnconnectedAccountAlert from '..'; -import { KOVAN_CHAIN_ID } from '../../../../../../../shared/constants/network'; +import * as actions from '../../../../store/actions'; +import { KOVAN_CHAIN_ID } from '../../../../../../shared/constants/network'; +import UnconnectedAccountAlert from '.'; describe('Unconnected Account Alert', function () { const selectedAddress = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; diff --git a/ui/app/components/app/app-header/tests/app-header.test.js b/ui/app/components/app/app-header/app-header.test.js similarity index 94% rename from ui/app/components/app/app-header/tests/app-header.test.js rename to ui/app/components/app/app-header/app-header.test.js index 40045defc..bee0ee2e0 100644 --- a/ui/app/components/app/app-header/tests/app-header.test.js +++ b/ui/app/components/app/app-header/app-header.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import MetaFoxLogo from '../../../ui/metafox-logo'; -import AppHeader from '..'; -import NetworkDisplay from '../../network-display'; +import MetaFoxLogo from '../../ui/metafox-logo'; +import NetworkDisplay from '../network-display'; +import AppHeader from './app-header.container'; describe('App Header', function () { let wrapper; diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js similarity index 97% rename from ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js rename to ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js index c019f3a5b..ea958d529 100644 --- a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import ConfirmDetailRow from '../confirm-detail-row.component'; +import ConfirmDetailRow from './confirm-detail-row.component'; const propsMethodSpies = { onHeaderClick: sinon.spy(), diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/tests/confirm-page-container-header.component.test.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js similarity index 86% rename from ui/app/components/app/confirm-page-container/confirm-page-container-header/tests/confirm-page-container-header.component.test.js rename to ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js index 24da85a38..e75893572 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/tests/confirm-page-container-header.component.test.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js @@ -3,11 +3,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; import { Provider } from 'react-redux'; -import ConfirmPageContainerHeader from '../confirm-page-container-header.component'; -import configureStore from '../../../../../store/store'; -import testData from '../../../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../../.storybook/test-data'; +import ConfirmPageContainerHeader from './confirm-page-container-header.component'; -const util = require('../../../../../../../app/scripts/lib/util'); +const util = require('../../../../../../app/scripts/lib/util'); describe('Confirm Detail Row Component', function () { describe('render', function () { diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index c713935f9..e344bedd5 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -104,8 +104,7 @@ export default class ConfirmPageContainer extends Component { showAccountInHeader, origin, } = this.props; - const renderAssetImage = - contentComponent || (!contentComponent && !identiconAddress); + const renderAssetImage = contentComponent || !identiconAddress; return (
    diff --git a/ui/app/components/app/dropdowns/components/dropdown.js b/ui/app/components/app/dropdowns/dropdown.js similarity index 98% rename from ui/app/components/app/dropdowns/components/dropdown.js rename to ui/app/components/app/dropdowns/dropdown.js index 0c1a7f88f..79dc5be2d 100644 --- a/ui/app/components/app/dropdowns/components/dropdown.js +++ b/ui/app/components/app/dropdowns/dropdown.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import MenuDroppo from '../../menu-droppo'; +import MenuDroppo from '../menu-droppo'; export class Dropdown extends Component { render() { diff --git a/ui/app/components/app/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/dropdown.test.js similarity index 93% rename from ui/app/components/app/dropdowns/tests/dropdown.test.js rename to ui/app/components/app/dropdowns/dropdown.test.js index 63faf3341..e0a7a2ae4 100644 --- a/ui/app/components/app/dropdowns/tests/dropdown.test.js +++ b/ui/app/components/app/dropdowns/dropdown.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import { DropdownMenuItem } from '../components/dropdown'; +import { DropdownMenuItem } from './dropdown'; describe('Dropdown', function () { let wrapper; diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index c0d010517..e0742722d 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -16,7 +16,7 @@ import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import ColorIndicator from '../../ui/color-indicator'; import { COLORS, SIZES } from '../../../helpers/constants/design-system'; -import { Dropdown, DropdownMenuItem } from './components/dropdown'; +import { Dropdown, DropdownMenuItem } from './dropdown'; // classes from nodes of the toggle element. const notToggleElementClassnames = [ @@ -25,6 +25,7 @@ const notToggleElementClassnames = [ 'network-indicator', 'network-caret', 'network-component', + 'modal-container__footer-button', ]; const DROP_DOWN_MENU_ITEM_STYLE = { diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/network-dropdown.test.js similarity index 93% rename from ui/app/components/app/dropdowns/tests/network-dropdown.test.js rename to ui/app/components/app/dropdowns/network-dropdown.test.js index c7f872c6a..83bd713a7 100644 --- a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/app/dropdowns/network-dropdown.test.js @@ -2,10 +2,10 @@ import assert from 'assert'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import NetworkDropdown from '../network-dropdown'; -import { DropdownMenuItem } from '../components/dropdown'; -import ColorIndicator from '../../../ui/color-indicator'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import ColorIndicator from '../../ui/color-indicator'; +import NetworkDropdown from './network-dropdown'; +import { DropdownMenuItem } from './dropdown'; describe('Network Dropdown', function () { let wrapper; diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-input-component.test.js similarity index 98% rename from ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js rename to ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-input-component.test.js index 59c762619..bccef80ab 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-input-component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import AdvancedTabContent from '..'; +import AdvancedTabContent from './advanced-gas-inputs.container'; describe('Advanced Gas Inputs', function () { let wrapper, clock; diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js similarity index 94% rename from ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js rename to ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js index 4b1bdc21d..f98546649 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import shallow from '../../../../../../../lib/shallow-with-context'; -import AdvancedTabContent from '../advanced-tab-content.component'; +import shallow from '../../../../../../lib/shallow-with-context'; +import AdvancedTabContent from './advanced-tab-content.component'; describe('AdvancedTabContent Component', function () { let wrapper; diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content-component.test.js similarity index 90% rename from ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js rename to ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content-component.test.js index e0597a5a0..77d18c8cf 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content-component.test.js @@ -1,10 +1,10 @@ import assert from 'assert'; import React from 'react'; -import shallow from '../../../../../../../lib/shallow-with-context'; -import BasicTabContent from '../basic-tab-content.component'; -import GasPriceButtonGroup from '../../../gas-price-button-group'; -import Loading from '../../../../../ui/loading-screen'; -import { GAS_ESTIMATE_TYPES } from '../../../../../../helpers/constants/common'; +import shallow from '../../../../../../lib/shallow-with-context'; +import GasPriceButtonGroup from '../../gas-price-button-group'; +import Loading from '../../../../ui/loading-screen'; +import { GAS_ESTIMATE_TYPES } from '../../../../../helpers/constants/common'; +import BasicTabContent from './basic-tab-content.component'; const mockGasPriceButtonGroupProps = { buttonDataLoading: false, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js similarity index 97% rename from ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js rename to ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js index 1fede0503..ba0793740 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js @@ -1,12 +1,12 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import shallow from '../../../../../../lib/shallow-with-context'; -import GasModalPageContainer from '../gas-modal-page-container.component'; +import shallow from '../../../../../lib/shallow-with-context'; -import PageContainer from '../../../../ui/page-container'; +import PageContainer from '../../../ui/page-container'; -import { Tab } from '../../../../ui/tabs'; +import { Tab } from '../../../ui/tabs'; +import GasModalPageContainer from './gas-modal-page-container.component'; const mockBasicGasEstimates = { average: '20', diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js similarity index 61% rename from ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js rename to ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js index 0ba5aabbc..2e9008e8e 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js @@ -1,9 +1,7 @@ import assert from 'assert'; import proxyquire from 'proxyquire'; import sinon from 'sinon'; -import { TRANSACTION_STATUSES } from '../../../../../../../shared/constants/transaction'; -let mapStateToProps; let mapDispatchToProps; let mergeProps; @@ -23,10 +21,9 @@ const sendActionSpies = { hideGasButtonGroup: sinon.spy(), }; -proxyquire('../gas-modal-page-container.container.js', { +proxyquire('./gas-modal-page-container.container.js', { 'react-redux': { - connect: (ms, md, mp) => { - mapStateToProps = ms; + connect: (_, md, mp) => { mapDispatchToProps = md; mergeProps = mp; return () => ({}); @@ -48,225 +45,6 @@ proxyquire('../gas-modal-page-container.container.js', { }); describe('gas-modal-page-container container', function () { - describe('mapStateToProps()', function () { - it('should map the correct properties to props', function () { - const baseMockState = { - appState: { - modal: { - modalState: { - props: { - hideBasic: true, - txData: { - id: 34, - }, - }, - }, - }, - }, - metamask: { - send: { - gasLimit: '16', - gasPrice: '32', - amount: '64', - maxModeOn: false, - }, - currentCurrency: 'abc', - conversionRate: 50, - usdConversionRate: 123, - preferences: { - showFiatInTestnets: false, - }, - provider: { - type: 'mainnet', - chainId: '0x1', - }, - currentNetworkTxList: [ - { - id: 34, - txParams: { - gas: '0x1600000', - gasPrice: '0x3200000', - value: '0x640000000000000', - }, - }, - ], - }, - gas: { - basicEstimates: { - blockTime: 12, - safeLow: 2, - }, - customData: { - limit: 'aaaaaaaa', - price: 'ffffffff', - }, - priceAndTimeEstimates: [ - { gasprice: 3, expectedTime: 31 }, - { gasprice: 4, expectedTime: 62 }, - { gasprice: 5, expectedTime: 93 }, - { gasprice: 6, expectedTime: 124 }, - ], - }, - confirmTransaction: { - txData: { - txParams: { - gas: '0x1600000', - gasPrice: '0x3200000', - value: '0x640000000000000', - }, - }, - }, - }; - const baseExpectedResult = { - balance: '0x0', - isConfirm: true, - customGasPrice: 4.294967295, - customGasLimit: 2863311530, - newTotalFiat: '637.41', - conversionRate: 50, - customModalGasLimitInHex: 'aaaaaaaa', - customModalGasPriceInHex: 'ffffffff', - customPriceIsExcessive: false, - customGasTotal: 'aaaaaaa955555556', - customPriceIsSafe: true, - gasPriceButtonGroupProps: { - buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', - defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff', - gasButtonInfo: 'mockRenderableBasicEstimateData:4', - }, - hideBasic: true, - infoRowProps: { - originalTotalFiat: '637.41', - originalTotalEth: '12.748189 ETH', - newTotalFiat: '637.41', - newTotalEth: '12.748189 ETH', - sendAmount: '0.45036 ETH', - transactionFee: '12.297829 ETH', - }, - insufficientBalance: true, - isSpeedUp: false, - isRetry: false, - txId: 34, - isMainnet: true, - maxModeOn: false, - sendToken: null, - tokenBalance: '0x0', - transaction: { - id: 34, - }, - value: '0x640000000000000', - }; - const baseMockOwnProps = { transaction: { id: 34 } }; - const tests = [ - { - mockState: baseMockState, - expectedResult: baseExpectedResult, - mockOwnProps: baseMockOwnProps, - }, - { - mockState: { - ...baseMockState, - metamask: { - ...baseMockState.metamask, - balance: '0xfffffffffffffffffffff', - }, - }, - expectedResult: { - ...baseExpectedResult, - balance: '0xfffffffffffffffffffff', - insufficientBalance: false, - }, - mockOwnProps: baseMockOwnProps, - }, - { - mockState: baseMockState, - mockOwnProps: { - ...baseMockOwnProps, - transaction: { id: 34, status: TRANSACTION_STATUSES.SUBMITTED }, - }, - expectedResult: { - ...baseExpectedResult, - isSpeedUp: true, - transaction: { id: 34 }, - }, - }, - { - mockState: { - ...baseMockState, - metamask: { - ...baseMockState.metamask, - preferences: { - ...baseMockState.metamask.preferences, - showFiatInTestnets: false, - }, - provider: { - ...baseMockState.metamask.provider, - type: 'rinkeby', - chainId: '0x4', - }, - }, - }, - mockOwnProps: baseMockOwnProps, - expectedResult: { - ...baseExpectedResult, - infoRowProps: { - ...baseExpectedResult.infoRowProps, - newTotalFiat: '', - }, - isMainnet: false, - }, - }, - { - mockState: { - ...baseMockState, - metamask: { - ...baseMockState.metamask, - preferences: { - ...baseMockState.metamask.preferences, - showFiatInTestnets: true, - }, - provider: { - ...baseMockState.metamask.provider, - type: 'rinkeby', - chainId: '0x4', - }, - }, - }, - mockOwnProps: baseMockOwnProps, - expectedResult: { - ...baseExpectedResult, - isMainnet: false, - }, - }, - { - mockState: { - ...baseMockState, - metamask: { - ...baseMockState.metamask, - preferences: { - ...baseMockState.metamask.preferences, - showFiatInTestnets: true, - }, - provider: { - ...baseMockState.metamask.provider, - type: 'mainnet', - chainId: '0x1', - }, - }, - }, - mockOwnProps: baseMockOwnProps, - expectedResult: baseExpectedResult, - }, - ]; - - let result; - tests.forEach(({ mockState, mockOwnProps, expectedResult }) => { - result = mapStateToProps(mockState, mockOwnProps); - assert.deepStrictEqual(result, expectedResult); - }); - }); - }); - describe('mapDispatchToProps()', function () { let dispatchSpy; let mapDispatchToPropsObject; diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 136fdccc8..4d57502b2 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -61,7 +61,7 @@ const mapStateToProps = (state, ownProps) => { const { currentNetworkTxList, send } = state.metamask; const { modalState: { props: modalProps } = {} } = state.appState.modal || {}; const { txData = {} } = modalProps || {}; - const { transaction = {} } = ownProps; + const { transaction = {}, onSubmit } = ownProps; const selectedTransaction = currentNetworkTxList.find( ({ id }) => id === (transaction.id || txData.id), ); @@ -77,7 +77,8 @@ const mapStateToProps = (state, ownProps) => { value: sendToken ? '0x0' : send.amount, }; - const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = txParams; + const { gasPrice: currentGasPrice, gas: currentGasLimit } = txParams; + const value = ownProps.transaction?.txParams?.value || txParams.value; const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice; const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit || '0x5208'; @@ -175,6 +176,7 @@ const mapStateToProps = (state, ownProps) => { tokenBalance: getTokenBalance(state), conversionRate, value, + onSubmit, }; }; @@ -253,6 +255,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...otherDispatchProps, ...ownProps, onSubmit: (gasLimit, gasPrice) => { + if (ownProps.onSubmit) { + dispatchHideSidebar(); + dispatchCancelAndClose(); + ownProps.onSubmit(gasLimit, gasPrice); + return; + } if (isConfirm) { const updatedTx = { ...transaction, diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group-component.test.js similarity index 97% rename from ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js rename to ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group-component.test.js index 7bf35c1a8..04426e6b7 100644 --- a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js +++ b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group-component.test.js @@ -1,11 +1,11 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import shallow from '../../../../../../lib/shallow-with-context'; -import GasPriceButtonGroup from '../gas-price-button-group.component'; -import { GAS_ESTIMATE_TYPES } from '../../../../../helpers/constants/common'; +import shallow from '../../../../../lib/shallow-with-context'; +import { GAS_ESTIMATE_TYPES } from '../../../../helpers/constants/common'; -import ButtonGroup from '../../../../ui/button-group'; +import ButtonGroup from '../../../ui/button-group'; +import GasPriceButtonGroup from './gas-price-button-group.component'; describe('GasPriceButtonGroup Component', function () { let mockButtonPropsAndFlags; diff --git a/ui/app/components/app/info-box/tests/info-box.test.js b/ui/app/components/app/info-box/info-box.test.js similarity index 95% rename from ui/app/components/app/info-box/tests/info-box.test.js rename to ui/app/components/app/info-box/info-box.test.js index 04c5056d5..7b1107336 100644 --- a/ui/app/components/app/info-box/tests/info-box.test.js +++ b/ui/app/components/app/info-box/info-box.test.js @@ -3,7 +3,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import InfoBox from '..'; +import InfoBox from './info-box.component'; describe('InfoBox', function () { let wrapper; diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js index 0419aed42..9097ca4d8 100644 --- a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js @@ -21,7 +21,7 @@ export default class LoadingNetworkScreen extends PureComponent { setProviderArgs: PropTypes.array, setProviderType: PropTypes.func, rollbackToPreviousProvider: PropTypes.func, - isLoadingNetwork: PropTypes.bool, + isNetworkLoading: PropTypes.bool, }; componentDidMount = () => { @@ -99,9 +99,9 @@ export default class LoadingNetworkScreen extends PureComponent { }; cancelCall = () => { - const { isLoadingNetwork } = this.props; + const { isNetworkLoading } = this.props; - if (isLoadingNetwork) { + if (isNetworkLoading) { this.setState({ showErrorScreen: true }); } }; diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js index 5ebddae61..bfce78fbe 100644 --- a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network'; import * as actions from '../../../store/actions'; -import { getNetworkIdentifier } from '../../../selectors'; +import { getNetworkIdentifier, isNetworkLoading } from '../../../selectors'; import LoadingNetworkScreen from './loading-network-screen.component'; const mapStateToProps = (state) => { const { loadingMessage } = state.appState; - const { provider, network } = state.metamask; + const { provider } = state.metamask; const { rpcUrl, chainId, ticker, nickname, type } = provider; const setProviderArgs = @@ -15,7 +15,7 @@ const mapStateToProps = (state) => { : [provider.type]; return { - isLoadingNetwork: network === 'loading', + isNetworkLoading: isNetworkLoading(state), loadingMessage, setProviderArgs, provider, diff --git a/ui/app/components/app/menu-bar/tests/menu-bar.test.js b/ui/app/components/app/menu-bar/menu-bar.test.js similarity index 90% rename from ui/app/components/app/menu-bar/tests/menu-bar.test.js rename to ui/app/components/app/menu-bar/menu-bar.test.js index fbac945f2..a1bfba6d4 100644 --- a/ui/app/components/app/menu-bar/tests/menu-bar.test.js +++ b/ui/app/components/app/menu-bar/menu-bar.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import React from 'react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import MenuBar from '..'; -import { ROPSTEN_CHAIN_ID } from '../../../../../../shared/constants/network'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import { ROPSTEN_CHAIN_ID } from '../../../../../shared/constants/network'; +import MenuBar from './menu-bar'; const initState = { activeTab: {}, diff --git a/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/modal-content.component.test.js similarity index 96% rename from ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js rename to ui/app/components/app/modal/modal-content/modal-content.component.test.js index 485b2f951..5d6de2f38 100644 --- a/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js +++ b/ui/app/components/app/modal/modal-content/modal-content.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import ModalContent from '../modal-content.component'; +import ModalContent from './modal-content.component'; describe('ModalContent Component', function () { it('should render a title', function () { diff --git a/ui/app/components/app/modal/tests/modal.component.test.js b/ui/app/components/app/modal/modal.component.test.js similarity index 98% rename from ui/app/components/app/modal/tests/modal.component.test.js rename to ui/app/components/app/modal/modal.component.test.js index 8af9d394b..0e4ccc9e8 100644 --- a/ui/app/components/app/modal/tests/modal.component.test.js +++ b/ui/app/components/app/modal/modal.component.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import React from 'react'; import { mount, shallow } from 'enzyme'; import sinon from 'sinon'; -import Modal from '../modal.component'; -import Button from '../../../ui/button'; +import Button from '../../ui/button'; +import Modal from './modal.component'; describe('Modal Component', function () { it('should render a modal with a submit button', function () { diff --git a/ui/app/components/app/modals/tests/account-details-modal.test.js b/ui/app/components/app/modals/account-details-modal/account-details-modal.test.js similarity index 96% rename from ui/app/components/app/modals/tests/account-details-modal.test.js rename to ui/app/components/app/modals/account-details-modal/account-details-modal.test.js index 304b76d7c..1de8b1d23 100644 --- a/ui/app/components/app/modals/tests/account-details-modal.test.js +++ b/ui/app/components/app/modals/account-details-modal/account-details-modal.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import AccountDetailsModal from '../account-details-modal'; +import AccountDetailsModal from './account-details-modal.container'; describe('Account Details Modal', function () { let wrapper; diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js similarity index 83% rename from ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js rename to ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js index 5d4ca39e6..0630d1f08 100644 --- a/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component'; -import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display'; +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display'; +import CancelTransactionGasFee from './cancel-transaction-gas-fee.component'; describe('CancelTransactionGasFee Component', function () { it('should render', function () { diff --git a/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.test.js similarity index 92% rename from ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js rename to ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.test.js index 9d75d3dab..9cba4687c 100644 --- a/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import CancelTransaction from '../cancel-transaction.component'; -import CancelTransactionGasFee from '../cancel-transaction-gas-fee'; -import Modal from '../../../modal'; +import Modal from '../../modal'; +import CancelTransaction from './cancel-transaction.component'; +import CancelTransactionGasFee from './cancel-transaction-gas-fee'; describe('CancelTransaction Component', function () { const t = (key) => key; diff --git a/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js index 687986fa7..f05ecdf51 100644 --- a/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js @@ -1,47 +1,40 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; -import { multiplyCurrencies } from '../../../../helpers/utils/conversion-util'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; import { showModal, createCancelTransaction } from '../../../../store/actions'; -import { getHexGasTotal } from '../../../../helpers/utils/confirm-tx.util'; -import { addHexPrefix } from '../../../../../../app/scripts/lib/util'; import CancelTransaction from './cancel-transaction.component'; const mapStateToProps = (state, ownProps) => { const { metamask } = state; - const { transactionId, originalGasPrice } = ownProps; + const { + transactionId, + originalGasPrice, + newGasFee, + defaultNewGasPrice, + gasLimit, + } = ownProps; const { currentNetworkTxList } = metamask; const transaction = currentNetworkTxList.find( ({ id }) => id === transactionId, ); const transactionStatus = transaction ? transaction.status : ''; - const defaultNewGasPrice = addHexPrefix( - multiplyCurrencies(originalGasPrice, 1.1, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 10, - }), - ); - - const newGasFee = getHexGasTotal({ - gasPrice: defaultNewGasPrice, - gasLimit: '0x5208', - }); - return { transactionId, transactionStatus, originalGasPrice, defaultNewGasPrice, newGasFee, + gasLimit, }; }; const mapDispatchToProps = (dispatch) => { return { - createCancelTransaction: (txId, customGasPrice) => { - return dispatch(createCancelTransaction(txId, customGasPrice)); + createCancelTransaction: (txId, customGasPrice, customGasLimit) => { + return dispatch( + createCancelTransaction(txId, customGasPrice, customGasLimit), + ); }, showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })), @@ -49,7 +42,12 @@ const mapDispatchToProps = (dispatch) => { }; const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps; + const { + transactionId, + defaultNewGasPrice, + gasLimit, + ...restStateProps + } = stateProps; // eslint-disable-next-line no-shadow const { createCancelTransaction, ...restDispatchProps } = dispatchProps; @@ -58,7 +56,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...restDispatchProps, ...ownProps, createCancelTransaction: () => - createCancelTransaction(transactionId, defaultNewGasPrice), + createCancelTransaction(transactionId, defaultNewGasPrice, gasLimit), }; }; diff --git a/ui/app/components/app/modals/confirm-delete-network/tests/confirm-delete-network.test.js b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.test.js similarity index 95% rename from ui/app/components/app/modals/confirm-delete-network/tests/confirm-delete-network.test.js rename to ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.test.js index 28d7e8214..3984971fb 100644 --- a/ui/app/components/app/modals/confirm-delete-network/tests/confirm-delete-network.test.js +++ b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import ConfirmDeleteNetwork from '..'; +import ConfirmDeleteNetwork from './confirm-delete-network.container'; describe('Confirm Delete Network', function () { let wrapper; diff --git a/ui/app/components/app/modals/confirm-remove-account/tests/confirm-remove-account.test.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.test.js similarity index 96% rename from ui/app/components/app/modals/confirm-remove-account/tests/confirm-remove-account.test.js rename to ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.test.js index f070092c2..c73ce8458 100644 --- a/ui/app/components/app/modals/confirm-remove-account/tests/confirm-remove-account.test.js +++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.test.js @@ -5,7 +5,7 @@ import { Provider } from 'react-redux'; import sinon from 'sinon'; import configureStore from 'redux-mock-store'; import { mount } from 'enzyme'; -import ConfirmRemoveAccount from '..'; +import ConfirmRemoveAccount from './confirm-remove-account.container'; describe('Confirm Remove Account', function () { let wrapper; diff --git a/ui/app/components/app/modals/confirm-reset-account/tests/confirm-reset-account.test.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.test.js similarity index 94% rename from ui/app/components/app/modals/confirm-reset-account/tests/confirm-reset-account.test.js rename to ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.test.js index 38af4a5a3..1239ddf4e 100644 --- a/ui/app/components/app/modals/confirm-reset-account/tests/confirm-reset-account.test.js +++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import ConfirmResetAccount from '..'; +import ConfirmResetAccount from './confirm-reset-account.container'; describe('Confirm Reset Account', function () { let wrapper; diff --git a/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js b/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js index 9b86e2b51..553f25973 100644 --- a/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js +++ b/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.component.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { getNetworkDisplayName } from '../../../../../../app/scripts/controllers/network/util'; +import { NETWORK_TO_NAME_MAP } from '../../../../../../shared/constants/network'; import Button from '../../../ui/button'; export default class DepositEtherModal extends Component { @@ -10,7 +10,7 @@ export default class DepositEtherModal extends Component { }; static propTypes = { - network: PropTypes.string.isRequired, + chainId: PropTypes.string.isRequired, isTestnet: PropTypes.bool.isRequired, isMainnet: PropTypes.bool.isRequired, toWyre: PropTypes.func.isRequired, @@ -21,10 +21,6 @@ export default class DepositEtherModal extends Component { showAccountDetailModal: PropTypes.func.isRequired, }; - faucetRowText = (networkName) => { - return this.context.t('getEtherFromFaucet', [networkName]); - }; - goToAccountDetailsModal = () => { this.props.hideWarning(); this.props.hideModal(); @@ -89,14 +85,14 @@ export default class DepositEtherModal extends Component { render() { const { - network, + chainId, toWyre, address, toFaucet, isTestnet, isMainnet, } = this.props; - const networkName = getNetworkDisplayName(network); + const networkName = NETWORK_TO_NAME_MAP[chainId]; return (
    @@ -159,14 +155,15 @@ export default class DepositEtherModal extends Component { buttonLabel: this.context.t('viewAccount'), onButtonClick: () => this.goToAccountDetailsModal(), })} - {this.renderRow({ - logo: , - title: this.context.t('testFaucet'), - text: this.faucetRowText(networkName), - buttonLabel: this.context.t('getEther'), - onButtonClick: () => toFaucet(network), - hide: !isTestnet, - })} + {networkName && + this.renderRow({ + logo: , + title: this.context.t('testFaucet'), + text: this.context.t('getEtherFromFaucet', [networkName]), + buttonLabel: this.context.t('getEther'), + onButtonClick: () => toFaucet(chainId), + hide: !isTestnet, + })}
    diff --git a/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.container.js b/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.container.js index f4f5713c9..b393f4497 100644 --- a/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.container.js +++ b/ui/app/components/app/modals/deposit-ether-modal/deposit-ether-modal.container.js @@ -5,15 +5,20 @@ import { showModal, hideWarning, } from '../../../../store/actions'; -import { getIsTestnet, getIsMainnet } from '../../../../selectors/selectors'; +import { + getIsTestnet, + getIsMainnet, + getCurrentChainId, + getSelectedAddress, +} from '../../../../selectors/selectors'; import DepositEtherModal from './deposit-ether-modal.component'; function mapStateToProps(state) { return { - network: state.metamask.network, + chainId: getCurrentChainId(state), isTestnet: getIsTestnet(state), isMainnet: getIsMainnet(state), - address: state.metamask.selectedAddress, + address: getSelectedAddress(state), }; } @@ -31,7 +36,7 @@ function mapDispatchToProps(dispatch) { showAccountDetailModal: () => { dispatch(showModal({ name: 'ACCOUNT_DETAILS' })); }, - toFaucet: (network) => dispatch(buyEth({ network })), + toFaucet: (chainId) => dispatch(buyEth({ chainId })), }; } diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/tests/metametrics-opt-in-modal.test.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.test.js similarity index 92% rename from ui/app/components/app/modals/metametrics-opt-in-modal/tests/metametrics-opt-in-modal.test.js rename to ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.test.js index a34ad9905..4c0f60003 100644 --- a/ui/app/components/app/modals/metametrics-opt-in-modal/tests/metametrics-opt-in-modal.test.js +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import MetaMetricsOptIn from '..'; -import messages from '../../../../../../../app/_locales/en/messages.json'; +import messages from '../../../../../../app/_locales/en/messages.json'; +import MetaMetricsOptIn from './metametrics-opt-in-modal.container'; describe('MetaMetrics Opt In', function () { let wrapper; diff --git a/ui/app/components/app/modals/reject-transactions/tests/reject-transactions.test.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.test.js similarity index 94% rename from ui/app/components/app/modals/reject-transactions/tests/reject-transactions.test.js rename to ui/app/components/app/modals/reject-transactions/reject-transactions.test.js index 7988d2681..439faa3ab 100644 --- a/ui/app/components/app/modals/reject-transactions/tests/reject-transactions.test.js +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import RejectTransactionsModal from '..'; +import RejectTransactionsModal from './reject-transactions.container'; describe('Reject Transactions Model', function () { let wrapper; diff --git a/ui/app/components/app/modals/transaction-confirmed/tests/transaction-confirmed.test.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.test.js similarity index 90% rename from ui/app/components/app/modals/transaction-confirmed/tests/transaction-confirmed.test.js rename to ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.test.js index 5ca602785..e005a38d8 100644 --- a/ui/app/components/app/modals/transaction-confirmed/tests/transaction-confirmed.test.js +++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import TransactionConfirmed from '..'; +import TransactionConfirmed from './transaction-confirmed.container'; describe('Transaction Confirmed', function () { it('clicks ok to submit and hide modal', function () { diff --git a/ui/app/components/app/network-display/index.scss b/ui/app/components/app/network-display/index.scss index c07126b6f..fdfdc3302 100644 --- a/ui/app/components/app/network-display/index.scss +++ b/ui/app/components/app/network-display/index.scss @@ -6,6 +6,7 @@ border-radius: 4px; min-height: 25px; cursor: pointer; + user-select: none; &--disabled { cursor: not-allowed; diff --git a/ui/app/components/app/network-display/network-display.js b/ui/app/components/app/network-display/network-display.js index 65060abdf..ba83bb5c9 100644 --- a/ui/app/components/app/network-display/network-display.js +++ b/ui/app/components/app/network-display/network-display.js @@ -16,6 +16,7 @@ import { } from '../../../helpers/constants/design-system'; import Chip from '../../ui/chip/chip'; import { useI18nContext } from '../../../hooks/useI18nContext'; +import { isNetworkLoading } from '../../../selectors'; export default function NetworkDisplay({ colored, @@ -27,14 +28,14 @@ export default function NetworkDisplay({ targetNetwork, onClick, }) { + const networkIsLoading = useSelector(isNetworkLoading); const currentNetwork = useSelector((state) => ({ - network: state.metamask.network, nickname: state.metamask.provider.nickname, type: state.metamask.provider.type, })); const t = useI18nContext(); - const { network = '', nickname: networkNickname, type: networkType } = + const { nickname: networkNickname, type: networkType } = targetNetwork ?? currentNetwork; return ( @@ -45,7 +46,7 @@ export default function NetworkDisplay({ - + ); default: diff --git a/ui/app/components/app/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/sidebar.component.test.js similarity index 96% rename from ui/app/components/app/sidebars/tests/sidebars-component.test.js rename to ui/app/components/app/sidebars/sidebar.component.test.js index 6ce792716..d8d325e71 100644 --- a/ui/app/components/app/sidebars/tests/sidebars-component.test.js +++ b/ui/app/components/app/sidebars/sidebar.component.test.js @@ -3,9 +3,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; -import Sidebar from '../sidebar.component'; - -import CustomizeGas from '../../gas-customization/gas-modal-page-container'; +import CustomizeGas from '../gas-customization/gas-modal-page-container'; +import Sidebar from './sidebar.component'; const propsMethodSpies = { hideSidebar: sinon.spy(), diff --git a/ui/app/components/app/signature-request/tests/signature-request.test.js b/ui/app/components/app/signature-request/signature-request.component.test.js similarity index 86% rename from ui/app/components/app/signature-request/tests/signature-request.test.js rename to ui/app/components/app/signature-request/signature-request.component.test.js index 261576f16..fb2526c20 100644 --- a/ui/app/components/app/signature-request/tests/signature-request.test.js +++ b/ui/app/components/app/signature-request/signature-request.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; -import shallow from '../../../../../lib/shallow-with-context'; -import SignatureRequest from '../signature-request.component'; +import shallow from '../../../../lib/shallow-with-context'; +import SignatureRequest from './signature-request.component'; describe('Signature Request Component', function () { describe('render', function () { diff --git a/ui/app/components/app/tests/signature-request.test.js b/ui/app/components/app/signature-request/signature-request.container.test.js similarity index 97% rename from ui/app/components/app/tests/signature-request.test.js rename to ui/app/components/app/signature-request/signature-request.container.test.js index 700b99a91..35453d513 100644 --- a/ui/app/components/app/tests/signature-request.test.js +++ b/ui/app/components/app/signature-request/signature-request.container.test.js @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; import { mountWithRouter } from '../../../../../test/lib/render-helpers'; -import SignatureRequest from '../signature-request'; +import SignatureRequest from './signature-request.container'; describe('Signature Request', function () { let wrapper; diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.test.js similarity index 98% rename from ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js rename to ui/app/components/app/transaction-activity-log/transaction-activity-log.component.test.js index 149ebd4c4..8fac60422 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import TransactionActivityLog from '../transaction-activity-log.component'; +import TransactionActivityLog from './transaction-activity-log.component'; describe('TransactionActivityLog Component', function () { it('should render properly', function () { diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.test.js similarity index 96% rename from ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js rename to ui/app/components/app/transaction-activity-log/transaction-activity-log.container.test.js index 612a0f046..6ca0d8787 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.test.js @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; let mapStateToProps; -proxyquire('../transaction-activity-log.container.js', { +proxyquire('./transaction-activity-log.container.js', { 'react-redux': { connect: (ms) => { mapStateToProps = ms; diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.test.js similarity index 98% rename from ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js rename to ui/app/components/app/transaction-activity-log/transaction-activity-log.util.test.js index 8421dca96..2d9987850 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.test.js @@ -2,15 +2,15 @@ import assert from 'assert'; import { ROPSTEN_CHAIN_ID, ROPSTEN_NETWORK_ID, -} from '../../../../../../shared/constants/network'; +} from '../../../../../shared/constants/network'; import { TRANSACTION_STATUSES, TRANSACTION_TYPES, -} from '../../../../../../shared/constants/transaction'; +} from '../../../../../shared/constants/transaction'; import { combineTransactionHistories, getActivities, -} from '../transaction-activity-log.util'; +} from './transaction-activity-log.util'; describe('TransactionActivityLog utils', function () { describe('combineTransactionHistories', function () { diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.test.js similarity index 91% rename from ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js rename to ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.test.js index 70929e527..7d2aff7a6 100644 --- a/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import TransactionBreakdownRow from '../transaction-breakdown-row.component'; -import Button from '../../../../ui/button'; +import Button from '../../../ui/button'; +import TransactionBreakdownRow from './transaction-breakdown-row.component'; describe('TransactionBreakdownRow Component', function () { it('should render text properly', function () { diff --git a/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.test.js similarity index 83% rename from ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js rename to ui/app/components/app/transaction-breakdown/transaction-breakdown.component.test.js index 1e4b89a8f..1d5dcf0f5 100644 --- a/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import TransactionBreakdown from '../transaction-breakdown.component'; -import { TRANSACTION_STATUSES } from '../../../../../../shared/constants/transaction'; +import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; +import TransactionBreakdown from './transaction-breakdown.component'; describe('TransactionBreakdown Component', function () { it('should render properly', function () { diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js index 1ab3b4818..d8f39cdd7 100644 --- a/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js @@ -6,19 +6,16 @@ import { } from '../../../selectors'; import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; import { sumHexes } from '../../../helpers/utils/transactions.util'; -import { TRANSACTION_CATEGORIES } from '../../../../../shared/constants/transaction'; import TransactionBreakdown from './transaction-breakdown.component'; const mapStateToProps = (state, ownProps) => { - const { transaction, transactionCategory } = ownProps; + const { transaction, isTokenApprove } = ownProps; const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {}, } = transaction; const { showFiatInTestnets } = getPreferences(state); const isMainnet = getIsMainnet(state); - const isTokenApprove = - transactionCategory === TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE; const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas; diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 3c93ff921..c1256d7cb 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -10,6 +10,7 @@ import Tooltip from '../../ui/tooltip'; import Copy from '../../ui/icon/copy-icon.component'; import Popover from '../../ui/popover'; import { getBlockExplorerUrlForTx } from '../../../../../shared/modules/transaction.utils'; +import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { @@ -156,7 +157,7 @@ export default class TransactionListItemDetails extends PureComponent { } = this.props; const { primaryTransaction: transaction, - initialTransaction: { transactionCategory }, + initialTransaction: { type }, } = transactionGroup; const { hash } = transaction; @@ -255,7 +256,7 @@ export default class TransactionListItemDetails extends PureComponent {
    - {t('speedUp')} + {hasCancelled ? t('speedUpCancellation') : t('speedUp')} ); - }, [shouldShowSpeedUp, isUnapproved, t, isPending, retryTransaction]); + }, [ + shouldShowSpeedUp, + isUnapproved, + t, + isPending, + retryTransaction, + hasCancelled, + cancelTransaction, + ]); return ( <> diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 7f20e03a8..389a76025 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -11,7 +11,7 @@ import TransactionListItem from '../transaction-list-item'; import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../../shared/constants/swaps'; -import { TRANSACTION_CATEGORIES } from '../../../../../shared/constants/transaction'; +import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction'; const PAGE_INCREMENT = 10; @@ -36,15 +36,11 @@ const getTransactionGroupRecipientAddressFilter = ( }; const tokenTransactionFilter = ({ - initialTransaction: { - transactionCategory, - destinationTokenSymbol, - sourceTokenSymbol, - }, + initialTransaction: { type, destinationTokenSymbol, sourceTokenSymbol }, }) => { - if (TOKEN_CATEGORY_HASH[transactionCategory]) { + if (TOKEN_CATEGORY_HASH[type]) { return false; - } else if (transactionCategory === TRANSACTION_CATEGORIES.SWAP) { + } else if (type === TRANSACTION_TYPES.SWAP) { return destinationTokenSymbol === 'ETH' || sourceTokenSymbol === 'ETH'; } return true; diff --git a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/app/transaction-status/transaction-status.component.test.js similarity index 91% rename from ui/app/components/app/transaction-status/tests/transaction-status.component.test.js rename to ui/app/components/app/transaction-status/transaction-status.component.test.js index 2300081de..f824160b5 100644 --- a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js +++ b/ui/app/components/app/transaction-status/transaction-status.component.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import React from 'react'; import { mount } from 'enzyme'; import sinon from 'sinon'; -import * as i18nHook from '../../../../hooks/useI18nContext'; -import TransactionStatus from '../transaction-status.component'; -import Tooltip from '../../../ui/tooltip'; +import * as i18nHook from '../../../hooks/useI18nContext'; +import Tooltip from '../../ui/tooltip'; +import TransactionStatus from './transaction-status.component'; describe('TransactionStatus Component', function () { before(function () { diff --git a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js similarity index 80% rename from ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js rename to ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js index 7b57d9265..52e896ce6 100644 --- a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js @@ -2,10 +2,10 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component'; -import CurrencyDisplay from '../../../ui/currency-display'; -import * as currencyHook from '../../../../hooks/useCurrencyDisplay'; -import * as currencyPrefHook from '../../../../hooks/useUserPreferencedCurrency'; +import CurrencyDisplay from '../../ui/currency-display'; +import * as currencyHook from '../../../hooks/useCurrencyDisplay'; +import * as currencyPrefHook from '../../../hooks/useUserPreferencedCurrency'; +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'; describe('UserPreferencedCurrencyDisplay Component', function () { describe('rendering', function () { diff --git a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js similarity index 87% rename from ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js rename to ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js index f0c704459..b52b931a7 100644 --- a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component'; -import CurrencyInput from '../../../ui/currency-input'; +import CurrencyInput from '../../ui/currency-input'; +import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'; describe('UserPreferencedCurrencyInput Component', function () { describe('rendering', function () { diff --git a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js similarity index 91% rename from ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js rename to ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js index 40234c9c5..b0f096550 100644 --- a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; let mapStateToProps; -proxyquire('../user-preferenced-currency-input.container.js', { +proxyquire('./user-preferenced-currency-input.container.js', { 'react-redux': { connect: (ms) => { mapStateToProps = ms; diff --git a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js similarity index 88% rename from ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js rename to ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js index 3fc20f657..98ca3760f 100644 --- a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import UserPreferencedTokenInput from '../user-preferenced-token-input.component'; -import TokenInput from '../../../ui/token-input'; +import TokenInput from '../../ui/token-input'; +import UserPreferencedTokenInput from './user-preferenced-token-input.component'; describe('UserPreferencedCurrencyInput Component', function () { describe('rendering', function () { diff --git a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js similarity index 91% rename from ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js rename to ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js index 9ff33f0f5..f91d2132b 100644 --- a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; let mapStateToProps; -proxyquire('../user-preferenced-token-input.container.js', { +proxyquire('./user-preferenced-token-input.container.js', { 'react-redux': { connect: (ms) => { mapStateToProps = ms; diff --git a/ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js b/ui/app/components/ui/account-mismatch-warning/acccount-mismatch-warning.component.test.js similarity index 84% rename from ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js rename to ui/app/components/ui/account-mismatch-warning/acccount-mismatch-warning.component.test.js index febddd53b..7e3abbae2 100644 --- a/ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js +++ b/ui/app/components/ui/account-mismatch-warning/acccount-mismatch-warning.component.test.js @@ -3,9 +3,9 @@ import React from 'react'; import * as reactRedux from 'react-redux'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import InfoIcon from '../../icon/info-icon.component'; -import AccountMismatchWarning from '../account-mismatch-warning.component'; -import { getSelectedAccount } from '../../../../selectors'; +import InfoIcon from '../icon/info-icon.component'; +import { getSelectedAccount } from '../../../selectors'; +import AccountMismatchWarning from './account-mismatch-warning.component'; describe('AccountMismatchWarning', function () { before(function () { diff --git a/ui/app/components/ui/alert/tests/alert.test.js b/ui/app/components/ui/alert/index.test.js similarity index 97% rename from ui/app/components/ui/alert/tests/alert.test.js rename to ui/app/components/ui/alert/index.test.js index 64c519e0e..ead7edaea 100644 --- a/ui/app/components/ui/alert/tests/alert.test.js +++ b/ui/app/components/ui/alert/index.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import Alert from '..'; +import Alert from '.'; describe('Alert', function () { let wrapper; diff --git a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js similarity index 93% rename from ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js rename to ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js index c2f7af83b..451d1af4b 100644 --- a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js +++ b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import Breadcrumbs from '../breadcrumbs.component'; +import Breadcrumbs from './breadcrumbs.component'; describe('Breadcrumbs Component', function () { it('should render with the correct colors', function () { diff --git a/ui/app/components/ui/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/button-group-component.test.js similarity index 98% rename from ui/app/components/ui/button-group/tests/button-group-component.test.js rename to ui/app/components/ui/button-group/button-group-component.test.js index a7e82cc45..5cbae1292 100644 --- a/ui/app/components/ui/button-group/tests/button-group-component.test.js +++ b/ui/app/components/ui/button-group/button-group-component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import ButtonGroup from '../button-group.component'; +import ButtonGroup from './button-group.component'; describe('ButtonGroup Component', function () { let wrapper; diff --git a/ui/app/components/ui/card/tests/card.component.test.js b/ui/app/components/ui/card/card.component.test.js similarity index 94% rename from ui/app/components/ui/card/tests/card.component.test.js rename to ui/app/components/ui/card/card.component.test.js index 2f1d4b43c..98a1d102b 100644 --- a/ui/app/components/ui/card/tests/card.component.test.js +++ b/ui/app/components/ui/card/card.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import Card from '../card.component'; +import Card from './card.component'; describe('Card Component', function () { it('should render a card with a title and child element', function () { diff --git a/ui/app/components/ui/confusable/test/confusable.component.test.js b/ui/app/components/ui/confusable/confusable.component.test.js similarity index 94% rename from ui/app/components/ui/confusable/test/confusable.component.test.js rename to ui/app/components/ui/confusable/confusable.component.test.js index d3166ccd7..79f4074a6 100644 --- a/ui/app/components/ui/confusable/test/confusable.component.test.js +++ b/ui/app/components/ui/confusable/confusable.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import Confusable from '../confusable.component'; +import Confusable from './confusable.component'; describe('Confusable component', function () { it('should detect zero-width unicode', function () { diff --git a/ui/app/components/ui/currency-display/tests/currency-display.component.test.js b/ui/app/components/ui/currency-display/currency-display.component.test.js similarity index 94% rename from ui/app/components/ui/currency-display/tests/currency-display.component.test.js rename to ui/app/components/ui/currency-display/currency-display.component.test.js index 295d4c463..ce81e58f1 100644 --- a/ui/app/components/ui/currency-display/tests/currency-display.component.test.js +++ b/ui/app/components/ui/currency-display/currency-display.component.test.js @@ -3,7 +3,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; import * as reactRedux from 'react-redux'; -import CurrencyDisplay from '../currency-display.component'; +import CurrencyDisplay from './currency-display.component'; describe('CurrencyDisplay Component', function () { beforeEach(function () { diff --git a/ui/app/components/ui/currency-input/tests/currency-input.component.test.js b/ui/app/components/ui/currency-input/currency-input.component.test.js similarity index 98% rename from ui/app/components/ui/currency-input/tests/currency-input.component.test.js rename to ui/app/components/ui/currency-input/currency-input.component.test.js index f47154b5c..aa233dc17 100644 --- a/ui/app/components/ui/currency-input/tests/currency-input.component.test.js +++ b/ui/app/components/ui/currency-input/currency-input.component.test.js @@ -5,9 +5,9 @@ import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; -import CurrencyInput from '../currency-input.component'; -import UnitInput from '../../unit-input'; -import CurrencyDisplay from '../../currency-display'; +import UnitInput from '../unit-input'; +import CurrencyDisplay from '../currency-display'; +import CurrencyInput from './currency-input.component'; describe('CurrencyInput Component', function () { describe('rendering', function () { diff --git a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/currency-input.container.test.js similarity index 98% rename from ui/app/components/ui/currency-input/tests/currency-input.container.test.js rename to ui/app/components/ui/currency-input/currency-input.container.test.js index a0858288a..3748d103d 100644 --- a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js +++ b/ui/app/components/ui/currency-input/currency-input.container.test.js @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; let mapStateToProps, mergeProps; -proxyquire('../currency-input.container.js', { +proxyquire('./currency-input.container.js', { 'react-redux': { connect: (ms, _, mp) => { mapStateToProps = ms; diff --git a/ui/app/components/ui/error-message/tests/error-message.component.test.js b/ui/app/components/ui/error-message/error-message.component.test.js similarity index 95% rename from ui/app/components/ui/error-message/tests/error-message.component.test.js rename to ui/app/components/ui/error-message/error-message.component.test.js index a6c2d4ac3..7fc75472a 100644 --- a/ui/app/components/ui/error-message/tests/error-message.component.test.js +++ b/ui/app/components/ui/error-message/error-message.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import ErrorMessage from '../error-message.component'; +import ErrorMessage from './error-message.component'; describe('ErrorMessage Component', function () { const t = (key) => `translate ${key}`; diff --git a/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js b/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.test.js similarity index 92% rename from ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js rename to ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.test.js index 30bff9fce..c10cedeb8 100644 --- a/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js +++ b/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; -import HexToDecimal from '../hex-to-decimal.component'; +import HexToDecimal from './hex-to-decimal.component'; describe('HexToDecimal Component', function () { it('should render a prefixed hex as a decimal with a className', function () { diff --git a/ui/app/components/ui/identicon/identicon.component.js b/ui/app/components/ui/identicon/identicon.component.js index c999b6354..bad826dc9 100644 --- a/ui/app/components/ui/identicon/identicon.component.js +++ b/ui/app/components/ui/identicon/identicon.component.js @@ -101,7 +101,10 @@ export default class Identicon extends PureComponent { } return ( -
    +
    ); } } diff --git a/ui/app/components/ui/identicon/tests/identicon.component.test.js b/ui/app/components/ui/identicon/identicon.component.test.js similarity index 96% rename from ui/app/components/ui/identicon/tests/identicon.component.test.js rename to ui/app/components/ui/identicon/identicon.component.test.js index a482037e0..03b40ce34 100644 --- a/ui/app/components/ui/identicon/tests/identicon.component.test.js +++ b/ui/app/components/ui/identicon/identicon.component.test.js @@ -3,7 +3,7 @@ import React from 'react'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; import { mount } from 'enzyme'; -import Identicon from '../identicon.component'; +import Identicon from './identicon.component'; describe('Identicon', function () { const state = { diff --git a/ui/app/components/ui/list-item/tests/list-item.test.js b/ui/app/components/ui/list-item/list-item.component.test.js similarity index 93% rename from ui/app/components/ui/list-item/tests/list-item.test.js rename to ui/app/components/ui/list-item/list-item.component.test.js index 488ee9039..7936027bf 100644 --- a/ui/app/components/ui/list-item/tests/list-item.test.js +++ b/ui/app/components/ui/list-item/list-item.component.test.js @@ -2,9 +2,9 @@ import assert from 'assert'; import { shallow } from 'enzyme'; import React from 'react'; import Sinon from 'sinon'; -import ListItem from '../list-item.component'; -import Preloader from '../../icon/preloader/preloader-icon.component'; -import Send from '../../icon/send-icon.component'; +import Preloader from '../icon/preloader/preloader-icon.component'; +import Send from '../icon/send-icon.component'; +import ListItem from './list-item.component'; const TITLE = 'Hello World'; const SUBTITLE =

    I am a list item

    ; diff --git a/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js b/ui/app/components/ui/metafox-logo/metafox-logo.component.test.js similarity index 96% rename from ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js rename to ui/app/components/ui/metafox-logo/metafox-logo.component.test.js index 3c0f725a4..eb929e4fc 100644 --- a/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js +++ b/ui/app/components/ui/metafox-logo/metafox-logo.component.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import React from 'react'; import { mount } from 'enzyme'; -import MetaFoxLogo from '..'; +import MetaFoxLogo from '.'; describe('MetaFoxLogo', function () { it('sets icon height and width to 42 by default', function () { diff --git a/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js similarity index 96% rename from ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js rename to ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js index 56c798f65..8ca003142 100644 --- a/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js +++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import Button from '../../../button'; -import PageFooter from '../page-container-footer.component'; +import Button from '../../button'; +import PageFooter from './page-container-footer.component'; describe('Page Footer', function () { let wrapper; diff --git a/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js similarity index 97% rename from ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js rename to ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js index 1c96ee0e5..e465f28e8 100644 --- a/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js +++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import PageContainerHeader from '../page-container-header.component'; +import PageContainerHeader from './page-container-header.component'; describe('Page Container Header', function () { let wrapper, style, onBackButtonClick, onClose; diff --git a/ui/app/components/ui/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js index 49e6ca785..fffec3ab7 100644 --- a/ui/app/components/ui/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js @@ -88,6 +88,9 @@ const getMaterialThemeInputProps = ({ dir, classes: { materialLabel, materialFocused, materialError, materialUnderline }, startAdornment, + min, + max, + autoComplete, }) => ({ InputLabelProps: { classes: { @@ -103,6 +106,9 @@ const getMaterialThemeInputProps = ({ }, inputProps: { dir, + min, + max, + autoComplete, }, }, }); @@ -116,6 +122,9 @@ const getMaterialWhitePaddedThemeInputProps = ({ materialWhitePaddedUnderline, }, startAdornment, + min, + max, + autoComplete, }) => ({ InputProps: { startAdornment, @@ -127,6 +136,9 @@ const getMaterialWhitePaddedThemeInputProps = ({ }, inputProps: { dir, + min, + max, + autoComplete, }, }, }); @@ -145,6 +157,9 @@ const getBorderedThemeInputProps = ({ }, largeLabel, startAdornment, + min, + max, + autoComplete, }) => ({ InputLabelProps: { shrink: true, @@ -165,6 +180,9 @@ const getBorderedThemeInputProps = ({ }, inputProps: { dir, + min, + max, + autoComplete, }, }, }); @@ -182,6 +200,9 @@ const TextField = ({ startAdornment, largeLabel, dir, + min, + max, + autoComplete, ...textFieldProps }) => { const inputProps = themeToInputProps[theme]({ @@ -189,6 +210,9 @@ const TextField = ({ startAdornment, largeLabel, dir, + min, + max, + autoComplete, }); return ( @@ -214,6 +238,9 @@ TextField.propTypes = { theme: PropTypes.oneOf(['bordered', 'material', 'material-white-padded']), startAdornment: PropTypes.element, largeLabel: PropTypes.bool, + min: PropTypes.number, + max: PropTypes.number, + autoComplete: PropTypes.string, }; export default withStyles(styles)(TextField); diff --git a/ui/app/components/ui/token-input/tests/token-input.component.test.js b/ui/app/components/ui/token-input/token-input.component.test.js similarity index 98% rename from ui/app/components/ui/token-input/tests/token-input.component.test.js rename to ui/app/components/ui/token-input/token-input.component.test.js index 479d63b0e..59f4af171 100644 --- a/ui/app/components/ui/token-input/tests/token-input.component.test.js +++ b/ui/app/components/ui/token-input/token-input.component.test.js @@ -5,9 +5,9 @@ import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; -import TokenInput from '../token-input.component'; -import UnitInput from '../../unit-input'; -import CurrencyDisplay from '../../currency-display'; +import UnitInput from '../unit-input'; +import CurrencyDisplay from '../currency-display'; +import TokenInput from './token-input.component'; describe('TokenInput Component', function () { const t = (key) => `translate ${key}`; diff --git a/ui/app/components/ui/unit-input/tests/unit-input.component.test.js b/ui/app/components/ui/unit-input/unit-input.component.test.js similarity index 98% rename from ui/app/components/ui/unit-input/tests/unit-input.component.test.js rename to ui/app/components/ui/unit-input/unit-input.component.test.js index 032b9b76e..99b0ad4a7 100644 --- a/ui/app/components/ui/unit-input/tests/unit-input.component.test.js +++ b/ui/app/components/ui/unit-input/unit-input.component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; -import UnitInput from '../unit-input.component'; +import UnitInput from './unit-input.component'; describe('UnitInput Component', function () { describe('rendering', function () { diff --git a/test/unit/ui/app/reducers/app.test.js b/ui/app/ducks/app/app.test.js similarity index 98% rename from test/unit/ui/app/reducers/app.test.js rename to ui/app/ducks/app/app.test.js index f95060e32..8cc8d4204 100644 --- a/test/unit/ui/app/reducers/app.test.js +++ b/ui/app/ducks/app/app.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import reduceApp from '../../../../../ui/app/ducks/app/app'; -import * as actionConstants from '../../../../../ui/app/store/actionConstants'; +import * as actionConstants from '../../store/actionConstants'; +import reduceApp from './app'; const actions = actionConstants; diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 31c808877..7569f0ccb 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -7,7 +7,6 @@ export default function reduceMetamask(state = {}, action) { isInitialized: false, isUnlocked: false, isAccountMenuOpen: false, - rpcUrl: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, frequentRpcList: [], diff --git a/test/unit/ui/app/reducers/metamask.test.js b/ui/app/ducks/metamask/metamask.test.js similarity index 98% rename from test/unit/ui/app/reducers/metamask.test.js rename to ui/app/ducks/metamask/metamask.test.js index faf765b86..de56aacc2 100644 --- a/test/unit/ui/app/reducers/metamask.test.js +++ b/ui/app/ducks/metamask/metamask.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; -import reduceMetamask from '../../../../../ui/app/ducks/metamask/metamask'; -import * as actionConstants from '../../../../../ui/app/store/actionConstants'; +import * as actionConstants from '../../store/actionConstants'; +import reduceMetamask from './metamask'; describe('MetaMask Reducers', function () { it('init state', function () { diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 06e4c759b..1040c358e 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -56,7 +56,7 @@ import { SWAP_FAILED_ERROR, SWAPS_FETCH_ORDER_CONFLICT, } from '../../../../shared/constants/swaps'; -import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'; +import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; const GAS_PRICES_LOADING_STATES = { INITIAL: 'INITIAL', @@ -417,6 +417,7 @@ export const fetchQuotesAndSetQuoteState = ( let destinationTokenAddedForSwap = false; if ( + toTokenAddress && toTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[toTokenAddress] ) { @@ -432,6 +433,7 @@ export const fetchQuotesAndSetQuoteState = ( ); } if ( + fromTokenAddress && fromTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[fromTokenAddress] && fromTokenBalance && @@ -673,7 +675,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { updateTransaction( { ...approveTxMeta, - transactionCategory: TRANSACTION_CATEGORIES.SWAP_APPROVAL, + type: TRANSACTION_TYPES.SWAP_APPROVAL, sourceTokenSymbol: sourceTokenInfo.symbol, }, true, @@ -714,7 +716,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { ...tradeTxMeta, sourceTokenSymbol: sourceTokenInfo.symbol, destinationTokenSymbol: destinationTokenInfo.symbol, - transactionCategory: TRANSACTION_CATEGORIES.SWAP, + type: TRANSACTION_TYPES.SWAP, destinationTokenDecimals: destinationTokenInfo.decimals, destinationTokenAddress: destinationTokenInfo.address, swapMetaData, diff --git a/ui/app/helpers/constants/available-conversions.json b/ui/app/helpers/constants/available-conversions.json index 913e8795f..300bf0146 100644 --- a/ui/app/helpers/constants/available-conversions.json +++ b/ui/app/helpers/constants/available-conversions.json @@ -19,6 +19,10 @@ "code": "inr", "name": "Indian Rupee" }, + { + "code": "nzd", + "name": "New Zealand Dollar" + }, { "code": "php", "name": "Philippine Peso" diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js index f349b3ae6..288d497e2 100644 --- a/ui/app/helpers/constants/transactions.js +++ b/ui/app/helpers/constants/transactions.js @@ -1,5 +1,5 @@ import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; @@ -15,7 +15,7 @@ export const PRIORITY_STATUS_HASH = { }; export const TOKEN_CATEGORY_HASH = { - [TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE]: true, - [TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER]: true, - [TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM]: true, + [TRANSACTION_TYPES.TOKEN_METHOD_APPROVE]: true, + [TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER]: true, + [TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM]: true, }; diff --git a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.test.js similarity index 96% rename from ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js rename to ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.test.js index 79339b4ad..f47269a2e 100644 --- a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js +++ b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import configureMockStore from 'redux-mock-store'; import { mount } from 'enzyme'; import React from 'react'; -import withModalProps from '../with-modal-props'; +import withModalProps from './with-modal-props'; const mockState = { appState: { diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index ad2af9162..99ce94fe5 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -5,10 +5,9 @@ import log from 'loglevel'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_GROUP_STATUSES, TRANSACTION_STATUSES, - TRANSACTION_TYPES, } from '../../../../shared/constants/transaction'; import fetchWithCache from './fetch-with-cache'; @@ -112,15 +111,15 @@ export function getFourBytePrefix(data = '') { /** * Given an transaction category, returns a boolean which indicates whether the transaction is calling an erc20 token method * - * @param {string} transactionCategory - The category of transaction being evaluated + * @param {TRANSACTION_TYPES[keyof TRANSACTION_TYPES]} type - The type of transaction being evaluated * @returns {boolean} whether the transaction is calling an erc20 token method */ -export function isTokenMethodAction(transactionCategory) { +export function isTokenMethodAction(type) { return [ - TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, - TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE, - TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM, - ].includes(transactionCategory); + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, + TRANSACTION_TYPES.TOKEN_METHOD_APPROVE, + TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM, + ].includes(type); } export function getLatestSubmittedTxWithNonce( @@ -197,39 +196,37 @@ export function getStatusKey(transaction) { * * This will throw an error if the transaction category is unrecognized and no default is provided. * @param {function} t - The translation function - * @param {TRANSACTION_CATEGORIES[keyof TRANSACTION_CATEGORIES]} transactionCategory - The transaction category constant + * @param {TRANSACTION_TYPES[keyof TRANSACTION_TYPES]} type - The transaction type constant * @returns {string} The transaction category title */ -export function getTransactionCategoryTitle(t, transactionCategory) { - switch (transactionCategory) { - case TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER: { +export function getTransactionTypeTitle(t, type) { + switch (type) { + case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER: { return t('transfer'); } - case TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM: { + case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM: { return t('transferFrom'); } - case TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE: { + case TRANSACTION_TYPES.TOKEN_METHOD_APPROVE: { return t('approve'); } - case TRANSACTION_CATEGORIES.SENT_ETHER: { + case TRANSACTION_TYPES.SENT_ETHER: { return t('sentEther'); } - case TRANSACTION_CATEGORIES.CONTRACT_INTERACTION: { + case TRANSACTION_TYPES.CONTRACT_INTERACTION: { return t('contractInteraction'); } - case TRANSACTION_CATEGORIES.DEPLOY_CONTRACT: { + case TRANSACTION_TYPES.DEPLOY_CONTRACT: { return t('contractDeployment'); } - case TRANSACTION_CATEGORIES.SWAP: { + case TRANSACTION_TYPES.SWAP: { return t('swap'); } - case TRANSACTION_CATEGORIES.SWAP_APPROVAL: { + case TRANSACTION_TYPES.SWAP_APPROVAL: { return t('swapApproval'); } default: { - throw new Error( - `Unrecognized transaction category: ${transactionCategory}`, - ); + throw new Error(`Unrecognized transaction type: ${type}`); } } } diff --git a/ui/app/helpers/utils/transactions.util.test.js b/ui/app/helpers/utils/transactions.util.test.js index 386edd6f8..9bd410cdb 100644 --- a/ui/app/helpers/utils/transactions.util.test.js +++ b/ui/app/helpers/utils/transactions.util.test.js @@ -1,6 +1,6 @@ import assert from 'assert'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_GROUP_STATUSES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; @@ -14,7 +14,7 @@ describe('Transactions utils', function () { ); assert.ok(tokenData); const { name, args } = tokenData; - assert.strictEqual(name, TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER); + assert.strictEqual(name, TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER); const to = args._to; const value = args._value.toString(); assert.strictEqual(to, '0x50A9D56C2B8BA9A5c7f2C08C3d26E0499F23a706'); diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index 1480781c0..239af55a7 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -4,6 +4,14 @@ import BigNumber from 'bignumber.js'; import ethUtil from 'ethereumjs-util'; import { DateTime } from 'luxon'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; +import { + GOERLI_CHAIN_ID, + KOVAN_CHAIN_ID, + LOCALHOST_CHAIN_ID, + MAINNET_CHAIN_ID, + RINKEBY_CHAIN_ID, + ROPSTEN_CHAIN_ID, +} from '../../../../shared/constants/network'; // formatData :: ( date: ) -> String export function formatDate(date, format = "M/d/y 'at' T") { @@ -40,14 +48,19 @@ Object.keys(valueTable).forEach((currency) => { bnTable[currency] = new ethUtil.BN(valueTable[currency], 10); }); -export function isEthNetwork(netId) { +/** + * Determines if the provided chainId is a default MetaMask chain + * @param {string} chainId - chainId to check + */ +export function isDefaultMetaMaskChain(chainId) { if ( - !netId || - netId === '1' || - netId === '3' || - netId === '4' || - netId === '42' || - netId === '1337' + !chainId || + chainId === MAINNET_CHAIN_ID || + chainId === ROPSTEN_CHAIN_ID || + chainId === RINKEBY_CHAIN_ID || + chainId === KOVAN_CHAIN_ID || + chainId === GOERLI_CHAIN_ID || + chainId === LOCALHOST_CHAIN_ID ) { return true; } diff --git a/ui/app/hooks/useCancelTransaction.js b/ui/app/hooks/useCancelTransaction.js index 1ad4c12f8..da4312d2a 100644 --- a/ui/app/hooks/useCancelTransaction.js +++ b/ui/app/hooks/useCancelTransaction.js @@ -1,12 +1,18 @@ import { useDispatch, useSelector } from 'react-redux'; import { useCallback } from 'react'; -import { showModal } from '../store/actions'; +import { addHexPrefix } from 'ethereumjs-util'; +import { showModal, showSidebar } from '../store/actions'; import { isBalanceSufficient } from '../pages/send/send.utils'; import { getHexGasTotal, increaseLastGasPrice, } from '../helpers/utils/confirm-tx.util'; import { getConversionRate, getSelectedAccount } from '../selectors'; +import { + setCustomGasLimit, + setCustomGasPriceForRetry, +} from '../ducks/gas/gas.duck'; +import { multiplyCurrencies } from '../helpers/utils/conversion-util'; /** * Determine whether a transaction can be cancelled and provide a method to @@ -19,27 +25,61 @@ import { getConversionRate, getSelectedAccount } from '../selectors'; * @return {[boolean, Function]} */ export function useCancelTransaction(transactionGroup) { - const { primaryTransaction, initialTransaction } = transactionGroup; + const { primaryTransaction } = transactionGroup; const gasPrice = primaryTransaction.txParams?.gasPrice?.startsWith('-') ? '0x0' : primaryTransaction.txParams?.gasPrice; - const { id } = initialTransaction; + const transaction = primaryTransaction; const dispatch = useDispatch(); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); + const defaultNewGasPrice = addHexPrefix( + multiplyCurrencies(gasPrice, 1.1, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + }), + ); + const cancelTransaction = useCallback( (event) => { event.stopPropagation(); - + dispatch(setCustomGasLimit('0x5208')); + dispatch(setCustomGasPriceForRetry(defaultNewGasPrice)); + const tx = { + ...transaction, + txParams: { + ...transaction.txParams, + gas: '0x5208', + value: '0x0', + }, + }; return dispatch( - showModal({ - name: 'CANCEL_TRANSACTION', - transactionId: id, - originalGasPrice: gasPrice, + showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { + transaction: tx, + onSubmit: (newGasLimit, newGasPrice) => { + const userCustomizedGasTotal = getHexGasTotal({ + gasPrice: newGasPrice, + gasLimit: newGasLimit, + }); + dispatch( + showModal({ + name: 'CANCEL_TRANSACTION', + newGasFee: userCustomizedGasTotal, + transactionId: transaction.id, + defaultNewGasPrice: newGasPrice, + gasLimit: newGasLimit, + }), + ); + }, + }, }), ); }, - [dispatch, id, gasPrice], + [dispatch, transaction, defaultNewGasPrice], ); const hasEnoughCancelGas = diff --git a/ui/app/hooks/tests/useCancelTransaction.test.js b/ui/app/hooks/useCancelTransaction.test.js similarity index 65% rename from ui/app/hooks/tests/useCancelTransaction.test.js rename to ui/app/hooks/useCancelTransaction.test.js index 28957eb9c..96e14ea78 100644 --- a/ui/app/hooks/tests/useCancelTransaction.test.js +++ b/ui/app/hooks/useCancelTransaction.test.js @@ -2,11 +2,12 @@ import assert from 'assert'; import * as reactRedux from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; import sinon from 'sinon'; -import transactions from '../../../../test/data/transaction-data.json'; -import { getConversionRate, getSelectedAccount } from '../../selectors'; -import { useCancelTransaction } from '../useCancelTransaction'; -import { showModal } from '../../store/actions'; -import { increaseLastGasPrice } from '../../helpers/utils/confirm-tx.util'; +import transactions from '../../../test/data/transaction-data.json'; +import { getConversionRate, getSelectedAccount } from '../selectors'; +import { showModal } from '../store/actions'; +import { increaseLastGasPrice } from '../helpers/utils/confirm-tx.util'; +import * as actionConstants from '../store/actionConstants'; +import { useCancelTransaction } from './useCancelTransaction'; describe('useCancelTransaction', function () { let useSelector; @@ -46,7 +47,7 @@ describe('useCancelTransaction', function () { ); assert.strictEqual(result.current[0], false); }); - it(`should return a function that kicks off cancellation for id ${transactionId}`, function () { + it(`should return a function that opens the gas sidebar onsubmit kicks off cancellation for id ${transactionId}`, function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); @@ -55,12 +56,35 @@ describe('useCancelTransaction', function () { preventDefault: () => undefined, stopPropagation: () => undefined, }); + const dispatchAction = dispatch.args; + + // calls customize-gas sidebar + // also check type= customize-gas + assert.strictEqual( + dispatchAction[dispatchAction.length - 1][0].type, + actionConstants.SIDEBAR_OPEN, + ); + + assert.strictEqual( + dispatchAction[dispatchAction.length - 1][0].value.props.transaction + .id, + transactionId, + ); + + // call onSubmit myself + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( + '0x5208', + '0x1', + ); + assert.strictEqual( dispatch.calledWith( showModal({ name: 'CANCEL_TRANSACTION', transactionId, - originalGasPrice, + newGasFee: '0x5208', + defaultNewGasPrice: '0x1', + gasLimit: '0x5208', }), ), true, @@ -98,7 +122,7 @@ describe('useCancelTransaction', function () { ); assert.strictEqual(result.current[0], true); }); - it(`should return a function that kicks off cancellation for id ${transactionId}`, function () { + it(`should return a function that opens the gas sidebar onsubmit kicks off cancellation for id ${transactionId}`, function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); @@ -107,12 +131,31 @@ describe('useCancelTransaction', function () { preventDefault: () => undefined, stopPropagation: () => undefined, }); + const dispatchAction = dispatch.args; + + assert.strictEqual( + dispatchAction[dispatchAction.length - 1][0].type, + actionConstants.SIDEBAR_OPEN, + ); + assert.strictEqual( + dispatchAction[dispatchAction.length - 1][0].value.props.transaction + .id, + transactionId, + ); + + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( + '0x5208', + '0x1', + ); + assert.strictEqual( dispatch.calledWith( showModal({ name: 'CANCEL_TRANSACTION', transactionId, - originalGasPrice, + newGasFee: '0x5208', + defaultNewGasPrice: '0x1', + gasLimit: '0x5208', }), ), true, diff --git a/ui/app/hooks/tests/useCurrencyDisplay.test.js b/ui/app/hooks/useCurrencyDisplay.test.js similarity index 97% rename from ui/app/hooks/tests/useCurrencyDisplay.test.js rename to ui/app/hooks/useCurrencyDisplay.test.js index 8b019ab87..32861bf50 100644 --- a/ui/app/hooks/tests/useCurrencyDisplay.test.js +++ b/ui/app/hooks/useCurrencyDisplay.test.js @@ -2,12 +2,12 @@ import assert from 'assert'; import { renderHook } from '@testing-library/react-hooks'; import * as reactRedux from 'react-redux'; import sinon from 'sinon'; -import { useCurrencyDisplay } from '../useCurrencyDisplay'; import { getCurrentCurrency, getNativeCurrency, getConversionRate, -} from '../../selectors'; +} from '../selectors'; +import { useCurrencyDisplay } from './useCurrencyDisplay'; const tests = [ { diff --git a/ui/app/hooks/useRetryTransaction.js b/ui/app/hooks/useRetryTransaction.js index 9081ea44d..0f1532014 100644 --- a/ui/app/hooks/useRetryTransaction.js +++ b/ui/app/hooks/useRetryTransaction.js @@ -16,7 +16,7 @@ import { useMetricEvent } from './useMetricEvent'; * @return {Function} */ export function useRetryTransaction(transactionGroup) { - const { primaryTransaction, initialTransaction } = transactionGroup; + const { primaryTransaction } = transactionGroup; // Signature requests do not have a txParams, but this hook is called indiscriminately const gasPrice = primaryTransaction.txParams?.gasPrice; const trackMetricsEvent = useMetricEvent({ @@ -34,7 +34,7 @@ export function useRetryTransaction(transactionGroup) { trackMetricsEvent(); await dispatch(fetchBasicGasEstimates); - const transaction = initialTransaction; + const transaction = primaryTransaction; const increasedGasPrice = increaseLastGasPrice(gasPrice); await dispatch( setCustomGasPriceForRetry( @@ -50,7 +50,7 @@ export function useRetryTransaction(transactionGroup) { }), ); }, - [dispatch, trackMetricsEvent, initialTransaction, gasPrice], + [dispatch, trackMetricsEvent, gasPrice, primaryTransaction], ); return retryTransaction; diff --git a/ui/app/hooks/tests/useRetryTransaction.test.js b/ui/app/hooks/useRetryTransaction.test.js similarity index 55% rename from ui/app/hooks/tests/useRetryTransaction.test.js rename to ui/app/hooks/useRetryTransaction.test.js index 52be98fc1..a2a03cfc7 100644 --- a/ui/app/hooks/tests/useRetryTransaction.test.js +++ b/ui/app/hooks/useRetryTransaction.test.js @@ -2,11 +2,11 @@ import assert from 'assert'; import * as reactRedux from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; import sinon from 'sinon'; -import transactions from '../../../../test/data/transaction-data.json'; -import * as methodDataHook from '../useMethodData'; -import * as metricEventHook from '../useMetricEvent'; -import { showSidebar } from '../../store/actions'; -import { useRetryTransaction } from '../useRetryTransaction'; +import transactions from '../../../test/data/transaction-data.json'; +import { showSidebar } from '../store/actions'; +import * as methodDataHook from './useMethodData'; +import * as metricEventHook from './useMetricEvent'; +import { useRetryTransaction } from './useRetryTransaction'; describe('useRetryTransaction', function () { describe('when transaction meets retry enabled criteria', function () { @@ -64,6 +64,50 @@ describe('useRetryTransaction', function () { ); }); + it('should handle cancelled or multiple speedup transactions', async function () { + const cancelledTransaction = { + initialTransaction: { + ...transactions[0].initialTransaction, + txParams: { + ...transactions[0].initialTransaction.txParams, + }, + }, + primaryTransaction: { + ...transactions[0].primaryTransaction, + txParams: { + from: '0xee014609ef9e09776ac5fe00bdbfef57bcdefebb', + gas: '0x5308', + gasPrice: '0x77359400', + nonce: '0x3', + to: '0xabca64466f257793eaa52fcfff5066894b76a149', + value: '0x0', + }, + }, + transactions: [ + { + submittedTime: new Date() - 5001, + }, + ], + hasRetried: false, + }; + + const { result } = renderHook(() => + useRetryTransaction(cancelledTransaction, true), + ); + const retry = result.current; + await retry(event); + assert.strictEqual( + dispatch.calledWith( + showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { transaction: cancelledTransaction.primaryTransaction }, + }), + ), + true, + ); + }); + after(function () { sinon.restore(); }); diff --git a/ui/app/hooks/useSwappedTokenValue.js b/ui/app/hooks/useSwappedTokenValue.js index efe3c8cc2..6eff3726f 100644 --- a/ui/app/hooks/useSwappedTokenValue.js +++ b/ui/app/hooks/useSwappedTokenValue.js @@ -1,9 +1,9 @@ import { useSelector } from 'react-redux'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, } from '../../../shared/modules/swaps.utils'; -import { TRANSACTION_CATEGORIES } from '../../../shared/constants/transaction'; import { getSwapsTokensReceivedFromTxMeta } from '../pages/swaps/swaps.util'; import { getCurrentChainId } from '../selectors'; import { useTokenFiatAmount } from './useTokenFiatAmount'; @@ -31,7 +31,7 @@ import { useTokenFiatAmount } from './useTokenFiatAmount'; export function useSwappedTokenValue(transactionGroup, currentAsset) { const { symbol, decimals, address } = currentAsset; const { primaryTransaction, initialTransaction } = transactionGroup; - const { transactionCategory } = initialTransaction; + const { type } = initialTransaction; const { from: senderAddress } = initialTransaction.txParams || {}; const chainId = useSelector(getCurrentChainId); @@ -44,8 +44,7 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { )); const swapTokenValue = - transactionCategory === TRANSACTION_CATEGORIES.SWAP && - isViewingReceivedTokenFromSwap + type === TRANSACTION_TYPES.SWAP && isViewingReceivedTokenFromSwap ? getSwapsTokensReceivedFromTxMeta( primaryTransaction.destinationTokenSymbol, initialTransaction, @@ -55,8 +54,7 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { null, chainId, ) - : transactionCategory === TRANSACTION_CATEGORIES.SWAP && - primaryTransaction.swapTokenValue; + : type === TRANSACTION_TYPES.SWAP && primaryTransaction.swapTokenValue; const isNegative = typeof swapTokenValue === 'string' diff --git a/ui/app/hooks/tests/useTokenData.test.js b/ui/app/hooks/useTokenData.test.js similarity index 85% rename from ui/app/hooks/tests/useTokenData.test.js rename to ui/app/hooks/useTokenData.test.js index 593176105..915aa98a1 100644 --- a/ui/app/hooks/tests/useTokenData.test.js +++ b/ui/app/hooks/useTokenData.test.js @@ -1,15 +1,15 @@ import assert from 'assert'; import { ethers } from 'ethers'; import { renderHook } from '@testing-library/react-hooks'; -import { useTokenData } from '../useTokenData'; -import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; +import { useTokenData } from './useTokenData'; const tests = [ { data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a970000000000000000000000000000000000000000000000000000000000003a98', tokenData: { - name: TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, + name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, args: [ '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', ethers.BigNumber.from(15000), @@ -20,7 +20,7 @@ const tests = [ data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a9700000000000000000000000000000000000000000000000000000000000061a8', tokenData: { - name: TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, + name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, args: [ '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', ethers.BigNumber.from(25000), @@ -31,7 +31,7 @@ const tests = [ data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a970000000000000000000000000000000000000000000000000000000000002710', tokenData: { - name: TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER, + name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, args: [ '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', ethers.BigNumber.from(10000), diff --git a/ui/app/hooks/tests/useTokenDisplayValue.test.js b/ui/app/hooks/useTokenDisplayValue.test.js similarity index 93% rename from ui/app/hooks/tests/useTokenDisplayValue.test.js rename to ui/app/hooks/useTokenDisplayValue.test.js index 6fc6f6105..825b7b2a4 100644 --- a/ui/app/hooks/tests/useTokenDisplayValue.test.js +++ b/ui/app/hooks/useTokenDisplayValue.test.js @@ -1,9 +1,9 @@ import assert from 'assert'; import { renderHook } from '@testing-library/react-hooks'; import sinon from 'sinon'; -import * as tokenUtil from '../../helpers/utils/token-util'; -import * as txUtil from '../../helpers/utils/transactions.util'; -import { useTokenDisplayValue } from '../useTokenDisplayValue'; +import * as tokenUtil from '../helpers/utils/token-util'; +import * as txUtil from '../helpers/utils/transactions.util'; +import { useTokenDisplayValue } from './useTokenDisplayValue'; const tests = [ { diff --git a/ui/app/hooks/useTokenTracker.js b/ui/app/hooks/useTokenTracker.js index 2b2411d1e..448ec32b8 100644 --- a/ui/app/hooks/useTokenTracker.js +++ b/ui/app/hooks/useTokenTracker.js @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import TokenTracker from '@metamask/eth-token-tracker'; import { useSelector } from 'react-redux'; -import { getCurrentNetwork, getSelectedAddress } from '../selectors'; +import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker( @@ -9,7 +9,7 @@ export function useTokenTracker( includeFailedTokens = false, hideZeroBalanceTokens = false, ) { - const network = useSelector(getCurrentNetwork); + const chainId = useSelector(getCurrentChainId); const userAddress = useSelector(getSelectedAddress); const [loading, setLoading] = useState(() => tokens?.length >= 0); const [tokensWithBalances, setTokensWithBalances] = useState([]); @@ -74,14 +74,14 @@ export function useTokenTracker( // Effect to set loading state and initialize tracker when values change useEffect(() => { // This effect will only run initially and when: - // 1. network is updated, + // 1. chainId is updated, // 2. userAddress is changed, // 3. token list is updated and not equal to previous list // in any of these scenarios, we should indicate to the user that their token // values are in the process of updating by setting loading state. setLoading(true); - if (!userAddress || network === 'loading' || !global.ethereumProvider) { + if (!userAddress || chainId === undefined || !global.ethereumProvider) { // If we do not have enough information to build a TokenTracker, we exit early // When the values above change, the effect will be restarted. We also teardown // tracker because inevitably this effect will run again momentarily. @@ -98,7 +98,7 @@ export function useTokenTracker( }, [ userAddress, teardownTracker, - network, + chainId, memoizedTokens, updateBalances, buildTracker, diff --git a/ui/app/hooks/useTransactionDisplayData.js b/ui/app/hooks/useTransactionDisplayData.js index 18b0e7c39..ffdda4660 100644 --- a/ui/app/hooks/useTransactionDisplayData.js +++ b/ui/app/hooks/useTransactionDisplayData.js @@ -2,7 +2,7 @@ import { useSelector } from 'react-redux'; import { getKnownMethodData } from '../selectors/selectors'; import { getStatusKey, - getTransactionCategoryTitle, + getTransactionTypeTitle, } from '../helpers/utils/transactions.util'; import { camelCaseToCapitalize } from '../helpers/utils/common.util'; import { PRIMARY, SECONDARY } from '../helpers/constants/common'; @@ -18,7 +18,7 @@ import { } from '../helpers/constants/transactions'; import { getTokens } from '../ducks/metamask/metamask'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_GROUP_CATEGORIES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; @@ -62,7 +62,7 @@ export function useTransactionDisplayData(transactionGroup) { const t = useI18nContext(); const { initialTransaction, primaryTransaction } = transactionGroup; // initialTransaction contains the data we need to derive the primary purpose of this transaction group - const { transactionCategory } = initialTransaction; + const { type } = initialTransaction; const { from: senderAddress, to } = initialTransaction.txParams || {}; @@ -85,7 +85,7 @@ export function useTransactionDisplayData(transactionGroup) { // This value is used to determine whether we should look inside txParams.data // to pull out and render token related information - const isTokenCategory = TOKEN_CATEGORY_HASH[transactionCategory]; + const isTokenCategory = TOKEN_CATEGORY_HASH[type]; // these values are always instantiated because they are either // used by or returned from hooks. Hooks must be called at the top level, @@ -145,12 +145,12 @@ export function useTransactionDisplayData(transactionGroup) { // 6. Swap // 7. Swap Approval - if (transactionCategory === null || transactionCategory === undefined) { + if (type === null || type === undefined) { category = TRANSACTION_GROUP_CATEGORIES.SIGNATURE_REQUEST; title = t('signatureRequest'); subtitle = origin; subtitleContainsOrigin = true; - } else if (transactionCategory === TRANSACTION_CATEGORIES.SWAP) { + } else if (type === TRANSACTION_TYPES.SWAP) { category = TRANSACTION_GROUP_CATEGORIES.SWAP; title = t('swapTokenToToken', [ initialTransaction.sourceTokenSymbol, @@ -170,50 +170,45 @@ export function useTransactionDisplayData(transactionGroup) { } else { prefix = '-'; } - } else if (transactionCategory === TRANSACTION_CATEGORIES.SWAP_APPROVAL) { + } else if (type === TRANSACTION_TYPES.SWAP_APPROVAL) { category = TRANSACTION_GROUP_CATEGORIES.APPROVAL; title = t('swapApproval', [primaryTransaction.sourceTokenSymbol]); subtitle = origin; subtitleContainsOrigin = true; primarySuffix = primaryTransaction.sourceTokenSymbol; - } else if ( - transactionCategory === TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE - ) { + } else if (type === TRANSACTION_TYPES.TOKEN_METHOD_APPROVE) { category = TRANSACTION_GROUP_CATEGORIES.APPROVAL; prefix = ''; title = t('approveSpendLimit', [token?.symbol || t('token')]); subtitle = origin; subtitleContainsOrigin = true; } else if ( - transactionCategory === TRANSACTION_CATEGORIES.DEPLOY_CONTRACT || - transactionCategory === TRANSACTION_CATEGORIES.CONTRACT_INTERACTION + type === TRANSACTION_TYPES.DEPLOY_CONTRACT || + type === TRANSACTION_TYPES.CONTRACT_INTERACTION ) { category = TRANSACTION_GROUP_CATEGORIES.INTERACTION; - const transactionCategoryTitle = getTransactionCategoryTitle( - t, - transactionCategory, - ); + const transactionTypeTitle = getTransactionTypeTitle(t, type); title = (methodData?.name && camelCaseToCapitalize(methodData.name)) || - transactionCategoryTitle; + transactionTypeTitle; subtitle = origin; subtitleContainsOrigin = true; - } else if (transactionCategory === TRANSACTION_CATEGORIES.INCOMING) { + } else if (type === TRANSACTION_TYPES.INCOMING) { category = TRANSACTION_GROUP_CATEGORIES.RECEIVE; title = t('receive'); prefix = ''; subtitle = t('fromAddress', [shortenAddress(senderAddress)]); } else if ( - transactionCategory === TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM || - transactionCategory === TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER + type === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM || + type === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER ) { category = TRANSACTION_GROUP_CATEGORIES.SEND; title = t('sendSpecifiedTokens', [token?.symbol || t('token')]); recipientAddress = getTokenAddressParam(tokenData); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); - } else if (transactionCategory === TRANSACTION_CATEGORIES.SENT_ETHER) { + } else if (type === TRANSACTION_TYPES.SENT_ETHER) { category = TRANSACTION_GROUP_CATEGORIES.SEND; - title = t('sendETH'); + title = t('send'); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); } @@ -241,15 +236,12 @@ export function useTransactionDisplayData(transactionGroup) { subtitle, subtitleContainsOrigin, primaryCurrency: - transactionCategory === TRANSACTION_CATEGORIES.SWAP && isPending - ? '' - : primaryCurrency, + type === TRANSACTION_TYPES.SWAP && isPending ? '' : primaryCurrency, senderAddress, recipientAddress, secondaryCurrency: (isTokenCategory && !tokenFiatAmount) || - (transactionCategory === TRANSACTION_CATEGORIES.SWAP && - !swapTokenFiatAmount) + (type === TRANSACTION_TYPES.SWAP && !swapTokenFiatAmount) ? undefined : secondaryCurrency, displayedStatusKey, diff --git a/ui/app/hooks/tests/useTransactionDisplayData.test.js b/ui/app/hooks/useTransactionDisplayData.test.js similarity index 91% rename from ui/app/hooks/tests/useTransactionDisplayData.test.js rename to ui/app/hooks/useTransactionDisplayData.test.js index 4f6d4b3ff..572c8e99b 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/useTransactionDisplayData.test.js @@ -4,31 +4,31 @@ import * as reactRedux from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; import sinon from 'sinon'; import { MemoryRouter } from 'react-router-dom'; -import transactions from '../../../../test/data/transaction-data.json'; -import { useTransactionDisplayData } from '../useTransactionDisplayData'; -import * as useTokenFiatAmountHooks from '../useTokenFiatAmount'; +import transactions from '../../../test/data/transaction-data.json'; import { getPreferences, getShouldShowFiat, getNativeCurrency, getCurrentCurrency, getCurrentChainId, -} from '../../selectors'; -import { getTokens } from '../../ducks/metamask/metamask'; -import * as i18nhooks from '../useI18nContext'; -import { getMessage } from '../../helpers/utils/i18n-helper'; -import messages from '../../../../app/_locales/en/messages.json'; -import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; +} from '../selectors'; +import { getTokens } from '../ducks/metamask/metamask'; +import { getMessage } from '../helpers/utils/i18n-helper'; +import messages from '../../../app/_locales/en/messages.json'; +import { ASSET_ROUTE, DEFAULT_ROUTE } from '../helpers/constants/routes'; +import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_GROUP_CATEGORIES, TRANSACTION_STATUSES, -} from '../../../../shared/constants/transaction'; +} from '../../../shared/constants/transaction'; +import * as i18nhooks from './useI18nContext'; +import * as useTokenFiatAmountHooks from './useTokenFiatAmount'; +import { useTransactionDisplayData } from './useTransactionDisplayData'; const expectedResults = [ { - title: 'Send ETH', + title: 'Send', category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0xffe5...1a97', subtitleContainsOrigin: false, @@ -42,7 +42,7 @@ const expectedResults = [ isSubmitted: false, }, { - title: 'Send ETH', + title: 'Send', category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0x0ccc...8848', subtitleContainsOrigin: false, @@ -55,7 +55,7 @@ const expectedResults = [ displayedStatusKey: TRANSACTION_STATUSES.CONFIRMED, }, { - title: 'Send ETH', + title: 'Send', category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0xffe5...1a97', subtitleContainsOrigin: false, @@ -108,7 +108,7 @@ const expectedResults = [ }, { title: 'Swap ETH to ABC', - category: TRANSACTION_CATEGORIES.SWAP, + category: TRANSACTION_TYPES.SWAP, subtitle: '', subtitleContainsOrigin: false, date: 'May 12, 2020', diff --git a/ui/app/hooks/tests/useUserPreferencedCurrency.test.js b/ui/app/hooks/useUserPreferencedCurrency.test.js similarity index 95% rename from ui/app/hooks/tests/useUserPreferencedCurrency.test.js rename to ui/app/hooks/useUserPreferencedCurrency.test.js index 75ee4cf66..6adca5d4d 100644 --- a/ui/app/hooks/tests/useUserPreferencedCurrency.test.js +++ b/ui/app/hooks/useUserPreferencedCurrency.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import { renderHook } from '@testing-library/react-hooks'; import * as reactRedux from 'react-redux'; import sinon from 'sinon'; -import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency'; -import { getPreferences, getShouldShowFiat } from '../../selectors'; +import { getPreferences, getShouldShowFiat } from '../selectors'; +import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; const tests = [ { diff --git a/ui/app/pages/add-token/add-token.component.js b/ui/app/pages/add-token/add-token.component.js index 40f4a3675..103756e72 100644 --- a/ui/app/pages/add-token/add-token.component.js +++ b/ui/app/pages/add-token/add-token.component.js @@ -13,6 +13,9 @@ import TokenSearch from './token-search'; const emptyAddr = '0x0000000000000000000000000000000000000000'; +const MIN_DECIMAL_VALUE = 0; +const MAX_DECIMAL_VALUE = 36; + class AddToken extends Component { static contextTypes = { t: PropTypes.func, @@ -25,6 +28,7 @@ class AddToken extends Component { clearPendingTokens: PropTypes.func, tokens: PropTypes.array, identities: PropTypes.object, + showSearchTab: PropTypes.bool.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired, }; @@ -211,8 +215,8 @@ class AddToken extends Component { const validDecimals = customDecimals !== null && customDecimals !== '' && - customDecimals >= 0 && - customDecimals <= 36; + customDecimals >= MIN_DECIMAL_VALUE && + customDecimals <= MAX_DECIMAL_VALUE; let customDecimalsError = null; if (!validDecimals) { @@ -282,6 +286,8 @@ class AddToken extends Component { fullWidth margin="normal" disabled={autoFilled} + min={MIN_DECIMAL_VALUE} + max={MAX_DECIMAL_VALUE} />
    ); @@ -310,14 +316,23 @@ class AddToken extends Component { } renderTabs() { - return ( - - {this.renderSearchToken()} - - {this.renderCustomTokenForm()} - - + const { showSearchTab } = this.props; + const tabs = []; + + if (showSearchTab) { + tabs.push( + + {this.renderSearchToken()} + , + ); + } + tabs.push( + + {this.renderCustomTokenForm()} + , ); + + return {tabs}; } render() { diff --git a/ui/app/pages/add-token/add-token.container.js b/ui/app/pages/add-token/add-token.container.js index 490b34583..ab8bf24c8 100644 --- a/ui/app/pages/add-token/add-token.container.js +++ b/ui/app/pages/add-token/add-token.container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { setPendingTokens, clearPendingTokens } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; +import { getIsMainnet } from '../../selectors/selectors'; import AddToken from './add-token.component'; const mapStateToProps = (state) => { @@ -13,6 +14,7 @@ const mapStateToProps = (state) => { mostRecentOverviewPage: getMostRecentOverviewPage(state), tokens, pendingTokens, + showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true', }; }; diff --git a/ui/app/pages/add-token/tests/add-token.test.js b/ui/app/pages/add-token/add-token.test.js similarity index 95% rename from ui/app/pages/add-token/tests/add-token.test.js rename to ui/app/pages/add-token/add-token.test.js index bc130409c..4c1394085 100644 --- a/ui/app/pages/add-token/tests/add-token.test.js +++ b/ui/app/pages/add-token/add-token.test.js @@ -3,8 +3,8 @@ import React from 'react'; import { Provider } from 'react-redux'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; -import { mountWithRouter } from '../../../../../test/lib/render-helpers'; -import AddToken from '..'; +import { mountWithRouter } from '../../../../test/lib/render-helpers'; +import AddToken from './add-token.container'; describe('Add Token', function () { let wrapper; @@ -26,6 +26,7 @@ describe('Add Token', function () { tokens: [], identities: {}, mostRecentOverviewPage: '/', + showSearchTab: true, }; describe('Add Token', function () { diff --git a/ui/app/pages/add-token/token-search/token-search.component.js b/ui/app/pages/add-token/token-search/token-search.component.js index f58ca299f..76bfdab09 100644 --- a/ui/app/pages/add-token/token-search/token-search.component.js +++ b/ui/app/pages/add-token/token-search/token-search.component.js @@ -72,6 +72,7 @@ export default class TokenSearch extends Component { error={error} fullWidth autoFocus + autoComplete="off" startAdornment={this.renderAdornment()} /> ); diff --git a/ui/app/pages/asset/asset.scss b/ui/app/pages/asset/asset.scss index b9844e8e2..8cbb339e8 100644 --- a/ui/app/pages/asset/asset.scss +++ b/ui/app/pages/asset/asset.scss @@ -37,7 +37,7 @@ font-size: $font-size-paragraph; color: $Black-100; background-color: inherit; - padding: 2px 8px; + padding: 2px 0 2px 8px; } &__icon { diff --git a/ui/app/pages/confirm-approve/confirm-approve.util.js b/ui/app/pages/confirm-approve/confirm-approve.util.js index e724ba58e..3345b0966 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.util.js +++ b/ui/app/pages/confirm-approve/confirm-approve.util.js @@ -1,4 +1,4 @@ -import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'; +import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; import { decimalToHex } from '../../helpers/utils/conversions.util'; import { calcTokenValue, @@ -14,7 +14,7 @@ export function getCustomTxParamsData( if (!tokenData) { throw new Error('Invalid data'); - } else if (tokenData.name !== TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE) { + } else if (tokenData.name !== TRANSACTION_TYPES.TOKEN_METHOD_APPROVE) { throw new Error( `Invalid data; should be 'approve' method, but instead is '${tokenData.name}'`, ); diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index d1b702df9..65af5c890 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -19,10 +19,10 @@ import { hexToDecimal } from '../../helpers/utils/conversions.util'; import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'; import TextField from '../../components/ui/text-field'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; -import { getTransactionCategoryTitle } from '../../helpers/utils/transactions.util'; +import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util'; export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -87,7 +87,7 @@ export default class ConfirmTransactionBase extends Component { advancedInlineGasShown: PropTypes.bool, insufficientBalance: PropTypes.bool, hideFiatConversion: PropTypes.bool, - transactionCategory: PropTypes.string, + type: PropTypes.string, getNextNonce: PropTypes.func, nextNonce: PropTypes.number, tryReverseResolveAddress: PropTypes.func.isRequired, @@ -218,7 +218,7 @@ export default class ConfirmTransactionBase extends Component { functionType: actionKey || getMethodName(methodData.name) || - TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + TRANSACTION_TYPES.CONTRACT_INTERACTION, origin, }, }); @@ -395,7 +395,7 @@ export default class ConfirmTransactionBase extends Component { functionType: actionKey || getMethodName(methodData.name) || - TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + TRANSACTION_TYPES.CONTRACT_INTERACTION, origin, }, }); @@ -450,7 +450,7 @@ export default class ConfirmTransactionBase extends Component { functionType: actionKey || getMethodName(methodData.name) || - TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + TRANSACTION_TYPES.CONTRACT_INTERACTION, origin, }, }); @@ -500,7 +500,7 @@ export default class ConfirmTransactionBase extends Component { functionType: actionKey || getMethodName(methodData.name) || - TRANSACTION_CATEGORIES.CONTRACT_INTERACTION, + TRANSACTION_TYPES.CONTRACT_INTERACTION, origin, }, }); @@ -667,7 +667,7 @@ export default class ConfirmTransactionBase extends Component { customNonceValue, assetImage, unapprovedTxCount, - transactionCategory, + type, hideSenderToRecipient, showAccountInHeader, txData, @@ -690,8 +690,8 @@ export default class ConfirmTransactionBase extends Component { let functionType = getMethodName(name); if (!functionType) { - if (transactionCategory) { - functionType = getTransactionCategoryTitle(t, transactionCategory); + if (type) { + functionType = getTransactionTypeTitle(t, type); } else { functionType = t('contractInteraction'); } diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js similarity index 88% rename from ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js rename to ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js index a46d89f95..087834226 100644 --- a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js @@ -1,5 +1,5 @@ import assert from 'assert'; -import { getMethodName } from '../confirm-transaction-base.component'; +import { getMethodName } from './confirm-transaction-base.component'; describe('ConfirmTransactionBase Component', function () { describe('getMethodName', function () { diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 69c8fa0d9..cb5332a22 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -81,12 +81,7 @@ const mapStateToProps = (state, ownProps) => { provider: { chainId }, } = metamask; const { tokenData, txData, tokenProps, nonce } = confirmTransaction; - const { - txParams = {}, - lastGasPrice, - id: transactionId, - transactionCategory, - } = txData; + const { txParams = {}, lastGasPrice, id: transactionId, type } = txData; const transaction = Object.values(unapprovedTxs).find( ({ id }) => id === (transactionId || Number(paramsTransactionId)), @@ -189,7 +184,7 @@ const mapStateToProps = (state, ownProps) => { hideSubtitle: !isMainnet && !showFiatInTestnets, hideFiatConversion: !isMainnet && !showFiatInTestnets, metaMetricsSendCount, - transactionCategory, + type, nextNonce, mostRecentOverviewPage: getMostRecentOverviewPage(state), isMainnet, diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 18969e233..83aa7cdbc 100644 --- a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -15,7 +15,7 @@ import { ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, } from '../../helpers/constants/routes'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; -import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'; +import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; export default class ConfirmTransactionSwitch extends Component { static propTypes = { @@ -24,29 +24,29 @@ export default class ConfirmTransactionSwitch extends Component { redirectToTransaction() { const { txData } = this.props; - const { id, txParams: { data } = {}, transactionCategory } = txData; + const { id, txParams: { data } = {}, type } = txData; - if (transactionCategory === TRANSACTION_CATEGORIES.DEPLOY_CONTRACT) { + if (type === TRANSACTION_TYPES.DEPLOY_CONTRACT) { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`; return ; } - if (transactionCategory === TRANSACTION_CATEGORIES.SENT_ETHER) { + if (type === TRANSACTION_TYPES.SENT_ETHER) { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`; return ; } if (data) { - switch (transactionCategory) { - case TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER: { + switch (type) { + case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`; return ; } - case TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE: { + case TRANSACTION_TYPES.TOKEN_METHOD_APPROVE: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`; return ; } - case TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM: { + case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`; return ; } diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js index d07665256..68ee7c3ed 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js @@ -27,7 +27,7 @@ const mapStateToProps = (state, ownProps) => { const transaction = totalUnconfirmed ? unapprovedTxs[id] || unconfirmedTransactions[0] : {}; - const { id: transactionId, transactionCategory } = transaction; + const { id: transactionId, type } = transaction; return { totalUnapprovedCount: totalUnconfirmed, @@ -38,7 +38,7 @@ const mapStateToProps = (state, ownProps) => { paramsTransactionId: id && String(id), transactionId: transactionId && String(transactionId), transaction, - isTokenMethodAction: isTokenMethodAction(transactionCategory), + isTokenMethodAction: isTokenMethodAction(type), }; }; diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js index 4af6e501d..67051c213 100644 --- a/ui/app/pages/create-account/connect-hardware/index.js +++ b/ui/app/pages/create-account/connect-hardware/index.js @@ -17,9 +17,11 @@ const U2F_ERROR = 'U2F'; const LEDGER_LIVE_PATH = `m/44'/60'/0'/0/0`; const MEW_PATH = `m/44'/60'/0'`; +const BIP44_PATH = `m/44'/60'/0'/0`; const HD_PATHS = [ { name: 'Ledger Live', value: LEDGER_LIVE_PATH }, { name: 'Legacy (MEW / MyCrypto)', value: MEW_PATH }, + { name: `BIP44 Standard (e.g. MetaMask, Trezor)`, value: BIP44_PATH }, ]; class ConnectHardwareForm extends Component { diff --git a/ui/app/pages/create-account/tests/create-account.test.js b/ui/app/pages/create-account/create-account.test.js similarity index 92% rename from ui/app/pages/create-account/tests/create-account.test.js rename to ui/app/pages/create-account/create-account.test.js index 0c564a64b..579be2eaa 100644 --- a/ui/app/pages/create-account/tests/create-account.test.js +++ b/ui/app/pages/create-account/create-account.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import { mountWithRouter } from '../../../../../test/lib/render-helpers'; -import CreateAccountPage from '..'; +import { mountWithRouter } from '../../../../test/lib/render-helpers'; +import CreateAccountPage from '.'; describe('Create Account Page', function () { let wrapper; diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/tests/import-with-seed-phrase.component.test.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.test.js similarity index 97% rename from ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/tests/import-with-seed-phrase.component.test.js rename to ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.test.js index 57e8ddcee..7d1cb07c2 100644 --- a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/tests/import-with-seed-phrase.component.test.js +++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import ImportWithSeedPhrase from '../import-with-seed-phrase.component'; +import ImportWithSeedPhrase from './import-with-seed-phrase.component'; function shallowRender(props = {}, context = {}) { return shallow(, { diff --git a/ui/app/pages/first-time-flow/end-of-flow/tests/end-of-flow.test.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js similarity index 80% rename from ui/app/pages/first-time-flow/end-of-flow/tests/end-of-flow.test.js rename to ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js index 2e25aa6b9..13d0fefc6 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/tests/end-of-flow.test.js +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js @@ -1,9 +1,9 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes'; -import EndOfFlowScreen from '..'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import EndOfFlowScreen from './end-of-flow.container'; describe('End of Flow Screen', function () { let wrapper; diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/tests/first-time-flow-switch.test.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.test.js similarity index 94% rename from ui/app/pages/first-time-flow/first-time-flow-switch/tests/first-time-flow-switch.test.js rename to ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.test.js index 16270b2bd..3bb8fe0e0 100644 --- a/ui/app/pages/first-time-flow/first-time-flow-switch/tests/first-time-flow-switch.test.js +++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.test.js @@ -1,14 +1,14 @@ import assert from 'assert'; import React from 'react'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; import { DEFAULT_ROUTE, LOCK_ROUTE, INITIALIZE_WELCOME_ROUTE, INITIALIZE_UNLOCK_ROUTE, INITIALIZE_END_OF_FLOW_ROUTE, -} from '../../../../helpers/constants/routes'; -import FirstTimeFlowSwitch from '..'; +} from '../../../helpers/constants/routes'; +import FirstTimeFlowSwitch from './first-time-flow-switch.container'; describe('FirstTimeFlowSwitch', function () { it('redirects to /welcome route with null props', function () { diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/tests/metametrics-opt-in.test.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.test.js similarity index 86% rename from ui/app/pages/first-time-flow/metametrics-opt-in/tests/metametrics-opt-in.test.js rename to ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.test.js index 7aee30951..6322c3aea 100644 --- a/ui/app/pages/first-time-flow/metametrics-opt-in/tests/metametrics-opt-in.test.js +++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import MetaMetricsOptIn from '..'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import MetaMetricsOptIn from './metametrics-opt-in.container'; describe('MetaMetricsOptIn', function () { it('opt out of MetaMetrics', function () { diff --git a/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase-component.test.js similarity index 98% rename from ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js rename to ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase-component.test.js index 5add90bd9..8614901ac 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase-component.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import ConfirmSeedPhrase from '../confirm-seed-phrase/confirm-seed-phrase.component'; +import ConfirmSeedPhrase from './confirm-seed-phrase/confirm-seed-phrase.component'; function shallowRender(props = {}, context = {}) { return shallow(, { diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/tests/reveal-seed-phrase.test.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.test.js similarity index 95% rename from ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/tests/reveal-seed-phrase.test.js rename to ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.test.js index 29f1d6968..9fcd50cdd 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/tests/reveal-seed-phrase.test.js +++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.test.js @@ -2,7 +2,7 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { mount } from 'enzyme'; -import RevealSeedPhrase from '..'; +import RevealSeedPhrase from './reveal-seed-phrase.container'; describe('Reveal Seed Phrase', function () { let wrapper; diff --git a/ui/app/pages/first-time-flow/select-action/tests/select-action.test.js b/ui/app/pages/first-time-flow/select-action/select-action.test.js similarity index 91% rename from ui/app/pages/first-time-flow/select-action/tests/select-action.test.js rename to ui/app/pages/first-time-flow/select-action/select-action.test.js index 9f5b66fc6..3a370b088 100644 --- a/ui/app/pages/first-time-flow/select-action/tests/select-action.test.js +++ b/ui/app/pages/first-time-flow/select-action/select-action.test.js @@ -1,8 +1,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import SelectAction from '..'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import SelectAction from './select-action.container'; describe('Selection Action', function () { let wrapper; diff --git a/ui/app/pages/first-time-flow/welcome/tests/welcome.test.js b/ui/app/pages/first-time-flow/welcome/welcome.test.js similarity index 92% rename from ui/app/pages/first-time-flow/welcome/tests/welcome.test.js rename to ui/app/pages/first-time-flow/welcome/welcome.test.js index cc8dbab08..5914cadf7 100644 --- a/ui/app/pages/first-time-flow/welcome/tests/welcome.test.js +++ b/ui/app/pages/first-time-flow/welcome/welcome.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import Welcome from '..'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import Welcome from './welcome.container'; describe('Welcome', function () { const mockStore = { diff --git a/ui/app/pages/keychains/index.scss b/ui/app/pages/keychains/index.scss index a5934bb7a..263a1073c 100644 --- a/ui/app/pages/keychains/index.scss +++ b/ui/app/pages/keychains/index.scss @@ -25,6 +25,12 @@ margin: 60px 0 30px 0; position: relative; max-width: initial; + + &__input-label { + padding-bottom: 10px; + font-weight: 400; + display: inline-block; + } } @media only screen and (max-width: 575px) { diff --git a/ui/app/pages/keychains/restore-vault.js b/ui/app/pages/keychains/restore-vault.js index fc1121c7b..6073c647a 100644 --- a/ui/app/pages/keychains/restore-vault.js +++ b/ui/app/pages/keychains/restore-vault.js @@ -157,7 +157,9 @@ class RestoreVaultPage extends Component { {this.context.t('secretPhrase')}
    - + {showSeedPhrase ? (