From 9367426bdf7539d271509a173b3ddb5500f7e338 Mon Sep 17 00:00:00 2001 From: ryanml Date: Wed, 28 Apr 2021 16:14:39 -0700 Subject: [PATCH 1/7] Update the CLA Signature bot to v3.0.2 (#10947) (#10948) This update includes support for PRs with >100 commits. Co-authored-by: Mark Stacey --- .github/workflows/cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index b44bbf84a..f358e58a9 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "CLA Signature Bot" - uses: MetaMask/cla-signature-bot@v3.0.1 + uses: MetaMask/cla-signature-bot@v3.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 333206ccb14eff9e23e15f9d470b174ee46c126e Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Wed, 5 May 2021 15:38:59 -0230 Subject: [PATCH 2/7] Version v9.5.0 RC (#10944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add trezor HD path for ledger wallets (#10616) Co-authored-by: Barry Gitarts * Replace logic for eth swap token in fetchQuotesAndSetQuoteState with getSwapsEthToken call (#10624) * Ensure permission log will only store JSON-able data (#10524) * remove transactionCategory in favor of more types (#10615) * remove transactionCategory in favor of more types * remove reference to STANDARD in stubs * Removing double click bug from delete custom network modal (#10628) Fixes MetaMask/metamask-extension#10626 * Hide zero balance tokens at useTokenTracker layer (#10630) * Setting balance to 0x0 when the original value is undefined (#10634) * fix: speedup cancellation (#10579) fixes #7305 * no more node:console (#10640) * prefer chainId over networkId in most cases (#10594) * Move swaps constants to the shared constants directory (#10614) * Position the 3dot menu in the same spot on asset screen and home screen (#10642) * Ensure swaps detail height doesn't create jump in vertical height (#10644) * Fix: ETH 'token' now only appears once in the swaps to and from dropdowns. (#10650) * Prevent network menu highlighting (#10643) * Allow TextField to receive min and max attributes (#10656) * colocate tests in flat structure (#10655) * Fixing migration script generation paths (#10664) * Improve specificity of `test:unit:lax` npm script (#10661) The unit test npm script `test:unit:lax` is now more specific about which tests files to exclude. An `--ignore` CLI option is used to specify the files to ignore, rather than using the braces glob syntax to ignore them from the target glob itself. This makes the option easier to update going forward as we move more tests into the "strict" group, because the options are exactly the same between the two scripts. It also ensures we don't accidentally exclude other subdirectories that happen to also be named `permissions`. In trying to implement this, I stumbled at first because mocha expects the ignore pattern to be a relative path if the target is a relative path (i.e. they need to both start with `./` or neither). The script `test:unit:strict` has been updated to use a relative target pattern for consistency. * Swaps support for local testnet (#10658) * Swaps support for local testnet * Create util method for comparison of token addresses/symbols to default swaps token * Get chainId from txMeta in _trackSwapsMetrics of transaction controller * Add comment to document purpose of getTransactionGroupRecipientAddressFilter * Use isSwapsDefaultTokenSymbol in place of repeated defaultTokenSymbol comparisons in build-quote.js * fix: replace dnode background with JSON-RPC (#10627) fixes #10090 * Don't render faucet row in deposit modal for custom chains (#10674) Fixes MetaMask/metamask-extension#10038 * Change 'Send ETH' title to 'Send' (#10651) * Fixing incorrectly typed token decimal attribute (#10666) * refactor incoming tx controller (#10639) * make migration more safe (#10689) * Adding default properties to NetworkForm (#10682) Fixes MetaMask/metamask-extension#10681 * deps - remove "remotedev-server" (#10687) * deps - remove remotedev-server * Remove stale references from allow-scripts config Any packages that are no longer in the dependency tree have been removed from the `allow-scripts` config. Co-authored-by: Mark Stacey * Excluding sourcemaps comment in production builds (#10695) * Excluding sourcemaps comment in production builds Fixes MetaMask/metamask-extension#7077 * Fix source map explorer script The source map explorer script now re-adds the source map comment to each file to ensure the source map visualization still works. Each module with a sourcemap is copied to a temporary directory along with the module it corresponds to, and from there it's passed into `source-map-explorer`. This should ensure the resulting visualization matches what it was before. Everything has been moved inside of functions to generally improve readability, and to allow the use of local variables. Co-authored-by: Mark Stacey * Delete setupFetchDebugging.js (#10636) * Delete setupFetchDebugging.js * remove fetch-debugging (now handled corrently by sentry) * resolve issue with missing template error (#10692) * resolve issue with missing template error * also apply filtering to confirmation page * rename variable * Add MetaMask to list of BIP44 HD path examples (#10703) The "BIP44 Standard" HD path option in the Ledger connect flow listed only Trezor as an example. It seemed appropriate to include MetaMask as well, since we use the same path. This helps users who have imported their MetaMask seed phrase onto a Ledger device to discover this option. * Removing hard references to 12 word seed phrases in copy (#10704) Adding translation entry for "Wallet Seed" Fixed label padding issue by adding missing CSS rule * rule out empty string for symbol (#10712) * fix: remove unused `metamask.rpcUrl` from redux state + fix tests to reflect that (#10714) * Fix 10706 - Prevent autocomplete from add token input (#10700) * Fix mismatchedChain typo in custom network approval screen (#10723) * Fix 10562 - Hide the suggested token pane when not on Mainnet or test network (#10702) * Update @metamask/controllers to v6.2.1 (#10701) * Additional swaps network support (#10721) * Add swaps support for bnc chain * Use single default token address in shared/constants/swaps * ci - cache deps before patch-package (#10735) * ci - cache deps before patch-package * ci - bump dep cache number (cache break) * build - refactor build system for easier configuration (#10718) * build - refactor build system for easier configuration of before and after bundle * build - fix dependenciesToBundle option * build - fix bify external options and other config * build - refactor for cleanliness * build - fix minify argument * build - fix sourcemaps setup * scripts - refactor setupBundlerDefaults in anticipation of factor bundles * build - scripts - remove unused pipeline label * build - scripts - make filepath entry optional * build - scripts - rename filepath and filename options to entryFilepath and destFilepath * Update development/build/scripts.js Co-authored-by: Mark Stacey Co-authored-by: Mark Stacey * security - update SES lockdown (#10663) * update ses * build - reference ses directly * deps - unify regenerator-runtime versions on 0.13.7 * patches - apply regenerator-runtime ses compat patch\nhttps://github.com/facebook/regenerator/pull/411 * patches - patch regenerator-runtime for latest ses fix * reduc patch, new lockdown severe override taming * updated redux patch * update redux patch for production * ignore lockdown in lint * deps - bump patch-package just in case * trailing comma * remove ses as dep * fix path for frozen promise * remove js extension in lockdown require * Revert "ignore lockdown in lint" This reverts commit 8cefdc94dd25d7781bb09eed8af36441397676da. * Revert "build - reference ses directly" This reverts commit 30371a377dcdd781c1bf9abe55e9c8ae34da26b5. * deps - update ses * Revert "fix path for frozen promise" This reverts commit 966e4c60921a25befe8ca8dea58313cc25852f72. Co-authored-by: kumavis * Allow 11 characters in symbol for RPC (#10670) * Add error in RPC for zero length symbols * Increase RPC symbol length allowed to 11 * Add RPC tests for new symbol length checks * eth-block-tracker@5.0.1 (#10737) * Ensure swaps gas prices are fetched from the correct chain specific endpoint (#10744) * Ensure swaps gas prices are fetched from the correct chain specific endpoint * Just rely on fetchWithCache to cache swaps gas prices, instead of directly using storage in getSwapsPriceEstimatesLastRetrieved * Empty commit * update @metamask/etherscan-link to v2.0.0 (#10747) * Use correct block explorer name and link in swaps when on custom network (#10743) * Use correct block explorer name and link in swaps when on custom network. * Fix up custom etherscan link code in build-quote.js * Use blockExplorerUrl hostname instead of 'blockExplorerBaseUrl' * Use correct etherscan-link method for token links in build-quote * Create correct token link in build-quote for mainnet AND custom networks * Block explorer url improvements in awaiting-swap.js and build-quote.js * Use swapVerifyTokenExplanation message with substitutable block explorer for all applicable locales * Ensure that block explorer links are not shown in awaiting-swap if no url is available * Add New Zealand Dollar to currency options (#10746) * Ensure that the correct default currency symbols are used for fees on the view quote screen (#10753) * Fix 10517 - Prevent tokens without addresses from being added to token list (#10593) * Updating y18n and netmask to resolve dependency issues (#10765) netmask@1.0.6 -> 2.0.1, y18n@3.2.1 -> 3.2.2, y18n@4.0.0 -> 4.0.1 * Refactor Tx State Manager (#10672) Co-authored-by: Mark Stacey * Ensure that the approval fee in the swaps custom gas modal is in network specific currency (#10763) * Ensure that priceSlippage fiat amounts are always shown in view-quote.js (#10762) * Use network specific swaps contract address when checking swap contract token approval (#10774) * Build - refactor background process to use html (#10769) * build - declare background as html * build - fill in empty file when a missing file is expected * lint - fix * Update development/build/manifest.js Co-authored-by: Mark Stacey * Update development/build/manifest.js Co-authored-by: Mark Stacey Co-authored-by: Mark Stacey * build - fix use of empty file to replace unused js files (#10780) * cache lint results for faster repeat execution (#10773) * eslint perf improvement (#10775) * Improve detection of task process exit (#10776) Our build script waits for the `close` event to determine whether the task has exited. The `exit` event is a better representation of this, because if a stream is shared between multiple processes, the process may exit without the `close` event being emitted. We aren't sharing streams between processes, so this edge case doesn't apply to us. This just seemed like a more suitable event to listen to, since we care about the process exiting not the stream ending. See this description of the `close` event from the Node.js documentation [1]: >The `'close'` event is emitted when the stdio streams of a child >process have been closed. This is distinct from the `'exit'` event, >since multiple processes might share the same stdio streams. And see this description of the `exit` event: >The `'exit'` event is emitted after the child process ends. [1]: https://nodejs.org/docs/latest-v14.x/api/child_process.html#child_process_event_exit * Rewrite changelog script from Bash to JavaScript (#10782) The `auto-changelog` script has been rewritten from Bash to JavaScript. Functionally it should behave identically. * Refactoring ethereum-on.spec.js to use fixtures (#10778) * Remove useless negation (#10787) !contentComponent always evaluates to true * Remove date from changelog release header (#10790) New changelog release headers now omit the date. These headers are added automatically when a new release branch is created, and that rarely ends up being the actual date of the release, so these dates have all been inaccurate anyway. The date will be re-added to the changelog later as part of a new script, after a release has been published. * Remove script for creating master sync PR (#10791) The script responsible for creating the "Sync `master` with `develop`" PR has been removed. We will soon be eliminating the need for a `master` branch altogether, so we don't need this anymore. Also, this script hasn't been running correctly in a long time. We've been creating this PR manually. * Add changelog entries under release candidate header (#10784) Instead of always placing new changelog entries under the "Current Develop Branch" header, the changelog script now places them under the header for the current release if that release has not yet been tagged. This eliminates one manual step from the release process. Relates to #10752 * Prevent duplicate changelog entries (#10786) The changelog update script now prevents duplicate entries from being added. Specifically, it will ensure that if a PR has been referenced already in an entry, it will not add it again. This should prevent it from adding duplicate entries for changes that were cherry-picked into hotfix releases. Note that this duplication prevention only works for entries containing a PR number. We don't have any way to prevent duplicate entries yet in cases where we don't know the associated PR. We will be preventing this possibility entirely pretty soon in some upcoming release automation changes though, so I'm not concerned about this omission. * Set the BSC_CONTRACT_ADDRESS to lowercase (#10800) * only applies rules to the appropriate files (#10788) * upgrade eslint deps (#10789) * Ensure correct primary currency image is displayed on home screen and token list (#10777) * Add release header when updating changelog (#10794) The changelog update script now adds a release header if it doesn't find one already that matches the current release candidate version. * remove node-sass dependency (#10797) * Add support for locators into driver abstraction (#10802) * Update changelog headers and fix dates (#10805) The changelog release header format has been updated to match the "keep a changelog" [1] format. Each header is now the bracketed version number followed by a dash, then the release date in ISO-8601 format. The release dates in each header were also updated to match the date of the corresponding GitHub Release [2]. Many of these dates were incorrect because they were set on the day we created the release candidate, rather than on the day of release. Any changelog release entries without a corresponding GitHub release was left with the date already specified. The three oldest release headers were missing dates. For the first two, I used the date of the version bump commit. For the third, I removed it since no changes were listed anyway, and it represented a range of releases rather than a single one. The `auto-changelog.js` script has been updated to account for this new format as well. [1]: https://keepachangelog.com/en/1.0.0/ [2]: https://github.com/MetaMask/metamask-extension/releases * Update the changelog when creating an RC (#10795) The changelog will now be automatically updated when a release branch is created. A new release header along with changelog entries for any new commits will be added. Note that this changelog will still need to be manually cleaned up, but it's one less manual step at least. The old Bash script for adding a new release header to the changelog has been removed, as that functionality is now built into the changelog update script. A new script has been added to commit any changes made to the manifest and changelog. This step used to happen at the end of the bump manifest version script, but now the changelog update relies upon the manifest version bump happening first, so it needed to be re-ordered. The changes should only be committed on the first run of the branch, as it's contingent upon the manifest changing (due to the version bump). Further changelog updates won't trigger new automatic commits. * Refactoring address-book.spec.js to use fixtures (#10804) * Refactoring send-edit.spec.js to use fixtures (#10792) * Fix _getPermittedAccounts type safety (#10819) * Removing unnecessary params from withFixtures function call. (#10831) * Add links to release headers (#10808) Each release header now includes a link to the range of commits included with that release. These links are at the end of the document, in accordance with the "keep a changelog" [1] format. For the purpose of this changelog, the "previous release" is the most recent release mentioned in the changelogs. The diffs ignore any releases that were omitted from the changelog. This is mainly an issue with older releases, so it seemed acceptable. All releases have been documented for a couple of years now, and will be going forward as well. The name of the "Current Develop Branch" section was changed to "Unreleased" to confirm with "keep a changelog". The `auto-changelog.js` script has been updated to update/add these links whenever adding a new release header as well. [1]: https://keepachangelog.com/en/1.0.0/ * Refactoring permissions.spec.js to use fixtures (#10829) * Add validation for the `fee` property from the `/trades` API response (#10836) * Refactoring signature-request.spec.js to use fixtures (#10820) * use locator abstraction in tests folder (#10833) * Move BSC chain ID, rename some BSC-related vars (#10807) * Add categories to each changelog release (#10837) Each changelog release now has category headers. The standard "keep a changelog" [1] categories are used, along with the addition of "Uncategorized" for any changes that have not yet been categorized. The changelog script has been updated to add this "Uncategorized" header if it isn't already present, and to place any new commits under this header. The changelog has been updated to property categorize each change in recent releases, and to place changes in older releases under the header "Uncategorized". [1]: https://keepachangelog.com/en/1.0.0/ * Remove unused CI script (#10840) This script has not been used since #10795. It is now gone. * Add `--rc` flag to changelog script (#10839) The changelog script now accepts an `--rc` flag to tell it whether to add new changes to `Unreleased` or to the header for the current version. Previously this was inferred from whether the current version matched the most recent tag. However this method only works for the first update. Using a flag simplifies this logic, and makes it possible to manually re-run this for further updates to a release candidate. * Add `--help` flag to changelog script (#10846) The changelog script now accepts a `--help` flag, which prints a help text explaining how the script works and what each flag does. * add abstraction for waitForSelector (#10844) * Adds jest dependency (#10845) * Add Jest * Refactor changelog parsing and generation (#10847) The `auto-changelog.js` script has been refactoring into various different modules. This was done in preparation for migrating this to a separate repository, where it can be used in our libraries as well. Functionally this should act _mostly_ the same way, but there have been some changes. It was difficult to make this a pure refactor because of the strategy used to validate the changelog and ensure each addition remained valid. Instead of being updated in-place, the changelog is now parsed upfront and stored as a "Changelog" instance, which is a new class that was written to allow only valid changes. The new changelog is then stringified and completely overwrites the old one. The parsing had to be much more strict, as any unanticipated content would otherwise be erased unintentionally. This script now also normalizes the formatting of the changelog (though the individual change descriptions are still unformatted). The changelog stringification now accommodates non-linear releases as well. For example, you can now release v1.0.1 *after* v2.0.0, and it will be listed in chronological order while also correctly constructing the `compare` URLs for each release. * Increase default slippage from 2% to 3%, show Advanced Options by default (#10842) * Increase default slippage from 2% to 3%, show Advanced Options by default * Disable opening / closing of Advanced Options on the Swap page * Pre-select previously used slippage value when going back to the Swap page * Fix lint issues * Use a callback for setting up an initial customValue * Migrate unreleased changes in changelog (#10853) When updating the changelog for a release candidate, any unreleased changes are now migrated to the release header. Generally we don't make a habit of adding changes to the changelog prior to creating a release candidate, but if any are there we certainly don't want them duplicated. * @metamask/eslint-config*@6.0.0 (#10858) * @metamask/eslint-config*@6.0.0 * Minor eslintrc reorg * Refactoring threebox.spec.js to use fixtures (#10849) * Adjust renderWithProvider to accommodate redux-less components (#10857) * Jest config (#10855) * Setup jest config * Adjust test for jest. * Adjust lint config * Omit swaps ui folder for unit testing * Omit swaps from test:unit:lax * Add jest.config.js to script files * Restore mocks rather than clearing them. * Update jest config and adjust lint to include subdirs * Convert view-quote-price-difference test to jest * Add jest ci and ci coverage scripts. Add jest unit test to general test command * Add test coverage to ci * Use --ignore flag * Fixup * Add @metamask/eslint-config-jest * Update .eslintrc.js Co-authored-by: Mark Stacey * Adds jest-coverage/ Co-authored-by: Mark Stacey * Add jest-coverage/ to prettierignore (#10865) * Add jest coverage (#10868) * Add jest coverage This will add coverage for any tests ran in jest under the `test:coverage:jest` command, which is currently being used in CI. I set the values to the current test coverage in `ui/app/pages/swaps`. * Lint * Fix crash when adding new changelog release header (#10870) The `auto-changelog.js` script crashes when trying to add a new release header. This bug was introduced in #10847. The cause was a simple misnamed parameter. * Change caching for Swaps APIs from 1 hour to 5 minutes (#10871) * Change caching for the /tokens API from 1 hour to 5 minutes * Use 5 minutes caching for /topAssets and /aggregatorMetadata APIs as well * add key literals to driver (#10854) * Quote globs in prettier scripts (#10867) * use waitForSelector instead of until (#10852) * complete abstraction of until method * response to feedback * Adding recovery phrase video to onboarding process (#10717) * Adding recovery phrase video to onboarding process Adding english subtitles * Support textAlign in Box, converting sidebar to Box * Reduce calls of the `/featureFlag` API (#10859) * Remove periodic calls to the /featureFlag API * Always show the Swap button on the main page * Check if the Swaps feature is enabled, show loading animation while waiting * Reuse an existing useEffect call * Use ‘isFeatureFlagLoaded’ in React’s state, resolve lint issues * Add a watch mode for Jest testing * Add unit tests for Swaps: fetchSwapsLiveness, add /ducks/swaps into Jest testing * Remove Swaps Jest tests from Mocha’s ESLint rules * Ignore Swaps Jest tests while running Mocha, update paths * Increase test coverage to the current max * Fix ESLint issues for Swaps * Enable the Swaps feature by default and after state reset, remove loading screen before seeing Swaps * Update Jest config, fix tests * Update Jest coverage threshold to the current maximum * Update ESLint rule in jest.config.js * Disable the “Review Swap” button if the feature flag hasn’t loaded yet * Update jest threshold * Fix 10036 - Prevent odd localStorage migration error in Firefox (#10884) * Add jest watch mode script (#10869) * Refactoring metamask-responsive-ui.spec.js to use fixtures (#10866) * Handling infura blockage (#10883) * Handling infura blockage * Adding blockage home notification * Updating copy, adding temporary notification dismissal * Addressing review feedback * Using eth_blockNumber method to check Infura availability * Handling network changes in availability check * Use jest to run ui/**/*.test.js (#10885) * upgrade ethereumjs util (#10886) * Swaps: Show a network name dynamically in a tooltip (#10882) * Swaps: Show a network name dynamically in a tooltip * Replace “Ethereum” with “$1”, change “Test” to “Testnet” * Replace 이더리움 with $1 * Translate network names, use ‘Ethereum’ by default if a translation is not available yet * Reorder messages to resolve ESLint issues * Add a snapshot test for the FeeCard component, increase Jest threshold * Enable snapshot testing into external .snap files in ESLint * Add the “networkNameEthereum” key in ko/messages.json, remove default “Ethereum” value * Throw an error if chain ID is not supported by the Swaps feature * Use string literals when calling the `t` fn, * Adding option to set Custom Nonce to Confirm Approve Page (#10595) * Dep Upgrades for Lavamoat Patches (#10902) * dep upgrades * apply more patches * Dep upgrades and patches (#10903) * apply patches * lavamoat dep upgrades * remove lavamoat browserify * Increase Jest unit test coverage for the Swaps feature to ~25% (#10900) * Swaps: Show a network name dynamically in a tooltip * Replace “Ethereum” with “$1”, change “Test” to “Testnet” * Replace 이더리움 with $1 * Translate network names, use ‘Ethereum’ by default if a translation is not available yet * Reorder messages to resolve ESLint issues * Add a snapshot test for the FeeCard component, increase Jest threshold * Enable snapshot testing into external .snap files in ESLint * Add the “networkNameEthereum” key in ko/messages.json, remove default “Ethereum” value * Throw an error if chain ID is not supported by the Swaps feature * Use string literals when calling the `t` fn, * Watch Jest tests silently (no React warnings in terminal, only errors) * Add @testing-library/jest-dom, import it before running Jest tests * Add snapshot testing of Swaps’ React components for happy paths, increase minimum threshold for Jest * Add the test/jest folder for Jest setup and shared functions, use it in Swaps Jest tests * Fix ESLint issues, update linting config * Enable ESLint for .snap files (Jest snapshots), throw an error if a snapshot is bigger than 50 lines * Don’t run lint:fix for .snap files * Move `createProps` outside of `describe` blocks, move store creation inside tests * Use translations instead of keys, update a rendering function to load translations * Make sure all Jest snapshots are shorter than 50 lines (default limit) * Add / update props for Swaps tests * Fix React warnings when running tests for Swaps * Bump @metamask/contract-metadata from 1.23.0 to 1.25.0 (#10899) Bumps [@metamask/contract-metadata](https://github.com/MetaMask/contract-metadata) from 1.23.0 to 1.25.0. - [Release notes](https://github.com/MetaMask/contract-metadata/releases) - [Commits](https://github.com/MetaMask/contract-metadata/compare/v1.23.0...v1.25.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Refactoring from-import-ui.spec.js to use fixtures (#10907) * Fix 10458 - Understand where to get support (#10895) * Add contract address validation for token swaps (#10912) * Fixing ENS input entry in send flow (#10923) * Fixing ENS input entry in send flow Fixes MetaMask/metamask-extension#10691 * removed unnecessary apostrophe * Refactoring incremental-security.spec.js to use fixtures (#10917) * use one segment instance (#10915) * Implement Ledger Live bridge (#10293) * add view account-details menu item to token-options menu (#10932) * add view account-details menu item to token-options menu * add onViewAccountDetails propType * Fix 10609 - Prevent overflow of confirmation page hostname (#10935) * upgrade eth-keyring-controller (#10933) * [Fix] 10365 My Accounts Removal (#10680) * pushing my-accounts removal * removed CONTACT_MY_ACCOUNTS_ROUTE * removed CONTACT_MY_ACCOUNTS_VIEW_ROUTE * removing CONTACT_MY_ACCOUNTS_EDIT_ROUTE * removing CONTACT_MY_ACCOUNTS_EDIT_ROUTE * removed showingMyAccounts dead code * removed more dead code related to isMyAccountsPage * removing more dead code * fixed linting error(s) * removing my-accounts component/folder * added empty contact screen ui * styled empty contact page ui * fixed linting, removed add contacts button, and fixed errors * localized text and centered No Contacts * pushing localized verification and fixed e2e test * added listRoute redirect * added listroute and fixed linting error * Increase Jest unit test coverage for the Swaps feature to ~43% (#10934) * add hamburger menu to eth page (#10938) * add hamburger menu to eth page * change token-options to asset-options, use more direct selector for user address fetch * use token custom icons where possible (#10939) * Whats new popup (#10583) * Add 'What's New' notification popup * Move selectors from shared/notifications into ui/ directory * Use keys for localized message in whats new notifications objects, to ensure notifications will be translated. * Remove unused swaps intro popup locale messages * Fix keys of whats new notification locales * Remove notifications messages and descriptions from comment in shared/notifications * Move notifcationActionFunctions to shared/notifications and make it stateless * Get notification data from constants instead of state in whats-new-popup * Code cleanup * Fix build quote reference to swapsEthToken, broken during rebase * Rename notificationFilters to notificationToExclude to clarify its purpose * Documentation for getSortedNotificationsToShow * Move notification action functions from shared/ to whats-new-popup.js * Stop setting swapsWelcomeMessageHasBeenShown to state in app-state controller * Update e2e tests for whats new popup changes * Updating migration files * Addressing feedback part 1 * Addressing feedback part 2 * Remove unnecessary div in whats-new-popup * Change getNotificationsToExclude to getNotificationsToInclude for use in the getSortedNotificationsToShow selector * Delete intro-popup directory and test files * Lint fix * Add notifiction state to address-entry fixture * Use two separate functions for rendering first and subsequent notifications in the whats-new-popup * Ensure that string literals are passed to t for whats new popup text * Update import-ui fixtures to include notificaiton controller state * Remove unnecessary, accidental change confirm-approve * Remove swaps notification in favour of mobile swaps as first notifcation and TBD 3rd notification * Update whats-new-popup to use intersection observer api to detect if notification has been seen * Add notifications to send-edit and threebox e2e test fixtures * Update ui/app/selectors/selectors.js Co-authored-by: Mark Stacey * Update ui/app/selectors/selectors.js Co-authored-by: Mark Stacey * Clean up locale code for whats-new-popup notifications * Disconnect observers in whats-new-popup when their callback is first called * Add test case for migration 58 for when the AppStateController does not exist * Rename popover components containerRef to popoverWrapRef * Fix messages.json * Update notification messages and images * Rename popoverWrapRef -> popoverRef in whats-new-popup and popover.component * Only create one observer, and only after images have loaded, in whats-new-popup * Set width and height on whats-new-popup image, instead of setting state on img load * Update ui/app/components/app/whats-new-popup/whats-new-popup.js Co-authored-by: Mark Stacey * Code clean up in whats new popup re: notification rendering and action functions * Code cleanup in render notification functions of whats-new-popup * Update ui/app/components/app/whats-new-popup/whats-new-popup.js Co-authored-by: Mark Stacey * lint fix * Update and localize notification dates * Clean up date code in shred/notifications/index.js Co-authored-by: ryanml Co-authored-by: Mark Stacey * Revert "Implement Ledger Live bridge (#10293)" This reverts commit 15b596ad15f2d569255c39e5e7babc8b7ab700ca. * Version v9.5.0 * Update changelog for v9.5.0 * yarn allow-scripts and yarn lavamoat:auto for Version-v9.5.0 branch * Anchor the Need Help text to the bottom of the expanded page (#10955) * fix transaction sync logic (#10954) * Add backwards compatibility for ETH <-> WETH contract address validation (#10962) * Whats new popup design fixes (#10964) * Remove padding on QR code image * Allow the qr code to be below the description in the whats new popup * Fix size and position of QR code in whats new notification * Add right caret to action links in whats new popup * Clean up placeImageBelowDescription logic * Fix display of whats-new-popup image * Improve spacing and sizing of whats new popup in both full screen and popup view * refactor * Ensure method of adding contact when contacts exist (#10963) * Fixing alignment issue with bottom notification (#10979) * Revert "Adding recovery phrase video to onboarding process (#10717)" This reverts commit 9e918b602655e0e4620ab19a3959a8fc08ec0f47. * Removing recovery video entry from changelog * Remove tests that are only needed for the commit reverted in 68c5defc * Fix dependency vulnerability by upgrading xmlhttprequest-ssl via yarn.lock (#10990) Co-authored-by: Mark Stacey Co-authored-by: Barry Gitarts Co-authored-by: Dan J Miller Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> Co-authored-by: Brad Decker Co-authored-by: ryanml Co-authored-by: David Walsh Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Co-authored-by: Shane Co-authored-by: gitpurva <47534619+gitpurva@users.noreply.github.com> Co-authored-by: kumavis Co-authored-by: Etienne Dusseault Co-authored-by: kumavis Co-authored-by: Grant Bakker Co-authored-by: Michael Standen Co-authored-by: Muhammet Kara Co-authored-by: Daniel <80175477+dan437@users.noreply.github.com> Co-authored-by: Thomas Huang Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Co-authored-by: Alex Donesky Co-authored-by: Austin Akers Co-authored-by: MetaMask Bot --- .circleci/config.yml | 24 +- .../scripts/release-bump-changelog-version.sh | 44 - .../scripts/release-bump-manifest-version.sh | 10 - .../scripts/release-commit-version-bump.sh | 34 + .circleci/scripts/release-create-master-pr.sh | 52 - .eslintrc.js | 155 +- .gitignore | 2 + .prettierignore | 1 + .storybook/test-data.js | 1 - CHANGELOG.md | 1421 ++++--- app/_locales/am/messages.json | 24 - app/_locales/ar/messages.json | 24 - app/_locales/bg/messages.json | 24 - app/_locales/bn/messages.json | 24 - app/_locales/ca/messages.json | 24 - app/_locales/cs/messages.json | 12 - app/_locales/da/messages.json | 24 - app/_locales/de/messages.json | 24 - app/_locales/el/messages.json | 24 - app/_locales/en/messages.json | 137 +- app/_locales/es/messages.json | 53 +- app/_locales/es_419/messages.json | 53 +- app/_locales/et/messages.json | 24 - app/_locales/fa/messages.json | 24 - app/_locales/fi/messages.json | 24 - app/_locales/fil/messages.json | 24 - app/_locales/fr/messages.json | 24 - app/_locales/he/messages.json | 24 - app/_locales/hi/messages.json | 53 +- app/_locales/hn/messages.json | 12 - app/_locales/hr/messages.json | 24 - app/_locales/ht/messages.json | 15 - app/_locales/hu/messages.json | 24 - app/_locales/id/messages.json | 53 +- app/_locales/it/messages.json | 53 +- app/_locales/ja/messages.json | 53 +- app/_locales/kn/messages.json | 24 - app/_locales/ko/messages.json | 56 +- app/_locales/lt/messages.json | 24 - app/_locales/lv/messages.json | 24 - app/_locales/ms/messages.json | 24 - app/_locales/nl/messages.json | 12 - app/_locales/no/messages.json | 21 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 24 - app/_locales/pt/messages.json | 12 - app/_locales/pt_BR/messages.json | 24 - app/_locales/ro/messages.json | 24 - app/_locales/ru/messages.json | 53 +- app/_locales/sk/messages.json | 24 - app/_locales/sl/messages.json | 24 - app/_locales/sr/messages.json | 24 - app/_locales/sv/messages.json | 24 - app/_locales/sw/messages.json | 24 - app/_locales/ta/messages.json | 12 - app/_locales/th/messages.json | 12 - app/_locales/tl/messages.json | 53 +- app/_locales/tr/messages.json | 12 - app/_locales/uk/messages.json | 24 - app/_locales/vi/messages.json | 53 +- app/_locales/zh_CN/messages.json | 53 +- app/_locales/zh_TW/messages.json | 24 - app/background.html | 15 + app/images/address-book.svg | 1 + app/images/mobile-link-qr.svg | 1 + app/images/support.svg | 3 + app/manifest/_base.json | 11 +- .../account-import-strategies.test.js | 6 +- .../account-import-strategies/index.js | 15 +- app/scripts/background.js | 22 +- app/scripts/controllers/app-state.js | 10 - .../controllers/cached-balances.test.js | 4 +- .../controllers/detect-tokens.test.js | 8 +- app/scripts/controllers/ens/index.js | 6 +- .../scripts/controllers/ens/index.test.js | 4 +- .../controllers/incoming-transactions.js | 293 +- .../controllers/incoming-transactions.test.js | 902 ++--- app/scripts/controllers/metametrics.js | 26 +- .../scripts}/controllers/metametrics.test.js | 18 +- .../controllers/network/createInfuraClient.js | 4 +- .../network/createJsonRpcClient.js | 4 +- .../network/network-controller.test.js | 4 +- .../network/pending-middleware.test.js | 4 +- app/scripts/controllers/permissions/index.js | 8 +- .../permissions-controller.test.js | 18 +- .../permissions-log-controller.test.js | 18 +- .../permissions-middleware.test.js | 15 +- .../controllers/permissions/permissionsLog.js | 6 +- .../permissions/restricted-methods.test.js | 2 +- app/scripts/controllers/preferences.js | 11 +- .../scripts/controllers/preferences.test.js | 25 +- app/scripts/controllers/swaps.js | 98 +- .../scripts}/controllers/swaps.test.js | 289 +- .../token-rates-controller.test.js | 2 +- app/scripts/controllers/token-rates.js | 4 +- app/scripts/controllers/transactions/index.js | 187 +- .../controllers/transactions/index.test.js | 304 +- .../lib}/tx-state-history-helpers.test.js | 4 +- .../controllers/transactions/lib/util.js | 60 +- .../controllers/transactions/lib/util.test.js | 2 +- .../transactions/pending-tx-tracker.test.js | 4 +- .../controllers/transactions/tx-gas-utils.js | 4 +- .../transactions/tx-gas-utils.test.js | 4 +- .../transactions/tx-state-manager.js | 633 ++-- .../transactions/tx-state-manager.test.js | 727 ++-- .../lib}/ComposableObservableStore.test.js | 2 +- app/scripts/lib/buy-eth-url.js | 36 +- .../scripts/lib}/buy-eth-url.test.js | 16 +- .../scripts/lib}/cleanErrorStack.test.js | 2 +- app/scripts/lib/createMetaRPCHandler.js | 33 + app/scripts/lib/createMetaRPCHandler.test.js | 61 + app/scripts/lib/decrypt-message-manager.js | 8 +- .../lib/encryption-public-key-manager.js | 2 +- app/scripts/lib/message-manager.js | 6 +- .../scripts/lib}/message-manager.test.js | 2 +- app/scripts/lib/metaRPCClientFactory.js | 81 + app/scripts/lib/metaRPCClientFactory.test.js | 88 + .../scripts/lib/migrator/index.test.js | 8 +- .../app => app/scripts/lib}/nodeify.test.js | 2 +- app/scripts/lib/personal-message-manager.js | 8 +- .../lib}/personal-message-manager.test.js | 2 +- .../scripts/lib}/seed-phrase-verifier.test.js | 6 +- app/scripts/lib/segment.js | 10 - app/scripts/lib/setupFetchDebugging.js | 41 - app/scripts/lib/setupSentry.js | 2 +- app/scripts/lib/typed-message-manager.js | 2 +- .../lib}/typed-message-manager.test.js | 2 +- app/scripts/lib/util.js | 4 +- .../unit/app => app/scripts/lib}/util.test.js | 5 +- app/scripts/metamask-controller.js | 158 +- .../scripts}/metamask-controller.test.js | 57 +- .../scripts}/migrations/021.test.js | 4 +- .../scripts}/migrations/022.test.js | 2 +- .../scripts}/migrations/023.test.js | 2 +- .../scripts}/migrations/024.test.js | 4 +- .../scripts}/migrations/025.test.js | 4 +- .../scripts}/migrations/026.test.js | 4 +- .../scripts}/migrations/027.test.js | 4 +- .../scripts}/migrations/028.test.js | 4 +- .../scripts}/migrations/029.test.js | 2 +- .../scripts}/migrations/030.test.js | 2 +- .../scripts}/migrations/031.test.js | 2 +- .../scripts}/migrations/033.test.js | 2 +- .../scripts}/migrations/034.test.js | 2 +- .../scripts}/migrations/035.test.js | 2 +- .../scripts}/migrations/036.test.js | 2 +- .../scripts}/migrations/037.test.js | 2 +- .../scripts}/migrations/038.test.js | 2 +- app/scripts/migrations/039.js | 4 +- .../scripts}/migrations/039.test.js | 2 +- .../scripts}/migrations/040.test.js | 2 +- .../scripts}/migrations/041.test.js | 2 +- .../scripts}/migrations/042.test.js | 2 +- .../scripts}/migrations/043.test.js | 2 +- .../scripts}/migrations/044.test.js | 2 +- .../scripts}/migrations/045.test.js | 2 +- .../scripts}/migrations/046.test.js | 2 +- .../scripts}/migrations/047.test.js | 2 +- .../scripts}/migrations/048.test.js | 12 +- .../scripts}/migrations/049.test.js | 2 +- app/scripts/migrations/050.js | 2 +- .../scripts}/migrations/050.test.js | 2 +- .../scripts}/migrations/051.test.js | 2 +- .../scripts}/migrations/052.test.js | 2 +- app/scripts/migrations/053.js | 46 + app/scripts/migrations/053.test.js | 136 + app/scripts/migrations/054.js | 75 + app/scripts/migrations/054.test.js | 687 ++++ app/scripts/migrations/055.js | 43 + app/scripts/migrations/055.test.js | 96 + app/scripts/migrations/056.js | 51 + app/scripts/migrations/056.test.js | 155 + app/scripts/migrations/057.js | 44 + app/scripts/migrations/057.test.js | 193 + app/scripts/migrations/058.js | 23 + app/scripts/migrations/058.test.js | 46 + app/scripts/migrations/index.js | 6 + .../scripts}/migrations/migrations.test.js | 28 +- .../scripts}/migrations/template.test.js | 2 +- app/scripts/phishing-detect.js | 46 +- app/scripts/runLockdown.js | 1 + app/scripts/ui.js | 21 +- development/auto-changelog.js | 67 + development/auto-changelog.sh | 60 - development/build/manifest.js | 24 +- development/build/scripts.js | 422 ++- development/build/static.js | 20 +- development/build/styles.js | 2 +- development/build/task.js | 2 +- {app/scripts => development}/chromereload.js | 0 development/empty.js | 1 + development/generate-migration.sh | 4 +- development/lib/changelog/changelog.js | 305 ++ development/lib/changelog/constants.js | 68 + development/lib/changelog/parseChangelog.js | 84 + development/lib/changelog/updateChangelog.js | 171 + development/lib/runCommand.js | 79 + development/metamaskbot-build-announce.js | 2 +- development/source-map-explorer.sh | 35 +- jest.config.js | 17 + jsconfig.json | 3 + lavamoat/node/policy.json | 114 +- package.json | 80 +- patches/@formatjs+intl-utils+3.3.1.patch | 26 + patches/@reduxjs+toolkit+1.5.0.patch | 82 + patches/fast-json-patch+2.2.1.patch | 13 + patches/luxon+1.26.0.patch | 20 + patches/node-sass+4.14.1.patch | 13 - patches/regenerator-runtime+0.13.7.patch | 67 + patches/undeclared-identifiers+1.1.2.patch | 13 + patches/web3+0.20.7.patch | 39 + shared/constants/network.js | 4 + shared/constants/swaps.js | 17 +- shared/constants/transaction.js | 134 +- .../modules}/fetch-with-timeout.test.js | 2 +- .../lib => shared/modules}/random-id.js | 0 shared/notifications/index.js | 52 + stylelint.config.js | 2 +- test/data/transaction-data.json | 34 +- test/e2e/address-book.spec.js | 323 -- test/e2e/benchmark.js | 8 +- test/e2e/ethereum-on.spec.js | 201 - test/e2e/fixtures/address-entry/state.json | 149 + .../state.json | 16 +- test/e2e/fixtures/import-ui/state.json | 251 ++ test/e2e/fixtures/imported-account/state.json | 16 +- test/e2e/fixtures/localization/state.json | 16 +- test/e2e/fixtures/metrics-enabled/state.json | 16 +- test/e2e/fixtures/onboarding/state.json | 40 + test/e2e/fixtures/send-edit/state.json | 185 + test/e2e/fixtures/threebox-enabled/state.json | 135 + test/e2e/from-import-ui.spec.js | 425 --- test/e2e/helpers.js | 31 +- test/e2e/incremental-security.spec.js | 250 -- test/e2e/metamask-responsive-ui.spec.js | 289 -- test/e2e/metamask-ui.spec.js | 1200 +++--- test/e2e/metrics.spec.js | 6 +- test/e2e/mock-3box/server.js | 38 - test/e2e/mock-3box/threebox-mock-server.js | 57 + test/e2e/permissions.spec.js | 199 - test/e2e/run-all.sh | 61 - test/e2e/send-edit.spec.js | 254 -- test/e2e/signature-request.spec.js | 172 - test/e2e/tests/address-book.spec.js | 121 + test/e2e/tests/from-import-ui.spec.js | 302 ++ test/e2e/tests/incremental-security.spec.js | 179 + test/e2e/tests/localization.spec.js | 8 +- test/e2e/tests/metamask-responsive-ui.spec.js | 204 + test/e2e/tests/permissions.spec.js | 88 + test/e2e/tests/personal-sign.spec.js | 16 +- test/e2e/tests/provider-events.spec.js | 55 + test/e2e/tests/send-edit.spec.js | 96 + test/e2e/tests/signature-request.spec.js | 74 + test/e2e/tests/simple-send.spec.js | 31 +- test/e2e/tests/threebox.spec.js | 97 + test/e2e/threebox.spec.js | 302 -- test/e2e/webdriver/driver.js | 161 +- .../permission-controller-helpers.js} | 11 +- test/{helper.js => helpers/setup-helper.js} | 0 test/jest/background.js | 5 + test/jest/constants.js | 1 + test/jest/index.js | 5 + test/jest/mock-store.js | 79 + test/jest/mocks.js | 61 + test/jest/rendering.js | 59 + test/jest/setup.js | 2 + test/lib/render-helpers.js | 54 +- test/lib/tick.js | 5 + .../permission-controller.js} | 7 +- test/setup.js | 2 +- .../network/stubs.js => stub/tx-meta-stub.js} | 9 +- .../balance-formatter.test.js | 0 test/unit/actions/config.test.js | 33 - test/unit/actions/set_account_label.test.js | 34 - .../unit/actions/set_selected_account.test.js | 24 - test/unit/actions/tx.test.js | 54 - test/unit/actions/warning.test.js | 24 - test/unit/localhostState.js | 23 - .../responsive/components/dropdown.test.js | 44 - ui/app/__mocks__/react-router-dom.js | 11 + .../account-list-item-component.test.js | 111 +- .../account-menu/account-menu.component.js | 9 +- .../{tests => }/account-menu.test.js | 127 +- .../unconnected-account-alert.test.js | 49 +- ui/app/components/app/app-components.scss | 1 + .../app-header/{tests => }/app-header.test.js | 48 +- .../app/asset-list-item/asset-list-item.js | 32 +- .../confirm-detail-row.component.test.js | 59 +- ...rm-page-container-header.component.test.js | 29 +- .../confirm-page-container.component.js | 3 +- .../dropdowns/{components => }/dropdown.js | 2 +- .../components/app/dropdowns/dropdown.test.js | 34 + .../app/dropdowns/network-dropdown.js | 3 +- .../app/dropdowns/network-dropdown.test.js | 112 + .../app/dropdowns/tests/dropdown.test.js | 35 - .../dropdowns/tests/network-dropdown.test.js | 112 - .../advanced-gas-input-component.test.js | 60 +- .../advanced-tab-content-component.test.js | 56 +- .../basic-tab-content-component.test.js | 55 +- ...gas-modal-page-container-component.test.js | 137 +- ...gas-modal-page-container-container.test.js | 284 ++ .../gas-modal-page-container.container.js | 12 +- ...gas-modal-page-container-container.test.js | 515 --- .../gas-price-button-group-component.test.js | 202 +- .../app/info-box/{tests => }/info-box.test.js | 19 +- .../loading-network-screen.component.js | 6 +- .../loading-network-screen.container.js | 6 +- .../app/menu-bar/{tests => }/menu-bar.test.js | 21 +- .../modal-content.component.test.js | 40 + .../tests/modal-content.component.test.js | 45 - .../components/app/modal/modal.component.js | 5 + .../modal/{tests => }/modal.component.test.js | 77 +- .../account-details-modal.test.js | 25 +- ...ncel-transaction-gas-fee.component.test.js | 25 + ...ncel-transaction-gas-fee.component.test.js | 28 - .../cancel-transaction.component.test.js | 60 + .../cancel-transaction.container.js | 40 +- .../cancel-transaction.component.test.js | 65 - .../confirm-delete-network.test.js | 27 +- .../confirm-remove-account.test.js | 31 +- .../{tests => }/confirm-reset-account.test.js | 22 +- .../customize-nonce.component.js | 132 + .../app/modals/customize-nonce/index.js | 1 + .../app/modals/customize-nonce/index.scss | 53 + .../deposit-ether-modal.component.js | 29 +- .../deposit-ether-modal.container.js | 13 +- ui/app/components/app/modals/index.scss | 1 + .../metametrics-opt-in-modal.test.js | 47 +- ui/app/components/app/modals/modal.js | 14 + .../{tests => }/reject-transactions.test.js | 22 +- .../{tests => }/transaction-confirmed.test.js | 11 +- .../components/app/network-display/index.scss | 1 + .../app/network-display/network-display.js | 7 +- .../selected-account-component.test.js | 13 +- .../app/sidebars/sidebar.component.js | 4 +- ...nent.test.js => sidebar.component.test.js} | 78 +- .../signature-request-original.component.js | 4 +- ...js => signature-request.component.test.js} | 17 +- .../signature-request.container.test.js} | 19 +- .../app/token-cell/token-cell.test.js | 35 +- ...transaction-activity-log.component.test.js | 29 +- ...transaction-activity-log.container.test.js | 30 +- .../transaction-activity-log.util.test.js | 32 +- ...ransaction-breakdown-row.component.test.js | 34 +- .../transaction-breakdown.component.test.js | 13 +- .../transaction-breakdown.container.js | 5 +- ...transaction-list-item-details.component.js | 5 +- ...action-list-item-details.component.test.js | 62 +- .../transaction-list-item.component.js | 16 +- .../transaction-list.component.js | 12 +- .../transaction-status.component.test.js | 71 - .../transaction-status.component.test.js | 63 + ...erenced-currency-display.component.test.js | 40 - ...-preferenced-currency-display.component.js | 26 +- ...erenced-currency-display.component.test.js | 41 + ...eferenced-currency-input.component.test.js | 28 - ...eferenced-currency-input.container.test.js | 31 - ...eferenced-currency-input.component.test.js | 27 + ...eferenced-currency-input.container.test.js | 29 + ...-preferenced-token-input.component.test.js | 33 - ...-preferenced-token-input.container.test.js | 31 - ...-preferenced-token-input.component.test.js | 32 + ...-preferenced-token-input.container.test.js | 29 + .../app/wallet-overview/eth-overview.js | 58 +- .../app/wallet-overview/token-overview.js | 72 +- .../components/app/whats-new-popup/index.js | 1 + .../components/app/whats-new-popup/index.scss | 61 + .../app/whats-new-popup/whats-new-popup.js | 181 + ...cccount-mismatch-warning.component.test.js | 27 +- .../{tests/alert.test.js => index.test.js} | 25 +- ui/app/components/ui/box/box.js | 4 +- .../breadcrumbs/breadcrumbs.component.test.js | 22 + .../tests/breadcrumbs.component.test.js | 26 - .../button-group-component.test.js | 84 +- .../components/ui/button/button.component.js | 16 +- .../components/ui/card/card.component.test.js | 21 + .../ui/card/tests/card.component.test.js | 22 - .../confusable/confusable.component.test.js | 25 + .../test/confusable.component.test.js | 26 - .../currency-display.component.js | 24 +- .../currency-display.component.test.js | 25 +- .../currency-input.component.test.js | 201 +- .../currency-input.container.test.js | 35 +- .../definition-list.stories.js | 4 +- .../error-message.component.test.js | 33 + .../tests/error-message.component.test.js | 36 - .../hex-to-decimal.component.test.js | 17 +- .../components/ui/icon-button/icon-button.js | 12 +- .../ui/identicon/identicon.component.js | 5 +- .../{tests => }/identicon.component.test.js | 26 +- .../ui/list-item/list-item.component.js | 18 +- .../ui/list-item/list-item.component.test.js | 80 + .../ui/list-item/tests/list-item.test.js | 84 - ui/app/components/ui/menu/menu-item.js | 18 +- .../metafox-logo.component.test.js | 29 +- .../page-container-footer.component.test.js | 87 + .../page-container-footer.component.test.js | 96 - .../page-container-header.component.test.js | 85 + .../page-container-header.component.test.js | 91 - .../ui/popover/popover.component.js | 10 +- ui/app/components/ui/tabs/index.scss | 1 + .../components/ui/tabs/tab/tab.component.js | 12 +- .../ui/text-field/text-field.component.js | 51 +- .../{tests => }/token-input.component.test.js | 178 +- .../truncated-definition-list.stories.js | 4 +- .../tests/unit-input.component.test.js | 99 - .../unit-input/unit-input.component.test.js | 98 + ui/app/ducks/app/app.js | 13 + .../reducers => ui/app/ducks/app}/app.test.js | 173 +- .../confirm-transaction.duck.test.js | 162 +- ui/app/ducks/gas/gas-duck.test.js | 138 +- ui/app/ducks/metamask/metamask.js | 1 - .../app/ducks/metamask}/metamask.test.js | 133 +- ui/app/ducks/send/send-duck.test.js | 107 +- ui/app/ducks/swaps/swaps.js | 39 +- ui/app/ducks/swaps/swaps.test.js | 118 + .../constants/available-conversions.json | 4 + ui/app/helpers/constants/routes.js | 11 - ui/app/helpers/constants/transactions.js | 8 +- .../{tests => }/with-modal-props.test.js | 21 +- ui/app/helpers/utils/common.util.test.js | 9 +- ui/app/helpers/utils/confirm-tx.util.test.js | 99 +- ui/app/helpers/utils/conversion-util.js | 4 +- ui/app/helpers/utils/conversion-util.test.js | 112 +- ui/app/helpers/utils/conversions.util.test.js | 31 +- ui/app/helpers/utils/fetch-with-cache.test.js | 107 +- ui/app/helpers/utils/i18n-helper.test.js | 47 +- ui/app/helpers/utils/transactions.util.js | 41 +- .../helpers/utils/transactions.util.test.js | 27 +- ui/app/helpers/utils/util.js | 129 +- ui/app/helpers/utils/util.test.js | 349 +- .../hooks/tests/useRetryTransaction.test.js | 71 - ui/app/hooks/useCancelTransaction.js | 58 +- .../{tests => }/useCancelTransaction.test.js | 104 +- .../{tests => }/useCurrencyDisplay.test.js | 21 +- ui/app/hooks/useRetryTransaction.js | 6 +- ui/app/hooks/useRetryTransaction.test.js | 113 + ui/app/hooks/useSwappedTokenValue.js | 10 +- ui/app/hooks/{tests => }/useTokenData.test.js | 32 +- .../{tests => }/useTokenDisplayValue.test.js | 17 +- ui/app/hooks/useTokenTracker.js | 10 +- ui/app/hooks/useTransactionDisplayData.js | 46 +- .../useTransactionDisplayData.test.js | 89 +- .../useUserPreferencedCurrency.test.js | 18 +- ui/app/pages/add-token/add-token.component.js | 39 +- ui/app/pages/add-token/add-token.container.js | 2 + .../add-token/{tests => }/add-token.test.js | 54 +- .../token-search/token-search.component.js | 1 + ui/app/pages/asset/asset.scss | 4 +- .../pages/asset/components/asset-options.js | 80 + ui/app/pages/asset/components/native-asset.js | 32 +- ui/app/pages/asset/components/token-asset.js | 7 +- .../pages/asset/components/token-options.js | 60 - .../confirm-approve-content.component.js | 146 +- .../confirm-approve-content/index.scss | 32 + .../pages/confirm-approve/confirm-approve.js | 59 +- .../confirm-approve/confirm-approve.util.js | 4 +- .../confirm-deploy-contract.component.js | 4 +- .../confirm-transaction-base.component.js | 24 +- ...confirm-transaction-base.component.test.js | 15 + .../confirm-transaction-base.container.js | 12 +- ...confirm-transaction-base.component.test.js | 17 - .../confirm-transaction-switch.component.js | 16 +- .../confirm-transaction.container.js | 4 +- ui/app/pages/confirmation/confirmation.scss | 9 + .../create-account/connect-hardware/index.js | 2 + .../{tests => }/create-account.test.js | 25 +- .../import-with-seed-phrase.component.test.js | 42 +- .../end-of-flow/end-of-flow.test.js | 38 + .../end-of-flow/tests/end-of-flow.test.js | 37 - .../first-time-flow-switch.test.js | 73 +- .../{tests => }/metametrics-opt-in.test.js | 13 +- .../confirm-seed-phrase-component.test.js | 47 +- .../{tests => }/reveal-seed-phrase.test.js | 21 +- .../select-action/select-action.test.js | 51 + .../select-action/tests/select-action.test.js | 48 - .../welcome/{tests => }/welcome.test.js | 19 +- ui/app/pages/home/home.component.js | 33 +- ui/app/pages/home/home.container.js | 16 +- ui/app/pages/home/index.scss | 9 + ui/app/pages/keychains/index.scss | 6 + ui/app/pages/keychains/restore-vault.js | 4 +- .../keychains/{tests => }/reveal-seed.test.js | 10 +- ui/app/pages/lock/{tests => }/lock.test.js | 20 +- ui/app/pages/routes/routes.component.js | 13 +- ui/app/pages/routes/routes.container.js | 3 +- ...est.js => add-recipient.component.test.js} | 100 +- .../add-recipient.container.test.js | 64 + .../add-recipient/add-recipient.js | 13 +- .../add-recipient/add-recipient.utils.test.js | 109 + .../add-recipient/ens-input.component.js | 2 +- .../tests/add-recipient-container.test.js | 67 - .../tests/add-recipient-utils.test.js | 117 - ...js => amount-max-button.component.test.js} | 47 +- .../amount-max-button.container.test.js | 83 + ...est.js => amount-max-button.utils.test.js} | 21 +- .../tests/amount-max-button-container.test.js | 89 - ...t.js => send-amount-row.component.test.js} | 101 +- .../send-amount-row.container.test.js | 94 + .../tests/send-amount-row-container.test.js | 102 - .../send-asset-row.component.js | 8 +- .../send-asset-row.container.js | 2 + .../send-content.component.test.js | 144 + .../gas-fee-display.component.test.js | 38 +- ...test.js => send-gas-row.component.test.js} | 79 +- .../send-gas-row.container.test.js | 156 + .../tests/send-gas-row-container.test.js | 166 - .../send-row-error-message.component.test.js | 32 + .../send-row-error-message.container.test.js | 28 + .../send-row-error-message-component.test.js | 33 - .../send-row-error-message-container.test.js | 28 - ....js => send-row-wrapper.component.test.js} | 73 +- .../tests/send-content-component.test.js | 157 - .../send/send-footer/send-footer.component.js | 1 - ....test.js => send-footer.component.test.js} | 105 +- .../send-footer/send-footer.container.test.js | 187 + ...tils.test.js => send-footer.utils.test.js} | 134 +- .../tests/send-footer-container.test.js | 188 - .../send-header/send-header.component.test.js | 73 + .../tests/send-header-component.test.js | 75 - ui/app/pages/send/send.component.js | 16 +- ui/app/pages/send/send.component.test.js | 467 +++ ui/app/pages/send/send.container.js | 4 +- ui/app/pages/send/send.container.test.js | 130 + .../send-utils.test.js => send.utils.test.js} | 360 +- .../pages/send/tests/send-component.test.js | 540 --- .../pages/send/tests/send-container.test.js | 131 - ...test.js => advanced-tab.component.test.js} | 17 +- .../contact-list-tab.component.js | 110 +- .../contact-list-tab.container.js | 19 +- .../edit-contact/edit-contact.component.js | 1 + .../edit-contact/edit-contact.container.js | 16 +- .../settings/contact-list-tab/index.scss | 54 +- .../contact-list-tab/my-accounts/index.js | 1 - .../my-accounts/my-accounts.component.js | 34 - .../my-accounts/my-accounts.container.js | 15 - .../view-contact/view-contact.container.js | 15 +- .../network-form/network-form.component.js | 8 + ...test.js => security-tab.container.test.js} | 28 +- ...test.js => settings-tab.container.test.js} | 29 +- ui/app/pages/settings/settings.component.js | 18 - ui/app/pages/settings/settings.container.js | 28 +- .../swaps/__snapshots__/index.test.js.snap | 31 + .../actionable-message.test.js.snap | 16 + .../actionable-message/actionable-message.js | 2 +- .../actionable-message.test.js | 22 + .../__snapshots__/awaiting-swap.test.js.snap | 45 + .../quotes-timeout-icon.test.js.snap | 18 + .../swap-failure-icon.test.js.snap | 18 + .../swap-success-icon.test.js.snap | 18 + .../swaps/awaiting-swap/awaiting-swap.test.js | 37 + .../awaiting-swap/quotes-timeout-icon.test.js | 11 + .../awaiting-swap/swap-failure-icon.test.js | 11 + .../awaiting-swap/swap-success-icon.test.js | 11 + .../view-on-ether-scan-link.test.js.snap | 21 + .../view-on-ether-scan-link.test.js | 37 + .../__snapshots__/build-quote.test.js.snap | 66 + ui/app/pages/swaps/build-quote/build-quote.js | 4 + .../swaps/build-quote/build-quote.test.js | 47 + ui/app/pages/swaps/build-quote/index.scss | 1 + .../swaps/countdown-timer/countdown-timer.js | 1 + .../countdown-timer/countdown-timer.test.js | 31 + .../dropdown-input-pair.test.js.snap | 20 + .../dropdown-input-pair.js | 20 +- .../dropdown-input-pair.test.js | 23 + .../dropdown-search-list.test.js.snap | 40 + .../dropdown-search-list.test.js | 25 + .../exchange-rate-display.test.js.snap | 45 + .../exchange-rate-display.test.js | 28 + .../__snapshots__/fee-card.test.js.snap | 70 + ui/app/pages/swaps/fee-card/fee-card.js | 22 +- ui/app/pages/swaps/fee-card/fee-card.test.js | 61 + ui/app/pages/swaps/fee-card/pig-icon.test.js | 11 + ui/app/pages/swaps/index.js | 12 +- ui/app/pages/swaps/index.scss | 1 - ui/app/pages/swaps/index.test.js | 63 + ui/app/pages/swaps/intro-popup/index.js | 1 - ui/app/pages/swaps/intro-popup/index.scss | 71 - ui/app/pages/swaps/intro-popup/intro-popup.js | 108 - .../aggregator-logo.test.js.snap | 18 + ...swaps-quotes-stories-metadata.test.js.snap | 30 + .../aggregator-logo.test.js | 22 + .../background-animation.test.js | 12 + ...ding-swaps-quotes-stories-metadata.test.js | 7 + .../main-quote-summary.test.js.snap | 109 + .../__snapshots__/quote-backdrop.test.js.snap | 74 + .../main-quote-summary.test.js | 39 + .../main-quote-summary/quote-backdrop.test.js | 23 + .../searchable-item-list.test.js.snap | 77 + .../list-item-search.component.js | 2 +- .../searchable-item-list.test.js | 60 + .../select-quote-popover.test.js.snap | 9 + .../mock-quote-data.test.js | 18 + .../select-quote-popover.test.js | 24 + .../__snapshots__/sort-list.test.js.snap | 139 + .../sort-list/sort-list.test.js | 83 + .../slippage-buttons.test.js.snap | 42 + .../pages/swaps/slippage-buttons/index.scss | 7 +- .../slippage-buttons/slippage-buttons.js | 206 +- .../slippage-buttons/slippage-buttons.test.js | 31 + .../__snapshots__/swaps-footer.test.js.snap | 41 + .../swaps/swaps-footer/swaps-footer.test.js | 28 + .../pages/swaps/swaps-util-test-constants.js | 1 + ui/app/pages/swaps/swaps.util.js | 57 +- ui/app/pages/swaps/swaps.util.test.js | 321 +- .../view-quote-price-difference.test.js | 29 +- ui/app/pages/swaps/view-quote/view-quote.js | 1 + ui/app/pages/unlock-page/index.scss | 21 +- .../unlock-page/tests/unlock-page.test.js | 63 - .../unlock-page/unlock-page.component.js | 35 +- .../unlock-page/unlock-page.component.test.js | 29 + .../unlock-page/unlock-page.container.js | 1 - .../unlock-page/unlock-page.container.test.js | 48 + ui/app/selectors/confirm-transaction.js | 8 +- .../{tests => }/confirm-transaction.test.js | 35 +- .../selectors/{tests => }/custom-gas.test.js | 91 +- .../selectors/{tests => }/permissions.test.js | 180 +- ui/app/selectors/selectors.js | 61 +- ui/app/selectors/selectors.test.js | 94 + .../{tests => }/send-selectors-test-data.js | 3 +- ui/app/selectors/send.js | 6 +- ui/app/selectors/{tests => }/send.test.js | 286 +- ui/app/selectors/tests/selectors.test.js | 102 - ui/app/selectors/transactions.js | 13 +- .../{tests => }/transactions.test.js | 61 +- ui/app/store/actionConstants.js | 3 + ui/app/store/actionConstants.test.js | 84 + ui/app/store/actions.js | 56 +- .../ui/app => ui/app/store}/actions.test.js | 864 +++-- ui/index.js | 12 +- {test/unit => ui}/lib/account-link.test.js | 15 +- .../transactions => ui/lib}/tx-helper.test.js | 13 +- yarn.lock | 3315 +++++++++++------ 633 files changed, 22514 insertions(+), 18155 deletions(-) delete mode 100755 .circleci/scripts/release-bump-changelog-version.sh create mode 100755 .circleci/scripts/release-commit-version-bump.sh delete mode 100755 .circleci/scripts/release-create-master-pr.sh create mode 100644 app/background.html create mode 100644 app/images/address-book.svg create mode 100644 app/images/mobile-link-qr.svg create mode 100644 app/images/support.svg rename {test/unit/app => app/scripts/account-import-strategies}/account-import-strategies.test.js (93%) rename {test/unit/app => app/scripts}/controllers/cached-balances.test.js (95%) rename {test/unit/app => app/scripts}/controllers/detect-tokens.test.js (96%) rename test/unit/app/controllers/ens-controller.test.js => app/scripts/controllers/ens/index.test.js (98%) rename {test/unit/app => app/scripts}/controllers/incoming-transactions.test.js (60%) rename {test/unit/app => app/scripts}/controllers/metametrics.test.js (96%) rename {test/unit/app => app/scripts}/controllers/network/network-controller.test.js (95%) rename {test/unit/app => app/scripts}/controllers/network/pending-middleware.test.js (96%) rename {test/unit/app => app/scripts}/controllers/permissions/permissions-controller.test.js (99%) rename {test/unit/app => app/scripts}/controllers/permissions/permissions-log-controller.test.js (98%) rename {test/unit/app => app/scripts}/controllers/permissions/permissions-middleware.test.js (98%) rename {test/unit/app => app/scripts}/controllers/permissions/restricted-methods.test.js (98%) rename test/unit/app/controllers/preferences-controller.test.js => app/scripts/controllers/preferences.test.js (97%) rename {test/unit/app => app/scripts}/controllers/swaps.test.js (83%) rename {test/unit/app => app/scripts}/controllers/token-rates-controller.test.js (92%) rename test/unit/app/controllers/transactions/tx-controller.test.js => app/scripts/controllers/transactions/index.test.js (76%) rename {test/unit/app/controllers/transactions => app/scripts/controllers/transactions/lib}/tx-state-history-helpers.test.js (96%) rename test/unit/app/controllers/transactions/tx-utils.test.js => app/scripts/controllers/transactions/lib/util.test.js (98%) rename {test/unit/app => app/scripts}/controllers/transactions/pending-tx-tracker.test.js (99%) rename test/unit/app/controllers/transactions/tx-gas-util.test.js => app/scripts/controllers/transactions/tx-gas-utils.test.js (94%) rename {test/unit/app => app/scripts}/controllers/transactions/tx-state-manager.test.js (62%) rename {test/unit/app => app/scripts/lib}/ComposableObservableStore.test.js (93%) rename {test/unit/app => app/scripts/lib}/buy-eth-url.test.js (78%) rename {test/unit/app => app/scripts/lib}/cleanErrorStack.test.js (92%) create mode 100644 app/scripts/lib/createMetaRPCHandler.js create mode 100644 app/scripts/lib/createMetaRPCHandler.test.js rename {test/unit/app => app/scripts/lib}/message-manager.test.js (98%) create mode 100644 app/scripts/lib/metaRPCClientFactory.js create mode 100644 app/scripts/lib/metaRPCClientFactory.test.js rename test/unit/migrations/migrator.test.js => app/scripts/lib/migrator/index.test.js (92%) rename {test/unit/app => app/scripts/lib}/nodeify.test.js (97%) rename {test/unit/app => app/scripts/lib}/personal-message-manager.test.js (98%) rename {test/unit/app => app/scripts/lib}/seed-phrase-verifier.test.js (95%) delete mode 100644 app/scripts/lib/setupFetchDebugging.js rename {test/unit/app => app/scripts/lib}/typed-message-manager.test.js (97%) rename {test/unit/app => app/scripts/lib}/util.test.js (98%) rename {test/unit/app/controllers => app/scripts}/metamask-controller.test.js (96%) rename {test/unit => app/scripts}/migrations/021.test.js (80%) rename {test/unit => app/scripts}/migrations/022.test.js (95%) rename {test/unit => app/scripts}/migrations/023.test.js (98%) rename {test/unit => app/scripts}/migrations/024.test.js (93%) rename {test/unit => app/scripts}/migrations/025.test.js (93%) rename {test/unit => app/scripts}/migrations/026.test.js (90%) rename {test/unit => app/scripts}/migrations/027.test.js (92%) rename {test/unit => app/scripts}/migrations/028.test.js (94%) rename {test/unit => app/scripts}/migrations/029.test.js (96%) rename {test/unit => app/scripts}/migrations/030.test.js (95%) rename {test/unit => app/scripts}/migrations/031.test.js (96%) rename {test/unit => app/scripts}/migrations/033.test.js (93%) rename {test/unit => app/scripts}/migrations/034.test.js (97%) rename {test/unit => app/scripts}/migrations/035.test.js (97%) rename {test/unit => app/scripts}/migrations/036.test.js (97%) rename {test/unit => app/scripts}/migrations/037.test.js (98%) rename {test/unit => app/scripts}/migrations/038.test.js (95%) rename {test/unit => app/scripts}/migrations/039.test.js (99%) rename {test/unit => app/scripts}/migrations/040.test.js (94%) rename {test/unit => app/scripts}/migrations/041.test.js (96%) rename {test/unit => app/scripts}/migrations/042.test.js (96%) rename {test/unit => app/scripts}/migrations/043.test.js (95%) rename {test/unit => app/scripts}/migrations/044.test.js (96%) rename {test/unit => app/scripts}/migrations/045.test.js (96%) rename {test/unit => app/scripts}/migrations/046.test.js (94%) rename {test/unit => app/scripts}/migrations/047.test.js (97%) rename {test/unit => app/scripts}/migrations/048.test.js (98%) rename {test/unit => app/scripts}/migrations/049.test.js (97%) rename {test/unit => app/scripts}/migrations/050.test.js (97%) rename {test/unit => app/scripts}/migrations/051.test.js (98%) rename {test/unit => app/scripts}/migrations/052.test.js (99%) create mode 100644 app/scripts/migrations/053.js create mode 100644 app/scripts/migrations/053.test.js create mode 100644 app/scripts/migrations/054.js create mode 100644 app/scripts/migrations/054.test.js create mode 100644 app/scripts/migrations/055.js create mode 100644 app/scripts/migrations/055.test.js create mode 100644 app/scripts/migrations/056.js create mode 100644 app/scripts/migrations/056.test.js create mode 100644 app/scripts/migrations/057.js create mode 100644 app/scripts/migrations/057.test.js create mode 100644 app/scripts/migrations/058.js create mode 100644 app/scripts/migrations/058.test.js rename {test/unit => app/scripts}/migrations/migrations.test.js (89%) rename {test/unit => app/scripts}/migrations/template.test.js (82%) create mode 100755 development/auto-changelog.js delete mode 100755 development/auto-changelog.sh rename {app/scripts => development}/chromereload.js (100%) create mode 100644 development/empty.js create mode 100644 development/lib/changelog/changelog.js create mode 100644 development/lib/changelog/constants.js create mode 100644 development/lib/changelog/parseChangelog.js create mode 100644 development/lib/changelog/updateChangelog.js create mode 100644 development/lib/runCommand.js create mode 100644 jest.config.js create mode 100644 jsconfig.json create mode 100644 patches/@formatjs+intl-utils+3.3.1.patch create mode 100644 patches/@reduxjs+toolkit+1.5.0.patch create mode 100644 patches/fast-json-patch+2.2.1.patch create mode 100644 patches/luxon+1.26.0.patch delete mode 100644 patches/node-sass+4.14.1.patch create mode 100644 patches/regenerator-runtime+0.13.7.patch create mode 100644 patches/undeclared-identifiers+1.1.2.patch create mode 100644 patches/web3+0.20.7.patch rename {test/unit/app => shared/modules}/fetch-with-timeout.test.js (95%) rename {app/scripts/lib => shared/modules}/random-id.js (100%) create mode 100644 shared/notifications/index.js delete mode 100644 test/e2e/address-book.spec.js delete mode 100644 test/e2e/ethereum-on.spec.js create mode 100644 test/e2e/fixtures/address-entry/state.json rename test/e2e/fixtures/{personal-sign => connected-state}/state.json (94%) create mode 100644 test/e2e/fixtures/import-ui/state.json create mode 100644 test/e2e/fixtures/onboarding/state.json create mode 100644 test/e2e/fixtures/send-edit/state.json create mode 100644 test/e2e/fixtures/threebox-enabled/state.json delete mode 100644 test/e2e/from-import-ui.spec.js delete mode 100644 test/e2e/incremental-security.spec.js delete mode 100644 test/e2e/metamask-responsive-ui.spec.js delete mode 100644 test/e2e/mock-3box/server.js create mode 100644 test/e2e/mock-3box/threebox-mock-server.js delete mode 100644 test/e2e/permissions.spec.js delete mode 100644 test/e2e/send-edit.spec.js delete mode 100644 test/e2e/signature-request.spec.js create mode 100644 test/e2e/tests/address-book.spec.js create mode 100644 test/e2e/tests/from-import-ui.spec.js create mode 100644 test/e2e/tests/incremental-security.spec.js create mode 100644 test/e2e/tests/metamask-responsive-ui.spec.js create mode 100644 test/e2e/tests/permissions.spec.js create mode 100644 test/e2e/tests/provider-events.spec.js create mode 100644 test/e2e/tests/send-edit.spec.js create mode 100644 test/e2e/tests/signature-request.spec.js create mode 100644 test/e2e/tests/threebox.spec.js delete mode 100644 test/e2e/threebox.spec.js rename test/{unit/app/controllers/permissions/helpers.js => helpers/permission-controller-helpers.js} (94%) rename test/{helper.js => helpers/setup-helper.js} (100%) create mode 100644 test/jest/background.js create mode 100644 test/jest/constants.js create mode 100644 test/jest/index.js create mode 100644 test/jest/mock-store.js create mode 100644 test/jest/mocks.js create mode 100644 test/jest/rendering.js create mode 100644 test/jest/setup.js create mode 100644 test/lib/tick.js rename test/{unit/app/controllers/permissions/mocks.js => mocks/permission-controller.js} (98%) rename test/{unit/app/controllers/network/stubs.js => stub/tx-meta-stub.js} (94%) rename test/{unit => unit-global}/balance-formatter.test.js (100%) delete mode 100644 test/unit/actions/config.test.js delete mode 100644 test/unit/actions/set_account_label.test.js delete mode 100644 test/unit/actions/set_selected_account.test.js delete mode 100644 test/unit/actions/tx.test.js delete mode 100644 test/unit/actions/warning.test.js delete mode 100644 test/unit/localhostState.js delete mode 100644 test/unit/responsive/components/dropdown.test.js create mode 100644 ui/app/__mocks__/react-router-dom.js rename ui/app/components/app/account-list-item/{tests => }/account-list-item-component.test.js (53%) rename ui/app/components/app/account-menu/{tests => }/account-menu.test.js (50%) rename ui/app/components/app/alerts/unconnected-account-alert/{tests => }/unconnected-account-alert.test.js (76%) rename ui/app/components/app/app-header/{tests => }/app-header.test.js (61%) rename ui/app/components/app/confirm-page-container/confirm-detail-row/{tests => }/confirm-detail-row.component.test.js (65%) rename ui/app/components/app/confirm-page-container/confirm-page-container-header/{tests => }/confirm-page-container-header.component.test.js (63%) rename ui/app/components/app/dropdowns/{components => }/dropdown.js (98%) create mode 100644 ui/app/components/app/dropdowns/dropdown.test.js create mode 100644 ui/app/components/app/dropdowns/network-dropdown.test.js delete mode 100644 ui/app/components/app/dropdowns/tests/dropdown.test.js delete mode 100644 ui/app/components/app/dropdowns/tests/network-dropdown.test.js rename ui/app/components/app/gas-customization/advanced-gas-inputs/{tests => }/advanced-gas-input-component.test.js (53%) rename ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/{tests => }/advanced-tab-content-component.test.js (58%) rename ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/{tests => }/basic-tab-content-component.test.js (56%) rename ui/app/components/app/gas-customization/gas-modal-page-container/{tests => }/gas-modal-page-container-component.test.js (64%) create mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js delete mode 100644 ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js rename ui/app/components/app/gas-customization/gas-price-button-group/{tests => }/gas-price-button-group-component.test.js (64%) rename ui/app/components/app/info-box/{tests => }/info-box.test.js (54%) rename ui/app/components/app/menu-bar/{tests => }/menu-bar.test.js (71%) create mode 100644 ui/app/components/app/modal/modal-content/modal-content.component.test.js delete mode 100644 ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js rename ui/app/components/app/modal/{tests => }/modal.component.test.js (53%) rename ui/app/components/app/modals/{tests => account-details-modal}/account-details-modal.test.js (68%) create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js delete mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js create mode 100644 ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.test.js delete mode 100644 ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js rename ui/app/components/app/modals/confirm-delete-network/{tests => }/confirm-delete-network.test.js (57%) rename ui/app/components/app/modals/confirm-remove-account/{tests => }/confirm-remove-account.test.js (68%) rename ui/app/components/app/modals/confirm-reset-account/{tests => }/confirm-reset-account.test.js (61%) create mode 100644 ui/app/components/app/modals/customize-nonce/customize-nonce.component.js create mode 100644 ui/app/components/app/modals/customize-nonce/index.js create mode 100644 ui/app/components/app/modals/customize-nonce/index.scss rename ui/app/components/app/modals/metametrics-opt-in-modal/{tests => }/metametrics-opt-in-modal.test.js (53%) rename ui/app/components/app/modals/reject-transactions/{tests => }/reject-transactions.test.js (63%) rename ui/app/components/app/modals/transaction-confirmed/{tests => }/transaction-confirmed.test.js (62%) rename ui/app/components/app/selected-account/{tests => }/selected-account-component.test.js (54%) rename ui/app/components/app/sidebars/{tests/sidebars-component.test.js => sidebar.component.test.js} (50%) rename ui/app/components/app/signature-request/{tests/signature-request.test.js => signature-request.component.test.js} (53%) rename ui/app/components/app/{tests/signature-request.test.js => signature-request/signature-request.container.test.js} (87%) rename ui/app/components/app/transaction-activity-log/{tests => }/transaction-activity-log.component.test.js (83%) rename ui/app/components/app/transaction-activity-log/{tests => }/transaction-activity-log.container.test.js (65%) rename ui/app/components/app/transaction-activity-log/{tests => }/transaction-activity-log.util.test.js (93%) rename ui/app/components/app/transaction-breakdown/transaction-breakdown-row/{tests => }/transaction-breakdown-row.component.test.js (53%) rename ui/app/components/app/transaction-breakdown/{tests => }/transaction-breakdown.component.test.js (58%) rename ui/app/components/app/transaction-list-item-details/{tests => }/transaction-list-item-details.component.test.js (68%) delete mode 100644 ui/app/components/app/transaction-status/tests/transaction-status.component.test.js create mode 100644 ui/app/components/app/transaction-status/transaction-status.component.test.js delete mode 100644 ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js delete mode 100644 ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js delete mode 100644 ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js create mode 100644 ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js delete mode 100644 ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js delete mode 100644 ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js create mode 100644 ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js create mode 100644 ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js create mode 100644 ui/app/components/app/whats-new-popup/index.js create mode 100644 ui/app/components/app/whats-new-popup/index.scss create mode 100644 ui/app/components/app/whats-new-popup/whats-new-popup.js rename ui/app/components/ui/account-mismatch-warning/{tests => }/acccount-mismatch-warning.component.test.js (58%) rename ui/app/components/ui/alert/{tests/alert.test.js => index.test.js} (56%) create mode 100644 ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js delete mode 100644 ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js rename ui/app/components/ui/button-group/{tests => }/button-group-component.test.js (53%) create mode 100644 ui/app/components/ui/card/card.component.test.js delete mode 100644 ui/app/components/ui/card/tests/card.component.test.js create mode 100644 ui/app/components/ui/confusable/confusable.component.test.js delete mode 100644 ui/app/components/ui/confusable/test/confusable.component.test.js rename ui/app/components/ui/currency-display/{tests => }/currency-display.component.test.js (57%) rename ui/app/components/ui/currency-input/{tests => }/currency-input.component.test.js (56%) rename ui/app/components/ui/currency-input/{tests => }/currency-input.container.test.js (86%) create mode 100644 ui/app/components/ui/error-message/error-message.component.test.js delete mode 100644 ui/app/components/ui/error-message/tests/error-message.component.test.js rename ui/app/components/ui/hex-to-decimal/{tests => }/hex-to-decimal.component.test.js (52%) rename ui/app/components/ui/identicon/{tests => }/identicon.component.test.js (54%) create mode 100644 ui/app/components/ui/list-item/list-item.component.test.js delete mode 100644 ui/app/components/ui/list-item/tests/list-item.test.js rename ui/app/components/ui/metafox-logo/{tests => }/metafox-logo.component.test.js (59%) create mode 100644 ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js delete mode 100644 ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js create mode 100644 ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js delete mode 100644 ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js rename ui/app/components/ui/token-input/{tests => }/token-input.component.test.js (57%) delete mode 100644 ui/app/components/ui/unit-input/tests/unit-input.component.test.js create mode 100644 ui/app/components/ui/unit-input/unit-input.component.test.js rename {test/unit/ui/app/reducers => ui/app/ducks/app}/app.test.js (58%) rename {test/unit/ui/app/reducers => ui/app/ducks/metamask}/metamask.test.js (65%) create mode 100644 ui/app/ducks/swaps/swaps.test.js rename ui/app/helpers/higher-order-components/with-modal-props/{tests => }/with-modal-props.test.js (58%) delete mode 100644 ui/app/hooks/tests/useRetryTransaction.test.js rename ui/app/hooks/{tests => }/useCancelTransaction.test.js (57%) rename ui/app/hooks/{tests => }/useCurrencyDisplay.test.js (83%) create mode 100644 ui/app/hooks/useRetryTransaction.test.js rename ui/app/hooks/{tests => }/useTokenData.test.js (59%) rename ui/app/hooks/{tests => }/useTokenDisplayValue.test.js (84%) rename ui/app/hooks/{tests => }/useTransactionDisplayData.test.js (77%) rename ui/app/hooks/{tests => }/useUserPreferencedCurrency.test.js (86%) rename ui/app/pages/add-token/{tests => }/add-token.test.js (67%) create mode 100644 ui/app/pages/asset/components/asset-options.js delete mode 100644 ui/app/pages/asset/components/token-options.js create mode 100644 ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js delete mode 100644 ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js rename ui/app/pages/create-account/{tests => }/create-account.test.js (60%) rename ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/{tests => }/import-with-seed-phrase.component.test.js (59%) create mode 100644 ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js delete mode 100644 ui/app/pages/first-time-flow/end-of-flow/tests/end-of-flow.test.js rename ui/app/pages/first-time-flow/first-time-flow-switch/{tests => }/first-time-flow-switch.test.js (67%) rename ui/app/pages/first-time-flow/metametrics-opt-in/{tests => }/metametrics-opt-in.test.js (67%) rename ui/app/pages/first-time-flow/seed-phrase/{tests => }/confirm-seed-phrase-component.test.js (73%) rename ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/{tests => }/reveal-seed-phrase.test.js (63%) create mode 100644 ui/app/pages/first-time-flow/select-action/select-action.test.js delete mode 100644 ui/app/pages/first-time-flow/select-action/tests/select-action.test.js rename ui/app/pages/first-time-flow/welcome/{tests => }/welcome.test.js (75%) rename ui/app/pages/keychains/{tests => }/reveal-seed.test.js (65%) rename ui/app/pages/lock/{tests => }/lock.test.js (58%) rename ui/app/pages/send/send-content/add-recipient/{tests/add-recipient-component.test.js => add-recipient.component.test.js} (65%) create mode 100644 ui/app/pages/send/send-content/add-recipient/add-recipient.container.test.js create mode 100644 ui/app/pages/send/send-content/add-recipient/add-recipient.utils.test.js delete mode 100644 ui/app/pages/send/send-content/add-recipient/tests/add-recipient-container.test.js delete mode 100644 ui/app/pages/send/send-content/add-recipient/tests/add-recipient-utils.test.js rename ui/app/pages/send/send-content/send-amount-row/amount-max-button/{tests/amount-max-button-component.test.js => amount-max-button.component.test.js} (58%) create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.test.js rename ui/app/pages/send/send-content/send-amount-row/amount-max-button/{tests/amount-max-button-utils.test.js => amount-max-button.utils.test.js} (53%) delete mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js rename ui/app/pages/send/send-content/send-amount-row/{tests/send-amount-row-component.test.js => send-amount-row.component.test.js} (63%) create mode 100644 ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.test.js delete mode 100644 ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-content.component.test.js rename ui/app/pages/send/send-content/send-gas-row/gas-fee-display/{tests => }/gas-fee-display.component.test.js (52%) rename ui/app/pages/send/send-content/send-gas-row/{tests/send-gas-row-component.test.js => send-gas-row.component.test.js} (50%) create mode 100644 ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.test.js delete mode 100644 ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.test.js delete mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js delete mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js rename ui/app/pages/send/send-content/send-row-wrapper/{tests/send-row-wrapper-component.test.js => send-row-wrapper.component.test.js} (62%) delete mode 100644 ui/app/pages/send/send-content/tests/send-content-component.test.js rename ui/app/pages/send/send-footer/{tests/send-footer-component.test.js => send-footer.component.test.js} (70%) create mode 100644 ui/app/pages/send/send-footer/send-footer.container.test.js rename ui/app/pages/send/send-footer/{tests/send-footer-utils.test.js => send-footer.utils.test.js} (66%) delete mode 100644 ui/app/pages/send/send-footer/tests/send-footer-container.test.js create mode 100644 ui/app/pages/send/send-header/send-header.component.test.js delete mode 100644 ui/app/pages/send/send-header/tests/send-header-component.test.js create mode 100644 ui/app/pages/send/send.component.test.js create mode 100644 ui/app/pages/send/send.container.test.js rename ui/app/pages/send/{tests/send-utils.test.js => send.utils.test.js} (55%) delete mode 100644 ui/app/pages/send/tests/send-component.test.js delete mode 100644 ui/app/pages/send/tests/send-container.test.js rename ui/app/pages/settings/advanced-tab/{tests/advanced-tab-component.test.js => advanced-tab.component.test.js} (71%) delete mode 100644 ui/app/pages/settings/contact-list-tab/my-accounts/index.js delete mode 100644 ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.component.js delete mode 100644 ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.container.js rename ui/app/pages/settings/security-tab/{tests/security-tab.test.js => security-tab.container.test.js} (62%) rename ui/app/pages/settings/settings-tab/{tests/settings-tab.test.js => settings-tab.container.test.js} (64%) create mode 100644 ui/app/pages/swaps/__snapshots__/index.test.js.snap create mode 100644 ui/app/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap create mode 100644 ui/app/pages/swaps/actionable-message/actionable-message.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/__snapshots__/awaiting-swap.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/__snapshots__/quotes-timeout-icon.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/__snapshots__/swap-failure-icon.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/__snapshots__/swap-success-icon.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/awaiting-swap.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/quotes-timeout-icon.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/swap-failure-icon.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/swap-success-icon.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/__snapshots__/view-on-ether-scan-link.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.test.js create mode 100644 ui/app/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap create mode 100644 ui/app/pages/swaps/build-quote/build-quote.test.js create mode 100644 ui/app/pages/swaps/countdown-timer/countdown-timer.test.js create mode 100644 ui/app/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap create mode 100644 ui/app/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js create mode 100644 ui/app/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap create mode 100644 ui/app/pages/swaps/dropdown-search-list/dropdown-search-list.test.js create mode 100644 ui/app/pages/swaps/exchange-rate-display/__snapshots__/exchange-rate-display.test.js.snap create mode 100644 ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.test.js create mode 100644 ui/app/pages/swaps/fee-card/__snapshots__/fee-card.test.js.snap create mode 100644 ui/app/pages/swaps/fee-card/fee-card.test.js create mode 100644 ui/app/pages/swaps/fee-card/pig-icon.test.js create mode 100644 ui/app/pages/swaps/index.test.js delete mode 100644 ui/app/pages/swaps/intro-popup/index.js delete mode 100644 ui/app/pages/swaps/intro-popup/index.scss delete mode 100644 ui/app/pages/swaps/intro-popup/intro-popup.js create mode 100644 ui/app/pages/swaps/loading-swaps-quotes/__snapshots__/aggregator-logo.test.js.snap create mode 100644 ui/app/pages/swaps/loading-swaps-quotes/__snapshots__/loading-swaps-quotes-stories-metadata.test.js.snap create mode 100644 ui/app/pages/swaps/loading-swaps-quotes/aggregator-logo.test.js create mode 100644 ui/app/pages/swaps/loading-swaps-quotes/background-animation.test.js create mode 100644 ui/app/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.test.js create mode 100644 ui/app/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap create mode 100644 ui/app/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap create mode 100644 ui/app/pages/swaps/main-quote-summary/main-quote-summary.test.js create mode 100644 ui/app/pages/swaps/main-quote-summary/quote-backdrop.test.js create mode 100644 ui/app/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap create mode 100644 ui/app/pages/swaps/searchable-item-list/searchable-item-list.test.js create mode 100644 ui/app/pages/swaps/select-quote-popover/__snapshots__/select-quote-popover.test.js.snap create mode 100644 ui/app/pages/swaps/select-quote-popover/mock-quote-data.test.js create mode 100644 ui/app/pages/swaps/select-quote-popover/select-quote-popover.test.js create mode 100644 ui/app/pages/swaps/select-quote-popover/sort-list/__snapshots__/sort-list.test.js.snap create mode 100644 ui/app/pages/swaps/select-quote-popover/sort-list/sort-list.test.js create mode 100644 ui/app/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap create mode 100644 ui/app/pages/swaps/slippage-buttons/slippage-buttons.test.js create mode 100644 ui/app/pages/swaps/swaps-footer/__snapshots__/swaps-footer.test.js.snap create mode 100644 ui/app/pages/swaps/swaps-footer/swaps-footer.test.js rename ui/app/pages/swaps/view-quote/{tests => }/view-quote-price-difference.test.js (84%) delete mode 100644 ui/app/pages/unlock-page/tests/unlock-page.test.js create mode 100644 ui/app/pages/unlock-page/unlock-page.component.test.js create mode 100644 ui/app/pages/unlock-page/unlock-page.container.test.js rename ui/app/selectors/{tests => }/confirm-transaction.test.js (62%) rename ui/app/selectors/{tests => }/custom-gas.test.js (88%) rename ui/app/selectors/{tests => }/permissions.test.js (74%) create mode 100644 ui/app/selectors/selectors.test.js rename ui/app/selectors/{tests => }/send-selectors-test-data.js (97%) rename ui/app/selectors/{tests => }/send.test.js (54%) delete mode 100644 ui/app/selectors/tests/selectors.test.js rename ui/app/selectors/{tests => }/transactions.test.js (81%) create mode 100644 ui/app/store/actionConstants.test.js rename {test/unit/ui/app => ui/app/store}/actions.test.js (66%) rename {test/unit => ui}/lib/account-link.test.js (79%) rename {test/unit/app/controllers/transactions => ui/lib}/tx-helper.test.js (51%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4955fe38f..83ccb67ee 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 --rc + - 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 @@ -438,6 +444,9 @@ jobs: - store_artifacts: path: coverage destination: coverage + - store_artifacts: + path: jest-coverage + destination: jest-coverage - store_artifacts: path: test-artifacts destination: test-artifacts @@ -470,9 +479,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 @@ -498,11 +504,15 @@ jobs: - run: name: test:coverage command: yarn test:coverage + - run: + name: test:coverage:jest + command: yarn test:coverage:jest - persist_to_workspace: root: . paths: - .nyc_output - coverage + - jest-coverage test-unit-global: executor: node-browsers steps: diff --git a/.circleci/scripts/release-bump-changelog-version.sh b/.circleci/scripts/release-bump-changelog-version.sh deleted file mode 100755 index 9fd4ddbb8..000000000 --- a/.circleci/scripts/release-bump-changelog-version.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -if [[ "${CI:-}" != 'true' ]] -then - printf '%s\n' 'CI environment variable must be set to true' - exit 1 -fi - -if [[ "${CIRCLECI:-}" != 'true' ]] -then - printf '%s\n' 'CIRCLECI environment variable must be set to true' - exit 1 -fi - -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 < CHANGELOG.md - rm CHANGELOG.md.bak -else - printf '%s\n' "CHANGELOG.md already includes a header for ${version}" - exit 0 -fi diff --git a/.circleci/scripts/release-bump-manifest-version.sh b/.circleci/scripts/release-bump-manifest-version.sh index 78686d250..776a33a63 100755 --- a/.circleci/scripts/release-bump-manifest-version.sh +++ b/.circleci/scripts/release-bump-manifest-version.sh @@ -26,14 +26,4 @@ yarn prettier --write app/manifest/_base.json if [[ -z $(git status --porcelain) ]] then printf '%s\n' 'App manifest version already set' - exit 0 fi - -git \ - -c user.name='MetaMask Bot' \ - -c user.email='metamaskbot@users.noreply.github.com' \ - commit --message "${CIRCLE_BRANCH/-/ }" \ - CHANGELOG.md app/manifest/_base.json - -repo_slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" -git push "https://$GITHUB_TOKEN_USER:$GITHUB_TOKEN@github.com/$repo_slug" "$CIRCLE_BRANCH" diff --git a/.circleci/scripts/release-commit-version-bump.sh b/.circleci/scripts/release-commit-version-bump.sh new file mode 100755 index 000000000..6ceb67fb9 --- /dev/null +++ b/.circleci/scripts/release-commit-version-bump.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +if [[ "${CI:-}" != 'true' ]] +then + printf '%s\n' 'CI environment variable must be set to true' + exit 1 +fi + +if [[ "${CIRCLECI:-}" != 'true' ]] +then + printf '%s\n' 'CIRCLECI environment variable must be set to true' + exit 1 +fi + +printf '%s\n' 'Commit the manifest version and changelog if the manifest has changed' + +if git diff --quiet app/manifest/_base.json; +then + printf '%s\n' 'No manifest changes to commit' + exit 0 +fi + +git \ + -c user.name='MetaMask Bot' \ + -c user.email='metamaskbot@users.noreply.github.com' \ + commit --message "${CIRCLE_BRANCH/-/ }" \ + CHANGELOG.md app/manifest/_base.json + +repo_slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" +git push "https://$GITHUB_TOKEN_USER:$GITHUB_TOKEN@github.com/$repo_slug" "$CIRCLE_BRANCH" diff --git a/.circleci/scripts/release-create-master-pr.sh b/.circleci/scripts/release-create-master-pr.sh deleted file mode 100755 index 9b62d2397..000000000 --- a/.circleci/scripts/release-create-master-pr.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -if [[ "${CI:-}" != 'true' ]] -then - printf '%s\n' 'CI environment variable must be set to true' - exit 1 -fi - -if [[ "${CIRCLECI:-}" != 'true' ]] -then - printf '%s\n' 'CIRCLECI environment variable must be set to true' - exit 1 -fi - -if [[ -z "${GITHUB_TOKEN:-}" ]] -then - printf '%s\n' 'GITHUB_TOKEN environment variable must be set' - exit 1 -fi - -function install_github_cli () -{ - printf '%s\n' 'Installing hub CLI' - pushd "$(mktemp -d)" - curl -sSL 'https://github.com/github/hub/releases/download/v2.11.2/hub-linux-amd64-2.11.2.tgz' | tar xz - PATH="$PATH:$PWD/hub-linux-amd64-2.11.2/bin" - popd -} - -base_branch='develop' - -if [[ -n "${CI_PULL_REQUEST:-}" ]] -then - printf '%s\n' 'CI_PULL_REQUEST is set, pull request already exists for this build' - exit 0 -fi - -install_github_cli - -printf '%s\n' "Creating a Pull Request to sync 'master' with 'develop'" - -if ! hub pull-request \ - --message "Master => 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..8b9f27b12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,7 +26,8 @@ module.exports = { 'test-*/**', 'docs/**', 'coverage/', - 'app/scripts/chromereload.js', + 'jest-coverage/', + 'development/chromereload.js', 'app/vendor/**', 'test/e2e/send-eth-with-private-key-test/**', 'nyc_output/**', @@ -36,13 +37,11 @@ module.exports = { extends: [ '@metamask/eslint-config', - '@metamask/eslint-config/config/nodejs', - '@metamask/eslint-config/config/mocha', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', + '@metamask/eslint-config-nodejs', + 'prettier', ], - plugins: ['@babel', 'react', 'import', 'prettier'], + plugins: ['@babel', 'import', 'prettier'], globals: { document: 'readonly', @@ -50,102 +49,18 @@ module.exports = { }, rules: { - // 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', + 'prefer-object-spread': 'error', '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', - }, - ], 'no-invalid-this': 'off', '@babel/no-invalid-this': 'error', - // prettier handles these - semi: 'off', + // Prettier handles this '@babel/semi': 'off', - 'mocha/no-setup-in-describe': 'off', 'node/no-process-env': 'off', // TODO: re-enable these rules @@ -155,9 +70,28 @@ module.exports = { }, overrides: [ { - files: ['test/e2e/**/*.js'], + files: ['ui/**/*.js', 'test/lib/render-helpers.js', 'test/jest/*.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-mocha'], rules: { 'mocha/no-hooks-for-single-case': 'off', + 'mocha/no-setup-in-describe': 'off', }, }, { @@ -173,14 +107,38 @@ module.exports = { }, }, { - files: ['test/**/*-test.js', 'test/**/*.spec.js'], + files: ['**/*.test.js'], + excludedFiles: ['ui/**/*.test.js', 'ui/app/__mocks__/*.js'], + extends: ['@metamask/eslint-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: ['**/__snapshots__/*.snap'], + plugins: ['jest'], + rules: { + 'jest/no-large-snapshots': [ + 'error', + { maxSize: 50, inlineMaxSize: 50 }, + ], + }, + }, + { + files: ['ui/**/*.test.js', 'ui/app/__mocks__/*.js'], + extends: ['@metamask/eslint-config-jest'], + rules: { + 'jest/no-restricted-matchers': 'off', + 'import/unambiguous': 'off', + 'import/named': 'off', + }, + }, + { + files: [ + 'development/**/*.js', + 'test/e2e/benchmark.js', + 'test/helpers/setup-helper.js', + ], rules: { 'node/no-process-exit': 'off', 'node/shebang': 'off', @@ -198,6 +156,7 @@ module.exports = { 'test/lib/wait-until-called.js', 'test/env.js', 'test/setup.js', + 'jest.config.js', ], parserOptions: { sourceType: 'script', diff --git a/.gitignore b/.gitignore index 5e46dd1e0..6333a39a1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ audit.json app/bower_components test/bower_components package +.eslintcache # IDEs .idea @@ -28,6 +29,7 @@ app/.DS_Store storybook-build/ coverage/ +jest-coverage/ dist builds/ builds.zip diff --git a/.prettierignore b/.prettierignore index 3059730e1..67bd43f64 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ dist/** builds/** test-*/** coverage/ +jest-coverage/ app/vendor/** .nyc_output/** .vscode/** 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 effe9fd23..39ae262ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,94 @@ # Changelog - -## Current Develop Branch - -## 9.4.0 Thu Apr 15 2021 +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [9.5.0] - 2021-04-28 +### Added +- [#10583](https://github.com/MetaMask/metamask-extension/pull/10583): Adding popup display to show new MetaMask notifications +- [#10938](https://github.com/MetaMask/metamask-extension/pull/10938): Add menu with "View on Etherscan" and "Account details" links to ETH asset page +- [#10932](https://github.com/MetaMask/metamask-extension/pull/10932): Add view account details menu item to token page menu +- [#10895](https://github.com/MetaMask/metamask-extension/pull/10895): Adding new links to contact MetaMask support +- [#10595](https://github.com/MetaMask/metamask-extension/pull/10595): Adding option to set Custom Nonce to Confirm Approve Page +- [#10616](https://github.com/MetaMask/metamask-extension/pull/10616): add trezor HD path for ledger wallets + +### Changed +- [#10939](https://github.com/MetaMask/metamask-extension/pull/10939): Use custom token icons in the send flow token dropdown +- [#10680](https://github.com/MetaMask/metamask-extension/pull/10680): Remove "My Wallet Account" section in Settings > Contact +- [#10912](https://github.com/MetaMask/metamask-extension/pull/10912): Harden contract address validation for token swaps +- [#10882](https://github.com/MetaMask/metamask-extension/pull/10882): Show the custom network name in swaps network fee tooltip +- [#10859](https://github.com/MetaMask/metamask-extension/pull/10859): Only check whether the swaps feature is live after entering the feature +- [#10871](https://github.com/MetaMask/metamask-extension/pull/10871): Update swaps metadata every 5 minutes as opposed to an hour +- [#10842](https://github.com/MetaMask/metamask-extension/pull/10842): Increase default slippage from 2% to 3% in swaps and show Advanced Options by default +- [#10593](https://github.com/MetaMask/metamask-extension/pull/10593): Prevent tokens without addresses from being added to token list +- [#10746](https://github.com/MetaMask/metamask-extension/pull/10746): Add New Zealand Dollar to currency options +- [#10670](https://github.com/MetaMask/metamask-extension/pull/10670): Allow 11 characters in symbol for custom RPCs +- [#10702](https://github.com/MetaMask/metamask-extension/pull/10702): Hide the suggested token pane when not on Mainnet or test network +- [#10700](https://github.com/MetaMask/metamask-extension/pull/10700): Prevents autocomplete text from displaying in the Add Token input +- [#10704](https://github.com/MetaMask/metamask-extension/pull/10704): Removing hard references to 12 word seed phrases in copy +- [#10703](https://github.com/MetaMask/metamask-extension/pull/10703): Add MetaMask to list of BIP44 HD path examples +- [#10651](https://github.com/MetaMask/metamask-extension/pull/10651): Change 'Send ETH' title to 'Send' in the send flow +- [#10674](https://github.com/MetaMask/metamask-extension/pull/10674): Don't render faucet row in deposit modal for custom chains + +### Fixed +- [#10935](https://github.com/MetaMask/metamask-extension/pull/10935): Prevent overflow of hostname on confirmation page +- [#10923](https://github.com/MetaMask/metamask-extension/pull/10923): Fixing ENS input entry in send flow +- [#10723](https://github.com/MetaMask/metamask-extension/pull/10723): Fix mismatchedChain typo in custom network approval screen +- [#10695](https://github.com/MetaMask/metamask-extension/pull/10695): Excluding sourcemaps comment in production builds +- [#10643](https://github.com/MetaMask/metamask-extension/pull/10643): Prevent network dropdown label highlighting +- [#10644](https://github.com/MetaMask/metamask-extension/pull/10644): Ensure swaps detail height doesn't create jump in vertical height +- [#10642](https://github.com/MetaMask/metamask-extension/pull/10642): Position the 3dot menu in the same spot on asset screen and home screen +- [#10594](https://github.com/MetaMask/metamask-extension/pull/10594): Ensure MetaMask works correctly when on a custom network that shares a chain id with a default Infura network +- [#10579](https://github.com/MetaMask/metamask-extension/pull/10579): Fixed bug that prevented speeding up cancelled transactions +- [#10630](https://github.com/MetaMask/metamask-extension/pull/10630): Fixes hidden token bug when zero balance preference is on +- [#10628](https://github.com/MetaMask/metamask-extension/pull/10628): Removing double click bug from delete custom network modal + +## [9.4.0] - 2021-04-15 +### Added - [#10883](https://github.com/MetaMask/metamask-extension/pull/10883): Notify users when MetaMask is unable to connect to the blockchain host -## 9.3.0 Fri Mar 26 2021 -- [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen +## [9.3.0] - 2021-04-02 +### Added - [#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 + +### Fixed +- [#10777](https://github.com/MetaMask/metamask-extension/pull/10777): Display BNB token image for default currency on BSC network home screen - [#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 +### Fixed - [#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 +### Added - [#10546](https://github.com/MetaMask/metamask-extension/pull/10546): Add a warning when sending a token to its own contract address +- [#10582](https://github.com/MetaMask/metamask-extension/pull/10582): Adding warnings for excessive custom gas input +- [#10505](https://github.com/MetaMask/metamask-extension/pull/10505): Add support for multiple Ledger & Trezor hardware accounts +- [#10486](https://github.com/MetaMask/metamask-extension/pull/10486): Add setting to hide zero balance tokens + +### Changed - [#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 + +### Fixed - [#10591](https://github.com/MetaMask/metamask-extension/pull/10591): Fix mobile sync of ERC20 tokens -- [#10582](https://github.com/MetaMask/metamask-extension/pull/10582): Adding warnings for excessive custom gas input - [#10601](https://github.com/MetaMask/metamask-extension/pull/10601): Fix activity title text truncation - [#10598](https://github.com/MetaMask/metamask-extension/pull/10598): Remove 'Ethereum' from custom RPC endpoint warning - [#10606](https://github.com/MetaMask/metamask-extension/pull/10606): Show loading screen while fetching token data for approve screen -- [#10486](https://github.com/MetaMask/metamask-extension/pull/10486): Add setting to hide zero balance tokens -- [#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 +### Fixed - [#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 +### Uncategorized - [#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. @@ -67,7 +123,8 @@ - [#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 +### Uncategorized - [#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 @@ -82,7 +139,8 @@ - [#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 +### Uncategorized - [#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 @@ -96,25 +154,26 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#8640](https://github.com/MetaMask/metamask-extension/pull/8640): Implement breaking window.ethereum API changes @@ -135,20 +194,20 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#10103](https://github.com/MetaMask/metamask-extension/pull/10103): Prevent stuck loading screen in some situations - [#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 +### Uncategorized - [#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 - [#10048](https://github.com/MetaMask/metamask-extension/pull/10048): Display boolean values when signing typed data @@ -157,19 +216,19 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - Revert SES lockdown -## 8.1.6 Wed Dec 02 2020 - +## [8.1.6] - 2020-12-04 +### Uncategorized - [#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 - [#9960](https://github.com/MetaMask/metamask-extension/pull/9960): Ensure watchAsset returns errors for invalid token symbols @@ -180,8 +239,8 @@ - [#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 +### Uncategorized - [#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 - [#9853](https://github.com/MetaMask/metamask-extension/pull/9853): Provide alternative text for images where appropriate @@ -195,8 +254,8 @@ - [#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 +### Uncategorized - [#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 - [#9695](https://github.com/MetaMask/metamask-extension/pull/9695): normalize app component font styles @@ -230,8 +289,8 @@ - [#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 +### Uncategorized - [#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 - [#9667](https://github.com/MetaMask/metamask-extension/pull/9667): Fix adding contact with QR code @@ -249,8 +308,8 @@ - [#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 +### Uncategorized - [#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 - [#9614](https://github.com/MetaMask/metamask-extension/pull/9614): Update swaps network fee tooltip @@ -258,8 +317,8 @@ - [#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 +### Uncategorized - [#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) - [#9596](https://github.com/MetaMask/metamask-extension/pull/9596): Fix TypeError when `signTypedData` throws (#9596) @@ -271,8 +330,8 @@ - [#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 +### Uncategorized - [#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 - [#9551](https://github.com/MetaMask/metamask-extension/pull/9551): Allow the "Localhost 8545" network to be edited, and require a chain ID to be specified for it @@ -304,26 +363,26 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#9063](https://github.com/MetaMask/metamask-extension/pull/9063): Fix broken UI upon failed password validation @@ -335,8 +394,8 @@ - [#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 +### Uncategorized - [#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 - [#9032](https://github.com/MetaMask/metamask-extension/pull/9032): Do not show spend limit for approvals @@ -345,8 +404,8 @@ - [#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 +### Uncategorized - [#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) - [#9003](https://github.com/MetaMask/metamask-extension/pull/9003): @metamask/inpage-provider@6.0.1 (#9003) @@ -358,28 +417,28 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#8909](https://github.com/MetaMask/metamask-extension/pull/8909): Prevent confirming blank suggested token - [#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 +### Uncategorized - [#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 - [#8883](https://github.com/MetaMask/metamask-extension/pull/8883): Allow setting a custom nonce of zero @@ -391,8 +450,8 @@ - [#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 +### Uncategorized - [#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 - [#7483](https://github.com/MetaMask/metamask-extension/pull/7483): Buffer 3 blocks before dropping a transaction @@ -483,8 +542,8 @@ - [#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 +### Uncategorized - [#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 - [#8447](https://github.com/MetaMask/metamask-extension/pull/8447): Delete Dai/Sai migration notification @@ -501,48 +560,48 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7920](https://github.com/MetaMask/metamask-extension/pull/7920): Validate custom spend limit - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7500](https://github.com/MetaMask/metamask-extension/pull/7500): Remove unused onClick prop from Dropdown component @@ -556,8 +615,8 @@ - [#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] +### Uncategorized - [#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 - [#7482](https://github.com/MetaMask/metamask-extension/pull/7482): Update Wyre ETH purchase url @@ -570,20 +629,20 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound (#7414) @@ -597,18 +656,18 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7333](https://github.com/MetaMask/metamask-extension/pull/7333): Cleanup beforeunload handler after transaction is resolved @@ -626,8 +685,8 @@ - [#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 +### Uncategorized - [#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 - [#7306](https://github.com/MetaMask/metamask-extension/pull/7306): correct the zh-TW translation @@ -640,12 +699,12 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7170](https://github.com/MetaMask/metamask-extension/pull/7170): Remove the disk store @@ -664,22 +723,22 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#7128](https://github.com/MetaMask/metamask-extension/pull/7128): Support for eth_signTypedData_v4 @@ -691,8 +750,8 @@ - [#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 +### Uncategorized - [#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 - [#7093](https://github.com/MetaMask/metamask-extension/pull/7093): Allow dismissing privacy mode notification from popup @@ -703,8 +762,8 @@ - [#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 +### Uncategorized - [#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) - [#7018](https://github.com/MetaMask/metamask-extension/pull/7018): Rename Contacts List settings tab to Contacts (#7018) @@ -717,12 +776,12 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6900](https://github.com/MetaMask/metamask-extension/pull/6900): Prevent opening of asset dropdown if no tokens in account @@ -731,12 +790,12 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6694](https://github.com/MetaMask/metamask-extension/pull/6694): Fixes #6694 @@ -750,12 +809,12 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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) - [#6718](https://github.com/MetaMask/metamask-extension/pull/6718): Add delete to custom RPC form @@ -765,40 +824,40 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - 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 +### Uncategorized - [#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 - [#6558](https://github.com/MetaMask/metamask-extension/pull/6558): Adds auto logout with customizable time frame @@ -808,12 +867,12 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6403](https://github.com/MetaMask/metamask-extension/pull/6403): Update to eth-method-registry@1.2.0 @@ -838,20 +897,20 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6302](https://github.com/MetaMask/metamask-extension/pull/6302): Replaces the coinbase link in the deposit modal with one for wyre @@ -865,24 +924,24 @@ - [#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 +### Uncategorized - [#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.0 Tue Mar 05 2019 +## [6.2.1] - 2019-03-11 +## [6.2.0] - 2019-03-05 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6146](https://github.com/MetaMask/metamask-extension/pull/6146): \* Add Copy Tx ID button to transaction-list-item-details @@ -890,15 +949,15 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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. - [#6091](https://github.com/MetaMask/metamask-extension/pull/6091): Add Swap feature to CurrencyInput @@ -908,23 +967,23 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#6048](https://github.com/MetaMask/metamask-extension/pull/6048): Refactor BalanceComponent to jsx @@ -932,8 +991,8 @@ - [#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 +### Uncategorized - [#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 - [#6008](https://github.com/MetaMask/metamask-extension/pull/6008): Fix confirm screen for sending ether tx with hex data @@ -945,8 +1004,8 @@ - [#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 +### Uncategorized - [#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 - [#5922](https://github.com/MetaMask/metamask-extension/pull/5922): Prevent users from changing the From field in the send screen @@ -954,32 +1013,32 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 - [#5848](https://github.com/MetaMask/metamask-extension/pull/5848): Soften accusatory language on phishing warning @@ -1001,31 +1060,31 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Implements EIP 1102 as a user-activated "Privacy Mode". -## 4.17.1 Saturday November 3 2018 - +## [4.17.1] - 2018-11-03 +### Uncategorized - 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 +### Uncategorized - Fix bug where data lookups like balances would get stale data (stopped block-tracker bug) - Transaction Details now show entry for onchain failure - [#5559](https://github.com/MetaMask/metamask-extension/pull/5559) Localize language names in translation select list @@ -1036,8 +1095,8 @@ - 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 +### Uncategorized - Feature: Add toggle for primary currency (eth/fiat) - Feature: add tooltip for view etherscan tx - Feature: add Polish translations @@ -1048,35 +1107,35 @@ - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Reintroduces changes from 4.10.0 -## 4.11.1 Tuesday September 25 2018 - +## [4.11.1] - 2018-09-25 +### Uncategorized - Adds Ledger support. -## 4.11.0 Monday September 24 2018 - +## [4.11.0] - 2018-09-24 +### Uncategorized - 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 +### Uncategorized - [#4803](https://github.com/MetaMask/metamask-extension/pull/4803): Implement EIP-712: Sign typed data, but continue to support v1. - [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. - [#4279](https://github.com/MetaMask/metamask-extension/pull/4279): New BlockTracker and Json-Rpc-Engine based Provider. @@ -1089,26 +1148,26 @@ - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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 +### Uncategorized - [#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. - [#5010](https://github.com/MetaMask/metamask-extension/pull/5010): Fix ENS resolution issues. - [#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 +### Uncategorized - [#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. - [#4947](https://github.com/MetaMask/metamask-extension/pull/4947): Prevent sending multiple transasctions on multiple confirm clicks. @@ -1123,8 +1182,8 @@ - [#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 +### Uncategorized - [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error. - [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use. - [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers. @@ -1133,31 +1192,31 @@ - [#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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fix bug where errors were not returned to Dapps. -## 4.7.0 Wed May 30 2018 - +## [4.7.0] - 2018-05-30 +### Uncategorized - Fix Brave support - Adds error messages when passwords don't match in onboarding flow. - Adds modal notification if a retry in the process of being confirmed is dropped. @@ -1171,16 +1230,16 @@ - 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 +### Uncategorized - Fix bug where sending a transaction resulted in an infinite spinner - Allow transactions with a 0 gwei gas price - Handle encoding errors in ERC20 symbol + digits - Fix ShapeShift forms (new + old ui) - Fix sourcemaps -## 4.6.0 Thu Apr 26 2018 - +## [4.6.0] - 2018-04-26 +### Uncategorized - Correctly format currency conversion for locally selected preferred currency. - Improved performance of 3D fox logo. - Fetch token prices based on contract address, not symbol @@ -1189,44 +1248,44 @@ - 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 +### Uncategorized - Graceful handling of unknown keys in txParams - Fixes buggy handling of historical transactions with 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) - Enhanced migration error handling + reporting -## 4.5.4 (aborted) Thu Apr 05 2018 - +## [4.5.4] - 2018-04-05 [WITHDRAWN] +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - (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 +### Uncategorized - Internationalization: Taiwanese, Thai, Slovenian - Fixes bug where MetaMask would not open once its storage grew too large. - Updates design of new-ui Add Token screen @@ -1236,8 +1295,8 @@ - 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 +### Uncategorized - (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 - Will now throw an error if the `to` field in txParams is not valid. @@ -1252,8 +1311,8 @@ - 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 +### Uncategorized - Replace "Loose" wording to "Imported". - Replace "Unlock" wording with "Log In". - Add Imported Account disclaimer. @@ -1262,69 +1321,69 @@ - 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 +### Uncategorized - 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 +### Uncategorized - Actually includes all the fixes mentioned in 4.1.1 (sorry) -## 4.1.1 2018-2-28 - +## [4.1.1] - 2018-02-28 +### Uncategorized - Fix "Add Token" screen referencing missing token logo urls - Prevent user from switching network during signature request - Fix misleading language "Contract Published" -> "Contract Deployment" - 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 +### Uncategorized - 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 +### Uncategorized - Introduce new MetaMask user interface. -## 3.14.2 2018-2-15 - +## [3.14.2] - 2018-02-27 +### Uncategorized - 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 +### Uncategorized - Further fix scrolling for Firefox. -## 3.14.0 2018-2-1 - +## [3.14.0] - 2018-02-01 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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. - Fix Firefox scrollbar. @@ -1332,8 +1391,8 @@ - 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 +### Uncategorized - 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. - Throw an error if a application tries to submit a tx whose value is a decimal, and inform that it should be in wei. @@ -1342,96 +1401,96 @@ - 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 +### Uncategorized - 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 +### Uncategorized - Reduce new block polling interval to 8000 ms, to ease server load. -## 3.13.1 2017-12-7 - +## [3.13.1] - 2017-12-07 +### Uncategorized - 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 +### Uncategorized - Allow resubmitting transactions that are taking long to complete. -## 3.12.1 2017-11-29 - +## [3.12.1] - 2017-11-29 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fixed usage of new currency fetching API. -## 3.10.7 2017-9-28 - +## [3.10.7] - 2017-09-29 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fix block gas limit estimation. -## 3.10.4 2017-9-27 - +## [3.10.4] - 2017-09-27 +### Uncategorized - 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 +### Uncategorized - 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 - -rollback to 3.10.0 due to bug - -## 3.10.1 2017-9-18 +## [3.10.2] - 2017-09-19 +### Uncategorized +- rollback to 3.10.0 due to bug +## [3.10.1] - 2017-09-18 +### Uncategorized - Add ability to export private keys as a file. - Add ability to export seed words as a file. - Changed state logs to a file download than a clipboard copy. @@ -1442,20 +1501,20 @@ 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 +### Uncategorized - Readded loose keyring label back into the account list. - Remove cryptonator from chrome permissions. - Add info on token contract addresses. - 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 +### Uncategorized - 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 +### Uncategorized - Fix bug that prevented Web3 1.0 compatibility - Make eth_sign deprecation warning less noisy - Add useful link to eth_sign deprecation warning. @@ -1466,60 +1525,60 @@ 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Reenable token list. - Remove default tokens. -## 3.9.7 2017-8-15 - +## [3.9.7] - 2017-08-15 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Improved phishing detection configuration update rate -## 3.9.4 2017-8-03 - +## [3.9.4] - 2017-08-04 +### Uncategorized - Fixed bug that prevented transactions from being rejected. -## 3.9.3 2017-8-03 - +## [3.9.3] - 2017-08-03 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - No longer automatically request 1 ropsten ether for the first account in a new vault. - Now redirects from known malicious sites faster. - Added a link to our new support page to the help screen. @@ -1527,46 +1586,46 @@ 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 +### Uncategorized - Now detects and blocks known phishing sites. -## 3.8.6 2017-7-11 - +## [3.8.6] - 2017-07-11 +### Uncategorized - 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 +### Uncategorized - Fix transaction resubmit logic to fail slightly less eagerly. -## 3.8.4 2017-7-7 - +## [3.8.4] - 2017-07-07 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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. - Fix precision bug in token balances. - 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 +### Uncategorized - No longer stop rebroadcasting transactions - Add list of popular tokens held to the account detail view. - Add ability to add Tokens to token list. @@ -1580,39 +1639,39 @@ 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 +### Uncategorized - Add an `ethereum:` prefix to the QR code address - The default network on installation is now MainNet - Fix currency API URL from cryptonator. - 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 +### Uncategorized - 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 +### Uncategorized - Fix bug that prevented publishing contracts. -## 3.7.5 2017-6-5 - +## [3.7.5] - 2017-06-05 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Rebuilt to fix cache clearing bug. -## 3.7.2 2017-5-31 - +## [3.7.2] - 2017-05-31 +### Uncategorized - Now when switching networks sites that use web3 will reload - Now when switching networks the extension does not restart - Cleanup decimal bugs in our gas inputs. @@ -1624,16 +1683,16 @@ 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 +### Uncategorized - Add Transaction Number (nonce) to transaction list. - Label the pending tx icon with a tooltip. - Fix bug where website filters would pile up and not deallocate when leaving a site. - 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 +### Uncategorized - Fix bug where edited gas parameters would not take effect. - Trim currency list. - Enable decimals in our gas prices. @@ -1641,56 +1700,56 @@ 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 +### Uncategorized - Fix main-net ENS resolution. -## 3.6.3 2017-5-8 - +## [3.6.3] - 2017-05-09 +### Uncategorized - 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 +### Uncategorized - Input gas price in Gwei. - Enforce Safe Gas Minimum recommended by EthGasStation. - Fix bug where block-tracker could stop polling for new blocks. - 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 +### Uncategorized - 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 +### Uncategorized - Add Rinkeby Test Network to our network list. -## 3.5.4 2017-4-25 - +## [3.5.4] - 2017-04-25 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Add better error messages for when a transaction fails on approval - Allow sending to ENS names in send form on Ropsten. - Added an address book functionality that remembers the last 15 unique addresses sent to. @@ -1704,62 +1763,57 @@ 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 +### Uncategorized - Add two most recently used custom RPCs to network dropdown menu. - Add personal_sign method support. - Add personal_ecRecover method support. - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fix "New Account" default keychain -## 3.1.1 2017-1-20 - +## [3.1.1] - 2017-01-20 +### Uncategorized - Fix HD wallet seed export -## 3.1.0 2017-1-18 - +## [3.1.0] - 2017-01-18 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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. - Create visible difference in transaction history between an approved but not yet included in a block transaction and a transaction who has been confirmed. @@ -1773,54 +1827,54 @@ 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Add support for the new, default Ropsten Test Network. - Fix bug that would cause MetaMask to occasionally lose its StreamProvider connection and drop requests. - Fix bug that would cause the Custom RPC menu item to not appear when Localhost 8545 was selected. - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Add a check for improper Transaction data. - Inject up to date version of web3.js - Now nicknaming new accounts "Account #" instead of "Wallet #" for clarity. @@ -1828,19 +1882,19 @@ 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fix bug where log queries were filtered out. - Decreased vault confirmation button font size to help some Linux users who could not see it. - Made popup a little taller because it would sometimes cut off buttons. @@ -1850,43 +1904,41 @@ 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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. +- Add a link to the transaction in history that goes to https://metamask.github.io/eth-tx-viz to 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 - -- Fixed bug where if you send a transaction from within MetaMask extension the - popup notification opens up. +## [2.12.1] - 2016-09-14 +### Uncategorized +- 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 +### Uncategorized - 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 +### Uncategorized - Fix bug that prevented caches from being cleared in Opera. -## 2.11.0 2016-09-12 - +## [2.11.0] - 2016-09-12 +### Uncategorized - Fix bug where pending transactions from Test net (or other networks) show up In Main net. - Add fiat conversion values to more views. - On fresh install, open a new tab with the MetaMask Introduction video. Does not open on update. @@ -1896,30 +1948,30 @@ 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 +### Uncategorized - Fix bug where notification popup would not display. -## 2.10.1 2016-09-02 - +## [2.10.1] - 2016-09-02 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fixed shortcut bug from preventing installation. -## 2.9.1 2016-08-24 - +## [2.9.1] - 2016-08-24 +### Uncategorized - Added static image as fallback for when WebGL isn't supported. - Transaction history now has a hard limit. - Added info link on account screen that visits Etherscan. @@ -1928,51 +1980,51 @@ 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 +### Uncategorized - 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 +### Uncategorized - Integrate ShapeShift - Add a form for Coinbase to specify amount to buy - Fix various typos. - Make dapp-metamask connection more reliable - Remove Ethereum Classic from provider menu. -## 2.7.3 2016-07-29 - +## [2.7.3] - 2016-07-29 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Fix tool tips on Eth balance to show the 6 decimals - Fix rendering of recipient SVG in tx approval notification. - New vaults now generate only one wallet instead of three. @@ -1980,8 +2032,8 @@ 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 +### Uncategorized - Fix formatting of ETH balance - Fix formatting of account details. - Use web3 minified dist for faster inject times @@ -1992,8 +2044,8 @@ 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 +### Uncategorized - Implement new account design. - Added a network indicator mark in dropdown menu - Added network name next to network indicator @@ -2001,8 +2053,8 @@ 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 +### Uncategorized - Fixed bug where MetaMask interfered with PDF loading. - Moved switch account icon into menu bar. - Changed status shapes to be a yellow warning sign for failure and ellipsis for pending transactions. @@ -2013,24 +2065,24 @@ 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 +### Uncategorized - Update web3-stream-provider for batch payload bug fix -## 2.4.3 2016-06-23 - +## [2.4.3] - 2016-06-23 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Clean up UI. - Remove nonfunctional QR code button. - Make network loading indicator clickable to select accessible network. @@ -2038,35 +2090,35 @@ 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Added copy address button to account list. - Fixed back button on confirm transaction screen. - Add indication of pending transactions to account list screen. - 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 +### Uncategorized - UI Overhaul per Vlad Todirut's designs. - Replaced identicons with jazzicons. - Fixed glitchy transitions. @@ -2075,28 +2127,28 @@ 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 +### Uncategorized - Point rpc servers to https endpoints. -## 1.8.3 2016-05-12 - +## [1.8.3] - 2016-05-12 +### Uncategorized - Bumped web3 to 0.6.0 - Really fixed `eth_syncing` method response. -## 1.8.2 2016-05-11 - +## [1.8.2] - 2016-05-11 +### Uncategorized - 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 +### Uncategorized - 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 +### Uncategorized - Add support for calls to `eth.sign`. - Moved account exporting within subview of the account detail view. - Added buttons to the account export process. @@ -2106,8 +2158,8 @@ 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 +### Uncategorized - Account detail view is now the primary view. - The account detail view now has a "Change acct" button which shows the account list. - Clicking accounts in the account list now both selects that account and displays that account's detail view. @@ -2120,8 +2172,8 @@ 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 +### Uncategorized - 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. - Added transaction list to account detail view. @@ -2132,22 +2184,22 @@ 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 +### Uncategorized - 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. - Fix Ether display rounding error. Now rendering to four decimal points. - 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 +### Uncategorized - 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 +### Uncategorized - Removed extra entropy text field for simplified vault creation. - Now supports exporting an account's private key. - Unified button and input styles across the app. @@ -2155,20 +2207,289 @@ 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 +### Uncategorized - 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 - -Made seed word restoring BIP44 compatible. - -## 0.14.0 - -Added the ability to restore accounts from seed words. +## [1.0.0] - 2016-03-25 +### Uncategorized +- Made seed word restoring BIP44 compatible. + +## [0.14.0] - 2016-03-16 +### Uncategorized +- Added the ability to restore accounts from seed words. + +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.3.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.5.0...HEAD +[9.5.0]: https://github.com/MetaMask/metamask-extension/compare/v9.4.0...v9.5.0 +[9.4.0]: https://github.com/MetaMask/metamask-extension/compare/v9.3.0...v9.4.0 +[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..a2845d6b0 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "መለያን በዘር ሐረግ አስመጣ" }, - "importUsingSeed": { - "message": "የመለያ የዘር ሐረግ በመጠቀም ያስመጡ" - }, "importWallet": { "message": "ቋት አስመጣ" }, - "importYourExisting": { - "message": "ባለ 12 ቃል የዘር ሐረግን በመጠቀም ነባር ቋትዎን ያስመጡ" - }, "imported": { "message": "ከውጭ የመጣ", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "መረጃ እና እገዛ " - }, "initialTransactionConfirmed": { "message": "የመጀመሪያ ግብይትዎ በአውታረ መረቡ ተረጋግጧል። ወደ ኋላ ለመመለስ እሺ የሚለውን ጠቅ ያድርጉ።" }, @@ -612,12 +603,6 @@ "myAccounts": { "message": "የእኔ መለያዎች" }, - "myWalletAccounts": { - "message": "የቋት መለያዎቼ" - }, - "myWalletAccountsDescription": { - "message": "በ MetaMask የተፈጠሩ መለያዎችዎ በሙሉ በራስ ሰር መንገድ ወደዚህ ክፍል ይታከላሉ።" - }, "needEtherInWallet": { "message": "MetaMask በመጠቀም ያልተማከሉ መተግበሪያዎች ጋር ግንኙነት ለማድረግ፣ በቋትዎ ውስጥ Ether ያስፈልግዎታል።" }, @@ -835,9 +820,6 @@ "restoreAccountWithSeed": { "message": "መለያዎን በዘር ሐረግ ወደነበረበት ይመልሱ" }, - "restoreFromSeed": { - "message": "መለያው ወደነበረበት ይመለስ?" - }, "revealSeedWords": { "message": "የዘር ቃላትን ይግለጹ" }, @@ -892,9 +874,6 @@ "secretBackupPhraseWarning": { "message": "ማስጠንቀቂያ፡ የመጠባበቂያ ምዕራፍዎን በጭራሽ አይግለጹ። ይህን ሐረገ የያዘ ማንኛውም ሰው የእርስዎን Ether እስከወዲያኛው ሊወስደው ይችላል።" }, - "secretPhrase": { - "message": "ካዝናዎን ወደነበረበት ለመመለስ ሚስጥራዊ ባለ አስራ ሁለት ቃል ሐረግዎን ያስገቡ።" - }, "securityAndPrivacy": { "message": "ደህንነት እና ግላዊነት" }, @@ -934,9 +913,6 @@ "sendAmount": { "message": "መጠኑን ላክ" }, - "sendETH": { - "message": "ETH ላክ" - }, "sendTokens": { "message": "ተለዋጭ ስሞችን ላክ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 9a04e15d8..680f6ab9f 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "استيراد حساب باستخدام عبارة الأمان" }, - "importUsingSeed": { - "message": "استيراد باستخدام عبارة أمان الحساب" - }, "importWallet": { "message": "استيراد المحفظة" }, - "importYourExisting": { - "message": "قم باستيراد محفظتك الحالية باستخدام جملة بذرية مكونة من 12 كلمة" - }, "imported": { "message": "المستوردة", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "المعلومات والمساعدة" - }, "initialTransactionConfirmed": { "message": "تم تأكيد المعاملة الأولية الخاصة بك بواسطة الشبكة. انقر فوق موافق للعودة." }, @@ -608,12 +599,6 @@ "myAccounts": { "message": "حساباتي" }, - "myWalletAccounts": { - "message": "حسابات محفظتي" - }, - "myWalletAccountsDescription": { - "message": "ستتم إضافة جميع حسابات MetaMask الخاصة بك والتي تم إنشاؤها تلقائياً إلى هذا القسم." - }, "needEtherInWallet": { "message": "للتفاعل مع التطبيقات اللامركزية باستخدام MetaMask، ستحتاج إلى الإيثير في محفظتك." }, @@ -831,9 +816,6 @@ "restoreAccountWithSeed": { "message": "قم باستعادة حسابك بواسطة عبارة الأمان" }, - "restoreFromSeed": { - "message": "هل تريد استعادة الحساب؟" - }, "revealSeedWords": { "message": "كشف كلمات عبارات الأمان" }, @@ -888,9 +870,6 @@ "secretBackupPhraseWarning": { "message": "تحذير: لا تكشف مطلقاً عن عبارة الدعم الخاصة بك. يمكن لأي شخص بهذه العبارة أن يستحوذ على الأثير الخاص بك إلى الأبد." }, - "secretPhrase": { - "message": "أدخل جملتك السرية المكونة من اثني عشر كلمة هنا لاستعادة خزنتك." - }, "securityAndPrivacy": { "message": "الأمن والخصوصية" }, @@ -930,9 +909,6 @@ "sendAmount": { "message": "إرسال المبلغ" }, - "sendETH": { - "message": "إرسال عملة إيثيريوم" - }, "sendTokens": { "message": "إرسال عملات رمزية" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 26abfe956..6878128f6 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "Импортирайте акаунт с фраза зародиш" }, - "importUsingSeed": { - "message": "Импортиране с помощта на фразата зародиш на профила" - }, "importWallet": { "message": "Импортиране на портфейла" }, - "importYourExisting": { - "message": "Импортирайте съществуващия си портфейл с помощта на фраза зародиш с 12 думи" - }, "imported": { "message": "Импортирани", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Информация и помощ" - }, "initialTransactionConfirmed": { "message": "Първоначалната транзакция беше потвърдена от мрежата. Кликнете върху OK, за да се върнете обратно." }, @@ -611,12 +602,6 @@ "myAccounts": { "message": "Моите акаунти" }, - "myWalletAccounts": { - "message": "Моите акаунти в портфейла" - }, - "myWalletAccountsDescription": { - "message": "Всичките ви създадени от MetaMask акаунти ще бъдат автоматично добавени в този раздел." - }, "needEtherInWallet": { "message": "За да взаимодействате с децентрализираните приложения, използвайки MetaMask, ще ви е необходим етер в портфейла ви." }, @@ -834,9 +819,6 @@ "restoreAccountWithSeed": { "message": "Възстановете акаунта си с фраза зародиш" }, - "restoreFromSeed": { - "message": "Да се възстанови ли акаунта?" - }, "revealSeedWords": { "message": "Разкрий думите зародиш" }, @@ -891,9 +873,6 @@ "secretBackupPhraseWarning": { "message": "ВНИМАНИЕ: Никога не разкривайте резервната си фраза. Всеки с тази фраза може да вземе вашия етер завинаги." }, - "secretPhrase": { - "message": "Въведете своята тайна фраза от дванадесет думи тук, за да възстановите портфейла си." - }, "securityAndPrivacy": { "message": "Сигурност и поверителност" }, @@ -933,9 +912,6 @@ "sendAmount": { "message": "Изпратете сумата" }, - "sendETH": { - "message": "Изпрати ETH" - }, "sendTokens": { "message": "Изпращане на жетони" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 2ef360f19..ee0272bfe 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "সীড ফ্রেজ দিয়ে একটি অ্যাকাউন্ট আমদানি করুন" }, - "importUsingSeed": { - "message": "অ্যাকাউন্ট সীড ফ্রেজ ব্যবহার করে আমদানি করুন" - }, "importWallet": { "message": "ওয়ালেট আমদানি করুন" }, - "importYourExisting": { - "message": "একটি 12 শব্দের সীড ফ্রেজ ব্যবহার করে আপনার বিদ্যমান ওয়ালেট আমদানি করুন" - }, "imported": { "message": "আমদানিকৃত", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "তথ্য ও সহায়তা" - }, "initialTransactionConfirmed": { "message": "আপনার প্রারম্ভিক লেনদেনটি নেটওয়ার্ক নিশ্চিত করেছিল। ফিরে যেতে ঠিক আছে তে ক্লিক করুন।" }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "আমার অ্যাকাউন্টগুলি" }, - "myWalletAccounts": { - "message": "আমার ওয়ালেট অ্যাকাউন্টগুলি" - }, - "myWalletAccountsDescription": { - "message": "MetaMask এর তৈরি করা আপনার সমস্ত অ্যাকাউন্টগুলি স্বয়ংক্রিয়ভাবে এই বিভাগে যোগ হয়ে যাবে।" - }, "needEtherInWallet": { "message": "MetaMask ব্যবহার করে ছড়িয়ে ছিটিয়ে থাকা অ্যাপ্লিকেশনগুলির সাথে যোগাযোগ করতে, আপনার ওয়ালেটে ইথার লাগবে।" }, @@ -838,9 +823,6 @@ "restoreAccountWithSeed": { "message": "সীড ফ্রেজ দিয়ে আপনার অ্যাকাউন্ট রিস্টোর করুন" }, - "restoreFromSeed": { - "message": "অ্যাকাউন্ট রিস্টোর করবেন?" - }, "revealSeedWords": { "message": "সীড শব্দগুলি প্রকাশ করুন" }, @@ -895,9 +877,6 @@ "secretBackupPhraseWarning": { "message": "সতর্কতা: কখনও আপনার ব্যাকআপ ফ্রেজ প্রকাশ করবেন না। এই ফ্রেজ পেয়ে গেলে যে কেউ আপনার ইথার চিরকালের জন্য নিয়ে নিতে পারবেন।" }, - "secretPhrase": { - "message": "আপনার ভল্ট রিস্টোর করতে এখানে আপনার গোপন বারো শব্দের ফ্রেজ লিখুন।" - }, "securityAndPrivacy": { "message": "নিরাপত্তা এবং গোপনীয়তা" }, @@ -937,9 +916,6 @@ "sendAmount": { "message": "পাঠানো অর্থরাশি" }, - "sendETH": { - "message": "ETH পাঠান" - }, "sendTokens": { "message": "টোকেনগুলি পাঠান" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index fda999248..6d0df200a 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -486,22 +486,13 @@ "importAccountSeedPhrase": { "message": "Importa un Compte amb Phrase de Recuperació" }, - "importUsingSeed": { - "message": "Importa utilitzant la frase de seeds del compte" - }, "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" }, - "infoHelp": { - "message": "Informació i Ajuda" - }, "initialTransactionConfirmed": { "message": "La teva transacció inicial ha sigut confirmada per la xarxa. Fes clic a OK per tornar enrere." }, @@ -599,12 +590,6 @@ "myAccounts": { "message": "Els meus Comptes" }, - "myWalletAccounts": { - "message": "Els meus Comptes de Moneder" - }, - "myWalletAccountsDescription": { - "message": "Tots els teus comptes creats a MetaMask s'afegiran automàticament a aquesta secció." - }, "needEtherInWallet": { "message": "Per a interactuar amb aplicacions descentralitzades fent servir MetaMask, necessitaràs Ether al teu moneder." }, @@ -816,9 +801,6 @@ "restoreAccountWithSeed": { "message": "Restaura el teu compte amb Frase de Recuperació" }, - "restoreFromSeed": { - "message": "Restaurar compte?" - }, "revealSeedWords": { "message": "Revelar Paraules de Recuperació" }, @@ -873,9 +855,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 +894,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..442b91544 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -192,9 +192,6 @@ "message": "Importováno", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Informace a nápověda" - }, "insufficientFunds": { "message": "Nedostatek finančních prostředků." }, @@ -326,9 +323,6 @@ "resetAccount": { "message": "Resetovat účet" }, - "restoreFromSeed": { - "message": "Obnovit z seed fráze" - }, "revealSeedWords": { "message": "Zobrazit slova klíčové fráze" }, @@ -344,9 +338,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 +347,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..a9ce79ad1 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -492,22 +492,13 @@ "importAccountSeedPhrase": { "message": "Importér en Konto med Seedfrase" }, - "importUsingSeed": { - "message": "Importér ved brug af kontoens seed-sætning" - }, "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" }, - "infoHelp": { - "message": "Info & hjælp" - }, "initialTransactionConfirmed": { "message": "Din indledende transaktion blev bekræftet af netværket. Klik OK for at gå tilbage." }, @@ -599,12 +590,6 @@ "myAccounts": { "message": "Mine Konti" }, - "myWalletAccounts": { - "message": "Mine Pungkonti" - }, - "myWalletAccountsDescription": { - "message": "Alle dine MetaMask-oprettede konti føjes automatisk til dette afsnit." - }, "needEtherInWallet": { "message": "Du skal have Ether i din tegnebog for at interagere med decentraliserede applikationer, der bruger MetaMask." }, @@ -819,9 +804,6 @@ "restoreAccountWithSeed": { "message": "Gendan din konto med Seed-sætning" }, - "restoreFromSeed": { - "message": "Genopret konto?" - }, "revealSeedWords": { "message": "Vis Seedord" }, @@ -876,9 +858,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 +894,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..6ec0ae92f 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -487,22 +487,13 @@ "importAccountSeedPhrase": { "message": "Ein Konto mit einem Seed-Schlüssel importieren" }, - "importUsingSeed": { - "message": "Mittels mnemonischer Phrase des Kontos importieren" - }, "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" }, - "infoHelp": { - "message": "Info & Hilfe" - }, "initialTransactionConfirmed": { "message": "Ihre erste Transaktion wurde vom Netzwerk bestätigt. Klicken Sie auf Okay, um zurückzukehren." }, @@ -594,12 +585,6 @@ "myAccounts": { "message": "Meine Accounts" }, - "myWalletAccounts": { - "message": "Meine Wallet-Konten" - }, - "myWalletAccountsDescription": { - "message": "Jedes Ihrer von MetaMask erstellten Konten wird automatisch zu diesem Abschnitt hinzugefügt." - }, "needEtherInWallet": { "message": "Um dezentralisierte Applikationen mit MetaMask verwenden zu können, benötigst du Ether in deiner Wallet." }, @@ -807,9 +792,6 @@ "restoreAccountWithSeed": { "message": "Ihr Konto mit mnemonischer Phrase wiederherstellen" }, - "restoreFromSeed": { - "message": "Mit Hilfe der Seed-Wörterfolge wiederherstellen." - }, "revealSeedWords": { "message": "Seed-Wörterfolge anzeigen" }, @@ -864,9 +846,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 +885,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..2add7cd8b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -496,22 +496,13 @@ "importAccountSeedPhrase": { "message": "Εισαγωγή λογαριασμού με Φράση Φύτρου" }, - "importUsingSeed": { - "message": "Εισαγωγή με χρήση φάσης σπόρου λογαριασμού" - }, "importWallet": { "message": "Εισαγωγή Πορτοφολιού" }, - "importYourExisting": { - "message": "Εισαγάγετε το υπάρχον πορτοφόλι σας χρησιμοποιώντας μια φράση φύτρου 12 λέξεων" - }, "imported": { "message": "Έγινε εισαγωγή", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Πληροφορίες & Βοήθεια" - }, "initialTransactionConfirmed": { "message": "Η αρχική σας συναλλαγή επιβεβαιώθηκε από το δίκτυο. Πατήστε ΕΝΤΑΞΕΙ για επιστροφή." }, @@ -612,12 +603,6 @@ "myAccounts": { "message": "Οι Λογαριασμοί μου" }, - "myWalletAccounts": { - "message": "Λογαριασμοί Πορτοφολιού" - }, - "myWalletAccountsDescription": { - "message": "Όλοι οι λογαριασμοί σας που έχετε δημιουργήσει στο MetaMask θα προστεθούν αυτόματα σε αυτήν την ενότητα." - }, "needEtherInWallet": { "message": "Για να αλληλεπιδράσετε με αποκεντρωμένες εφαρμογές χρησιμοποιώντας το MetaMask, θα χρειαστείτε Ether στο πορτοφόλι σας." }, @@ -835,9 +820,6 @@ "restoreAccountWithSeed": { "message": "Επαναφέρετε τον Λογαριασμό σας με Φράση Επαναφοράς" }, - "restoreFromSeed": { - "message": "Επαναφορά λογαριασμού;" - }, "revealSeedWords": { "message": "Αποκάλυψη Λέξεων Φύτρου" }, @@ -892,9 +874,6 @@ "secretBackupPhraseWarning": { "message": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Ποτέ μην αποκαλύπτετε την εφεδρική φράση. Όποιος έχει αυτή τη φράση μπορεί να πάρει τα Ether σας για πάντα." }, - "secretPhrase": { - "message": "Εισαγάγετε εδώ τη μυστική φράση δώδεκα λέξεων για να επαναφέρετε το χρηματοκιβώτιό σας." - }, "securityAndPrivacy": { "message": "Ασφάλεια και Απόρρητο" }, @@ -934,9 +913,6 @@ "sendAmount": { "message": "Αποστολή Ποσού" }, - "sendETH": { - "message": "Στείλτε ETH" - }, "sendTokens": { "message": "Στείλτε Tokens" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index fe1d4f7e9..be5a90306 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -43,15 +43,15 @@ "activityLog": { "message": "activity log" }, - "addAccount": { - "message": "Add an account" - }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, "addAlias": { "message": "Add alias" }, + "addContact": { + "message": "Add contact" + }, "addEthereumChainConfirmationDescription": { "message": "This will allow this network to be used within MetaMask." }, @@ -69,6 +69,9 @@ "addEthereumChainConfirmationTitle": { "message": "Allow this site to add a network?" }, + "addFriendsAndAddresses": { + "message": "Add friends and addresses you trust" + }, "addNetwork": { "message": "Add Network" }, @@ -176,6 +179,9 @@ "asset": { "message": "Asset" }, + "assetOptions": { + "message": "Asset options" + }, "assets": { "message": "Assets" }, @@ -243,6 +249,9 @@ "browserNotSupported": { "message": "Your Browser is not supported..." }, + "builContactList": { + "message": "Build your contact list" + }, "builtInCalifornia": { "message": "MetaMask is designed and built in California." }, @@ -574,6 +583,12 @@ "editContact": { "message": "Edit Contact" }, + "editNonceField": { + "message": "Edit Nonce" + }, + "editNonceMessage": { + "message": "This is an advanced feature, use cautiously." + }, "editPermission": { "message": "Edit Permission" }, @@ -856,28 +871,29 @@ "importAccount": { "message": "Import Account" }, + "importAccountLinkText": { + "message": "import using seed phrase" + }, "importAccountMsg": { "message": " Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts " }, "importAccountSeedPhrase": { "message": "Import an account with seed phrase" }, - "importUsingSeed": { - "message": "Import using account seed phrase" + "importAccountText": { + "message": "or $1", + "description": "$1 represents the text from `importAccountLinkText` as a link" }, "importWallet": { "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", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Info & Help" - }, "infuraBlockedNotification": { "message": "MetaMask is unable to connect to the blockchain host. Review possible reasons $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -1064,7 +1080,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": { @@ -1080,15 +1096,16 @@ "myAccounts": { "message": "My Accounts" }, - "myWalletAccounts": { - "message": "My Wallet Accounts" - }, - "myWalletAccountsDescription": { - "message": "All of your MetaMask created accounts will automatically be added to this section." - }, "needEtherInWallet": { "message": "To interact with decentralized applications using MetaMask, you’ll need Ether in your wallet." }, + "needHelp": { + "message": "Need help? Contact $1", + "description": "$1 represents `needHelpLinkText`, the text which goes in the help link" + }, + "needHelpLinkText": { + "message": "MetaMask Support" + }, "needImportFile": { "message": "You must select a file to import.", "description": "User is important an account and needs to add a file to continue" @@ -1102,9 +1119,18 @@ "networkName": { "message": "Network Name" }, + "networkNameBSC": { + "message": "BSC" + }, "networkNameDefinition": { "message": "The name associated with this network." }, + "networkNameEthereum": { + "message": "Ethereum" + }, + "networkNameTestnet": { + "message": "Testnet" + }, "networkSettingsChainIdDescription": { "message": "The chain ID is used for signing transactions. It must match the chain ID returned by the network. You can enter a decimal or '0x'-prefixed hexadecimal number, but we will display the number in decimal." }, @@ -1185,6 +1211,9 @@ "noWebcamFoundTitle": { "message": "Webcam not found" }, + "nonce": { + "message": "Nonce" + }, "nonceField": { "message": "Customize transaction nonce" }, @@ -1200,6 +1229,38 @@ "notEnoughGas": { "message": "Not Enough Gas" }, + "notifications1Description": { + "message": "MetaMask Mobile users can now swap tokens inside their mobile wallet. Scan the QR code to get the mobile app and start swapping.", + "description": "Description of a notification in the 'See What's New' popup. Describes the swapping on mobile feature." + }, + "notifications1Title": { + "message": "Swapping on mobile is here!", + "description": "Title for a notification in the 'See What's New' popup. Tells users that they can now use MetaMask Swaps on Mobile." + }, + "notifications2ActionText": { + "message": "Start survey", + "description": "The 'call to action' label on the button, or link, of the 'Help improve MetaMask' 'See What's New' notification. Upon clicking, users will be taken to an external page where they can complete a survey." + }, + "notifications2Description": { + "message": "Please share your experience in this 5 minute survey.", + "description": "Description of a notification in the 'See What's New' popup. Further clarifies how the users can help: by completing a 5 minute survey about MetaMask." + }, + "notifications2Title": { + "message": "Help improve MetaMask", + "description": "Title for a notification in the 'See What's New' popup. Asks users to take action to make MetaMask better." + }, + "notifications3ActionText": { + "message": "Read more", + "description": "The 'call to action' on the button, or link, of the 'Stay secure' notification. Upon clicking, users will be taken to a page about security on the metamask support website." + }, + "notifications3Description": { + "message": "Stay up to date on MetaMask security best practices and get the latest security tips from official MetaMask support.", + "description": "Description of a notification in the 'See What's New' popup. Describes the information they can get on security from the linked support page." + }, + "notifications3Title": { + "message": "Stay secure", + "description": "Title for a notification in the 'See What's New' popup. Encourages users to consider security." + }, "ofTextNofM": { "message": "of" }, @@ -1381,9 +1442,6 @@ "restoreAccountWithSeed": { "message": "Restore your Account with Seed Phrase" }, - "restoreFromSeed": { - "message": "Restore account?" - }, "restoreWalletPreferences": { "message": "A backup of your data from $1 has been found. Would you like to restore your wallet preferences?", "description": "$1 is the date at which the data was backed up" @@ -1455,7 +1513,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" @@ -1511,9 +1569,6 @@ "sendAmount": { "message": "Send Amount" }, - "sendETH": { - "message": "Send ETH" - }, "sendSpecifiedTokens": { "message": "Send $1", "description": "Symbol of the specified token" @@ -1660,6 +1715,9 @@ "submitted": { "message": "Submitted" }, + "support": { + "message": "Support" + }, "supportCenter": { "message": "Visit our Support Center" }, @@ -1745,24 +1803,6 @@ "swapHighSlippageWarning": { "message": "Slippage amount is very high. Make sure you know what you are doing!" }, - "swapIntroLearnMoreHeader": { - "message": "Want to learn more?" - }, - "swapIntroLearnMoreLink": { - "message": "Learn more about MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Liquidity sources include:" - }, - "swapIntroPopupSubTitle": { - "message": "You can now swap tokens directly in your MetaMask wallet. MetaMask Swaps combines multiple decentralized exchange aggregators, professional market makers, and individual DEXs to ensure MetaMask users always get the best price with the lowest network fees." - }, - "swapIntroPopupTitle": { - "message": "Token swapping is here!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Review our official contracts audit" - }, "swapLowSlippageError": { "message": "Transaction may fail, max slippage too low." }, @@ -1788,7 +1828,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "The network fee covers the cost of processing your swap and storing it on the Ethereum network. MetaMask does not profit from this fee." + "message": "The network fee covers the cost of processing your swap and storing it on the $1 network. MetaMask does not profit from this fee." }, "swapNewQuoteIn": { "message": "New quotes in $1", @@ -1889,9 +1929,6 @@ "swapSourceInfo": { "message": "We search multiple liquidity sources (exchanges, aggregators and professional market makers) to find the best rates and lowest network fees." }, - "swapStartSwapping": { - "message": "Start swapping" - }, "swapSwapFrom": { "message": "Swap from" }, @@ -2057,9 +2094,6 @@ "tokenContractAddress": { "message": "Token Contract Address" }, - "tokenOptions": { - "message": "Token options" - }, "tokenSymbol": { "message": "Token Symbol" }, @@ -2222,6 +2256,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." @@ -2232,6 +2269,10 @@ "welcomeBack": { "message": "Welcome Back!" }, + "whatsNew": { + "message": "What's new", + "description": "This is the title of a popup that gives users notifications about new features and updates to MetaMask." + }, "whatsThis": { "message": "What's this?" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 4a160692e..83540a855 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "registro de actividades" }, - "addAccount": { - "message": "Agregar una cuenta" - }, "addAcquiredTokens": { "message": "Agregar los tokens que has adquirido usando MetaMask" }, @@ -790,22 +787,13 @@ "importAccountSeedPhrase": { "message": "Importar una cuenta con la frase semilla" }, - "importUsingSeed": { - "message": "Importar usando la frase semilla de la cuenta" - }, "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" }, - "infoHelp": { - "message": "Información y ayuda" - }, "initialTransactionConfirmed": { "message": "La red confirmó tu transacción inicial. Hazle clic en Ok para volver." }, @@ -993,12 +981,6 @@ "myAccounts": { "message": "Mis cuentas" }, - "myWalletAccounts": { - "message": "Mis cuentas de monedero" - }, - "myWalletAccountsDescription": { - "message": "Todas tus cuentas creadas con MetaMask serán automáticamente agregadas a esta sección." - }, "needEtherInWallet": { "message": "Para interactuar con una aplicación descentralizada usando MetaMask, necesitas tener Ether en tu monedero" }, @@ -1282,9 +1264,6 @@ "restoreAccountWithSeed": { "message": "Restaurar tu Cuenta con la Frase Semilla" }, - "restoreFromSeed": { - "message": "¿Restaurar cuenta?" - }, "restoreWalletPreferences": { "message": "Se ha encontrado una copia de seguridad de sus datos de $1. ¿Le gustaría restaurar sus preferencias de monedero?", "description": "$1 is the date at which the data was backed up" @@ -1355,9 +1334,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 +1385,6 @@ "sendAmount": { "message": "Enviar cantidad" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendSpecifiedTokens": { "message": "Enviar $1", "description": "Symbol of the specified token" @@ -1639,24 +1612,6 @@ "swapHighSlippageWarning": { "message": "La cantidad de deslizamiento es muy alta. ¡Asegúrate de saber lo que estás haciendo!" }, - "swapIntroLearnMoreHeader": { - "message": "¿Quiere aprender más?" - }, - "swapIntroLearnMoreLink": { - "message": "Más información sobre los Intercambios MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Las fuentes de liquidez incluyen:" - }, - "swapIntroPopupSubTitle": { - "message": "Ahora puede intercambiar tokens directamente en su monedero MetaMask. Intercambios MetaMask combina múltiples agregadores de intercambio descentralizados, creadores de mercado profesionales y DEX individuales para garantizar que los usuarios de MetaMask siempre obtengan el mejor precio con las tarifas de red más bajas." - }, - "swapIntroPopupTitle": { - "message": "¡El intercambio de tokens está aquí!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Revise nuestra auditoría de contratos oficiales" - }, "swapLowSlippageError": { "message": "La transacción puede fallar, el deslizamiento máximo es demasiado bajo." }, @@ -1682,7 +1637,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "La tarifa de la red cubre el costo de procesar su intercambio y almacenarlo en la red Ethereum. MetaMask no se beneficia de esta tarifa." + "message": "La tarifa de la red cubre el costo de procesar su intercambio y almacenarlo en la red $1. MetaMask no se beneficia de esta tarifa." }, "swapNewQuoteIn": { "message": "Nuevas cotizaciones en $1", @@ -1777,9 +1732,6 @@ "swapSourceInfo": { "message": "Buscamos múltiples fuentes de liquidez (exchanges, agregadores y creadores de mercado profesionales) para encontrar las mejores tarifas y las tarifas de red más bajas." }, - "swapStartSwapping": { - "message": "Comenzar intercambio" - }, "swapSwapFrom": { "message": "Intercambiar desde" }, @@ -1915,9 +1867,6 @@ "tokenContractAddress": { "message": "Dirección del contrato de token" }, - "tokenOptions": { - "message": "Opciones del Token" - }, "tokenSymbol": { "message": "Símbolo del token" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index f7b9d815f..1711d2a05 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "registro de actividad" }, - "addAccount": { - "message": "Agregar una cuenta" - }, "addAcquiredTokens": { "message": "Agrega los tokens adquiridos con MetaMask" }, @@ -790,22 +787,13 @@ "importAccountSeedPhrase": { "message": "Importar una cuenta con frase semilla" }, - "importUsingSeed": { - "message": "Importar usando la frase semilla de la cuenta" - }, "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" }, - "infoHelp": { - "message": "Información y ayuda" - }, "initialTransactionConfirmed": { "message": "La red ha confirmado tu transacción inicial. Haz clic en Aceptar para volver." }, @@ -993,12 +981,6 @@ "myAccounts": { "message": "Mis cuentas" }, - "myWalletAccounts": { - "message": "Las cuentas de mi billetera" - }, - "myWalletAccountsDescription": { - "message": "Todas tus cuentas creadas en MetaMask se agregarán automáticamente a esta sección." - }, "needEtherInWallet": { "message": "Necesitarás tener Ether en tu billetera para poder interactuar con aplicaciones descentralizadas a través de MetaMask." }, @@ -1282,9 +1264,6 @@ "restoreAccountWithSeed": { "message": "Restaura tu Cuenta con la Frase Semilla" }, - "restoreFromSeed": { - "message": "¿Restaurar cuenta?" - }, "restoreWalletPreferences": { "message": "Se ha encontrado una copia de seguridad de sus datos de $1. ¿Le gustaría restaurar sus preferencias de billetera?", "description": "$1 is the date at which the data was backed up" @@ -1355,9 +1334,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 +1385,6 @@ "sendAmount": { "message": "Enviar monto" }, - "sendETH": { - "message": "Enviar ETH" - }, "sendSpecifiedTokens": { "message": "Enviar $1", "description": "Symbol of the specified token" @@ -1639,24 +1612,6 @@ "swapHighSlippageWarning": { "message": "La cantidad de deslizamiento es muy alta. ¡Asegúrate de saber lo que estás haciendo!" }, - "swapIntroLearnMoreHeader": { - "message": "¿Quiere aprender más?" - }, - "swapIntroLearnMoreLink": { - "message": "Más información sobre los Intercambios MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Las fuentes de liquidez incluyen:" - }, - "swapIntroPopupSubTitle": { - "message": "Ahora puede intercambiar tokens directamente en su billetera MetaMask. Intercambios MetaMask combina múltiples agregadores de intercambio descentralizados, creadores de mercado profesionales y DEX individuales para garantizar que los usuarios de MetaMask siempre obtengan el mejor precio con las tarifas de red más bajas." - }, - "swapIntroPopupTitle": { - "message": "¡El intercambio de tokens está aquí!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Revise nuestra auditoría de contratos oficiales" - }, "swapLowSlippageError": { "message": "La transacción puede fallar, el deslizamiento máximo es demasiado bajo." }, @@ -1682,7 +1637,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "La tarifa de la red cubre el costo de procesar su intercambio y almacenarlo en la red Ethereum. MetaMask no se beneficia de esta tarifa." + "message": "La tarifa de la red cubre el costo de procesar su intercambio y almacenarlo en la red $1. MetaMask no se beneficia de esta tarifa." }, "swapNewQuoteIn": { "message": "Nuevas cotizaciones en $1", @@ -1777,9 +1732,6 @@ "swapSourceInfo": { "message": "Buscamos múltiples fuentes de liquidez (exchanges, agregadores y creadores de mercado profesionales) para encontrar las mejores tarifas y las tarifas de red más bajas." }, - "swapStartSwapping": { - "message": "Comenzar intercambio" - }, "swapSwapFrom": { "message": "Intercambiar desde" }, @@ -1915,9 +1867,6 @@ "tokenContractAddress": { "message": "Dirección de contrato del token" }, - "tokenOptions": { - "message": "Opciones del Token" - }, "tokenSymbol": { "message": "Símbolo del token" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index e2cfe7aa3..81e7154f1 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "Impordi seemnefraasiga konto" }, - "importUsingSeed": { - "message": "Impordi konto seemnefraasi abil" - }, "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" }, - "infoHelp": { - "message": "Teave ja abi" - }, "initialTransactionConfirmed": { "message": "Võrk kinnitas teie algse tehingu. Tagasi minemiseks klõpsake OK-nuppu." }, @@ -605,12 +596,6 @@ "myAccounts": { "message": "Minu kontod" }, - "myWalletAccounts": { - "message": "Minu rahakotikontod" - }, - "myWalletAccountsDescription": { - "message": "Sellesse jaotisesse lisatakse automaatselt kõik teie MetaMaski kontod." - }, "needEtherInWallet": { "message": "Selleks, et suhelda MetaMaski abil detsentraliseeritud rakendustega, peab teil rahakotis eetrit olema." }, @@ -828,9 +813,6 @@ "restoreAccountWithSeed": { "message": "Taastage konto seemnefraasi abil" }, - "restoreFromSeed": { - "message": "Taastada konto?" - }, "revealSeedWords": { "message": "Kuva seemnesõnu" }, @@ -885,9 +867,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 +906,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..54e0a6e90 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "یک حساب با عبارت بازیاب را وارد کنید" }, - "importUsingSeed": { - "message": "وارد کردن با استفاده از عبارت بازیاب حساب" - }, "importWallet": { "message": "وارد سازی کیف" }, - "importYourExisting": { - "message": "کیف موجود تان را با استفاده از عبارت رمزیاب 12 کلمه یی وارد کنید" - }, "imported": { "message": "وارد شده", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "معلومات و کمک" - }, "initialTransactionConfirmed": { "message": "معامله آغازین تان توسط شبکه تأیید شد. برای رفتن به عقب بالای OK کلیک کنید." }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "حساب های من" }, - "myWalletAccounts": { - "message": "حساب های کیف من" - }, - "myWalletAccountsDescription": { - "message": "همه حساب های ایجاد شده MetaMask شما بصورت خودکار در این بخش اضافه خواهد شد." - }, "needEtherInWallet": { "message": "برای تعامل با اپلیکیشن های غیر متمرکز شده با استفاده از MetaMask، شما نیاز به ایتر در کیف تان خواهید داشت." }, @@ -838,9 +823,6 @@ "restoreAccountWithSeed": { "message": "حساب تان را با عبارت بازیاب، بازیابی کنید" }, - "restoreFromSeed": { - "message": "بازگرداندن حساب؟" - }, "revealSeedWords": { "message": "کلمات بازیاب را آشکار کنید" }, @@ -895,9 +877,6 @@ "secretBackupPhraseWarning": { "message": "هشدار: هرگز عبارت پشتیبان تان را به کسی فاش نسازید. هرکسیکه این عبارت را داشته باشد ایتر شما را برای همیشه خواهد گرفت." }, - "secretPhrase": { - "message": "جهت بازیابی خزانه تان عبارت دوازده کلمه یی تان را اینجا وارد کنید." - }, "securityAndPrivacy": { "message": "امنیت و حریم خصوصی" }, @@ -937,9 +916,6 @@ "sendAmount": { "message": "ارسال مبلغ" }, - "sendETH": { - "message": "ارسال ETH" - }, "sendTokens": { "message": "رمزیاب ها را ارسال کنید" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index fa5bd1125..e9f2ede88 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "Tuo tili siemenlauseella" }, - "importUsingSeed": { - "message": "Tuo käyttäen tilin siemenlausetta" - }, "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" }, - "infoHelp": { - "message": "Tietoja ja ohje" - }, "initialTransactionConfirmed": { "message": "Verkko hyväksyi alkuperäisen tapahtumasi. Siirry takaisin napsauttamalla OK-painiketta." }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "Omat tilit" }, - "myWalletAccounts": { - "message": "Omat kukkarotilini" - }, - "myWalletAccountsDescription": { - "message": "Kaikki MetaMaskilla luodut tilisi lisätään automaattisesti tähän osastoon." - }, "needEtherInWallet": { "message": "Ollaksesi vuorovaikutuksessa hajautettujen sovellusten kanssa sinulla on oltava lompakossasi Ether." }, @@ -835,9 +820,6 @@ "restoreAccountWithSeed": { "message": "Palauta tilisi käyttäen salaustekstiä (seed phrase)" }, - "restoreFromSeed": { - "message": "Palautetaanko tili?" - }, "revealSeedWords": { "message": "Paljasta salaussanat" }, @@ -892,9 +874,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 +913,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..81ad47621 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -456,22 +456,13 @@ "importAccountSeedPhrase": { "message": "Mag-import ng Account gamit ang Seed Phrase" }, - "importUsingSeed": { - "message": "Mag-import gamit ang seed phrase ng account" - }, "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" }, - "infoHelp": { - "message": "Impormasyon at Tulong" - }, "initialTransactionConfirmed": { "message": "Nakumpirma ng network ang iyong unang transaksyon. I-click ang OK para bumalik." }, @@ -553,12 +544,6 @@ "myAccounts": { "message": "Mga Account Ko" }, - "myWalletAccounts": { - "message": "Mga Wallet Account Ko" - }, - "myWalletAccountsDescription": { - "message": "Ang lahat ng iyong account na ginawa sa MetaMask ay awtomatikong idadagdag sa seksyong ito." - }, "needEtherInWallet": { "message": "Para gumamit ng mga decentralized na application gamit ang MetaMask, mangangailangan ka ng Ether sa iyong wallet." }, @@ -762,9 +747,6 @@ "restoreAccountWithSeed": { "message": "I-restore ang iyong Account gamit ang Seed Phrase" }, - "restoreFromSeed": { - "message": "I-restore ang account?" - }, "revealSeedWords": { "message": "Ipakita ang Seed Words" }, @@ -807,9 +789,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 +828,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..fcfc5869c 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -490,22 +490,13 @@ "importAccountSeedPhrase": { "message": "Importez un compte avec une phrase mnémotechnique" }, - "importUsingSeed": { - "message": "Importer à partir de la phrase Seed du compte" - }, "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" }, - "infoHelp": { - "message": "Info & Aide" - }, "initialTransactionConfirmed": { "message": "Votre transaction initiale a été confirmée par le réseau. Cliquez sur OK pour retourner à l'écran précédent." }, @@ -600,12 +591,6 @@ "myAccounts": { "message": "Mes comptes" }, - "myWalletAccounts": { - "message": "Mes comptes de portefeuille" - }, - "myWalletAccountsDescription": { - "message": "Tous vos comptes MetaMask créés seront automatiquement ajoutés à cette section." - }, "needEtherInWallet": { "message": "Pour interagir avec des applications décentralisées à l'aide de MetaMask, vous avez besoin d'Ether dans votre portefeuille." }, @@ -820,9 +805,6 @@ "restoreAccountWithSeed": { "message": "Restaurer votre compte avec une phrase Seed." }, - "restoreFromSeed": { - "message": "Restaurer le compte ?" - }, "revealSeedWords": { "message": "Révéler les mots Seed" }, @@ -877,9 +859,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 +898,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..8bb39ef27 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "יבא חשבון באמצעות Seed Phrase" }, - "importUsingSeed": { - "message": "ייבא באמצעות צירוף הגרעין של החשבון" - }, "importWallet": { "message": "ייבא ארנק" }, - "importYourExisting": { - "message": "יבא/י את הארנק הקיים שלך באמצעות seed phrase בן 12 מילים" - }, "imported": { "message": "מיובאות", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "מידע ועזרה" - }, "initialTransactionConfirmed": { "message": "העסקה הראשונית שלך אושרה על ידי הרשת. לחצ/י על אישור כדי לחזור." }, @@ -612,12 +603,6 @@ "myAccounts": { "message": "החשבונות שלי" }, - "myWalletAccounts": { - "message": "חשבונות הארנק שלי" - }, - "myWalletAccountsDescription": { - "message": "כל חשבונותיך שנוצרו ב-MetaMask יתווספו אוטומטית לחלק זה." - }, "needEtherInWallet": { "message": "כדי לתקשר עם אפליקציות מבוזרות באמצעות MetaMask, צריך להיות לך את'ר בארנק." }, @@ -835,9 +820,6 @@ "restoreAccountWithSeed": { "message": "שחזר את חשבונך באמצעות צירוף הגרעין" }, - "restoreFromSeed": { - "message": "לשחזר את החשבון?" - }, "revealSeedWords": { "message": "גלה מילות Seed" }, @@ -889,9 +871,6 @@ "secretBackupPhraseWarning": { "message": "אזהרה: לעולם אין לחשוף את צירוף הגיבוי שלך. כל מי שברשותו צירוף זה יכול לקחת את האת'ר שלך לצמיתות." }, - "secretPhrase": { - "message": "הזנ/י את הצירוף הסודי שלך של שתים-עשרה המילים כאן כדי לשחזר את הכספת שלך." - }, "securityAndPrivacy": { "message": "אבטחה ופרטיות" }, @@ -931,9 +910,6 @@ "sendAmount": { "message": "שלח סכום" }, - "sendETH": { - "message": "שלח/י ETH" - }, "sendTokens": { "message": "שלח טוקנים" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9e8d76544..4ece9cce8 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "गतिविधि लॉग" }, - "addAccount": { - "message": "एक खाता जोड़ें" - }, "addAcquiredTokens": { "message": "आपके द्वारा MetaMask का उपयोग करके प्राप्त किए गए टोकन जोड़ें" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "सीडफ्रेज़ के साथ कोई खाता आयात करें" }, - "importUsingSeed": { - "message": "खाता सीडफ्रेज़ का उपयोग करके आयात करें" - }, "importWallet": { "message": "वॉलेट आयात करें" }, - "importYourExisting": { - "message": "12 शब्द के सीडफ्रेज़ का उपयोग करके अपने मौजूदा वॉलेट को आयात करें" - }, "imported": { "message": "आयातित", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "जानकारी और मदद" - }, "initialTransactionConfirmed": { "message": "नेटवर्क द्वारा आपके प्रारंभिक लेनदेन की पुष्टि की गई थी। वापस जाने के लिए ठीक पर क्लिक करें।" }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "मेरे खाते" }, - "myWalletAccounts": { - "message": "मेरे वॉलेट खाते" - }, - "myWalletAccountsDescription": { - "message": "आपके सभी MetaMask निर्मित खाते स्वतः ही इस अनुभाग में जोड़ दिए जाएँगे।" - }, "needEtherInWallet": { "message": "MetaMask का उपयोग करके विकेंद्रीकृत एप्लिकेशन के साथ सहभागिता करने के लिए, आपको अपने वॉलेट में Ether की आवश्यकता होगी।" }, @@ -1273,9 +1255,6 @@ "restoreAccountWithSeed": { "message": "सीड फ्रेज़ के साथ अपने खाते को पुनर्स्थापित करें" }, - "restoreFromSeed": { - "message": "खाता पुनर्स्थापित करें?" - }, "restoreWalletPreferences": { "message": "$1 से आपके डेटा का बैकअप मिला है। क्या आप अपनी वॉलेट वरीयताओं को पुनर्स्थापित करना चाहते हैं?", "description": "$1 is the date at which the data was backed up" @@ -1346,9 +1325,6 @@ "secretBackupPhraseWarning": { "message": "चेतावनी: कभी भी अपने बैकअप वाक्यांश का खुलासा न करें। इस वाक्यांश के साथ कोई भी आपके Ether को हमेशा के लिए ले सकता है।" }, - "secretPhrase": { - "message": "अपनी तिजोरी को पुनर्स्थापित करने के लिए अपने गुप्त बारह शब्द वाक्यांश को यहाँ दर्ज करें।" - }, "securityAndPrivacy": { "message": "सुरक्षा और गोपनीयता" }, @@ -1400,9 +1376,6 @@ "sendAmount": { "message": "राशि भेजें" }, - "sendETH": { - "message": "ETH भेजें" - }, "sendSpecifiedTokens": { "message": "$1 भेजें", "description": "Symbol of the specified token" @@ -1627,24 +1600,6 @@ "swapHighSlippageWarning": { "message": "स्लिपेज राशि बहुत अधिक है। सुनिश्चित करें कि आप जानते हैं कि आप क्या कर रहे हैं!" }, - "swapIntroLearnMoreHeader": { - "message": "अधिक सीखना चाहते हैं?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask स्वैप के बारे में अधिक जानें" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "चलनिधि स्रोतों में निम्न शामिल हैं:" - }, - "swapIntroPopupSubTitle": { - "message": "अब आप अपने MetaMask वॉलेट में सीधे टोकन स्वैप कर सकते हैं। MetaMask स्वैप कई विकेंद्रीकृत विनिमय एग्रीगेटर, पेशेवर बाजार निर्माताओं और व्यक्तिगत DEX को जोड़ता है, ताकि MetaMask उपयोगकर्ताओं को हमेशा सबसे कम नेटवर्क शुल्क के साथ सबसे अच्छा मूल्य मिल सके।" - }, - "swapIntroPopupTitle": { - "message": "टोकन स्वैपिंग यहाँ उपलब्ध है!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "हमारे आधिकारिक अनुबंधों के ऑडिट की समीक्षा करें" - }, "swapLowSlippageError": { "message": "लेनदेन विफल हो सकता है, अधिकतम स्लिपेज बहुत कम हो सकता है।" }, @@ -1666,7 +1621,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "नेटवर्क शुल्क आपके स्वैप को संसाधित करने के शुल्क और उसे Ethereum नेटवर्क पर संग्रह करने को कवर करता है। MetaMask इस शुल्क से लाभ नहीं कमाता है।" + "message": "नेटवर्क शुल्क आपके स्वैप को संसाधित करने के शुल्क और उसे $1 नेटवर्क पर संग्रह करने को कवर करता है। MetaMask इस शुल्क से लाभ नहीं कमाता है।" }, "swapNewQuoteIn": { "message": "$1 में नए उद्धरण", @@ -1747,9 +1702,6 @@ "swapSourceInfo": { "message": "हम सर्वोत्तम दरों और न्यूनतम नेटवर्क शुल्क का पता लगाने के लिए कई चलनिधि स्रोतों (एक्सचेंज, एग्रीगेटर और पेशेवर बाजार निर्माताओं) की खोज करते हैं।" }, - "swapStartSwapping": { - "message": "स्वैप करना शुरू करें" - }, "swapSwapFrom": { "message": "इससे स्वैप करें" }, @@ -1879,9 +1831,6 @@ "tokenContractAddress": { "message": "टोकन अनुबंध पता" }, - "tokenOptions": { - "message": "टोकन के विकल्प" - }, "tokenSymbol": { "message": "टोकन का प्रतीक" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 0dac2b285..f440c3ff9 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -172,9 +172,6 @@ "message": "आयातित", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "जानकारी और सहायता" - }, "insufficientFunds": { "message": "अपर्याप्त धन" }, @@ -303,9 +300,6 @@ "resetAccount": { "message": "खाता रीसेट करें" }, - "restoreFromSeed": { - "message": "बीज वाक्यांश से पुनर्स्थापित करें" - }, "revealSeedWords": { "message": "बीज शब्द प्रकट करें" }, @@ -324,9 +318,6 @@ "search": { "message": "खोज करें" }, - "secretPhrase": { - "message": "अपनी गुप्त बारह शब्द वाक्यांश यहाँ अपनी तिजोरी को पुनर्स्थापित करने के लिए दर्ज करें।" - }, "seedPhraseReq": { "message": "बीज वाक्यांश 12 शब्द लंबा हैं" }, @@ -336,9 +327,6 @@ "send": { "message": "भेजें" }, - "sendETH": { - "message": "भेजें ETH" - }, "sendTokens": { "message": "भेजें टोकन" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 17c35b6b7..aacdd52d9 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "Uvezi račun početnom rečenicom" }, - "importUsingSeed": { - "message": "Uvezi početnom rečenicom za račun" - }, "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" }, - "infoHelp": { - "message": "Informacije i pomoć" - }, "initialTransactionConfirmed": { "message": "Vašu je početnu transakciju potvrdila mreža. Kliknite na U REDU za natrag." }, @@ -608,12 +599,6 @@ "myAccounts": { "message": "Moji računi" }, - "myWalletAccounts": { - "message": "Moji računi za novčanik" - }, - "myWalletAccountsDescription": { - "message": "Svi se vaši računi za MetaMask automatski dodaju u ovaj odjeljak." - }, "needEtherInWallet": { "message": "Trebate Ether u svojem novčaniku kako biste ostvarili interakciju s decentraliziranim aplikacijama uporabom usluge MetaMask. " }, @@ -831,9 +816,6 @@ "restoreAccountWithSeed": { "message": "Obnovite svoj račun početnom rečenicom" }, - "restoreFromSeed": { - "message": "Obnoviti račun?" - }, "revealSeedWords": { "message": "Otkrij početne riječi" }, @@ -888,9 +870,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 +909,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..327c534b9 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -282,16 +282,10 @@ "importAccountMsg": { "message": " Kont pòte pa pral asosye avèk orijinal ou te kreye nan kont MetaMask seed fraz. Aprann plis sou kont enpòte " }, - "importUsingSeed": { - "message": "Pòte lè sèvi avèk seed fraz" - }, "imported": { "message": "Pòte", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Enfo & Èd" - }, "initialTransactionConfirmed": { "message": "Premye tranzaksyon ou konfime sou rezo a. Klike sou OK pou tounen." }, @@ -516,9 +510,6 @@ "restoreAccountWithSeed": { "message": "Retabli kont ou avèk yo Seed Fraz" }, - "restoreFromSeed": { - "message": "Restore kont?" - }, "revealSeedWords": { "message": "Revele Seed Mo Yo" }, @@ -561,9 +552,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 +573,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..5a6d334e2 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "Fiók importálása seed mondat segítségével" }, - "importUsingSeed": { - "message": "Importálás a fiók seed mondatának használatával" - }, "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" }, - "infoHelp": { - "message": "Információk és súgó" - }, "initialTransactionConfirmed": { "message": "Eredeti tranzakciódat jóváhagyta a hálózat. A visszatéréshez kattints az OK-ra." }, @@ -608,12 +599,6 @@ "myAccounts": { "message": "Fiókjaim" }, - "myWalletAccounts": { - "message": "Saját tárca fiókjai" - }, - "myWalletAccountsDescription": { - "message": "Az összes MetaMask által létrehozott fiókot automatikusan hozzáadja a munkamenethez." - }, "needEtherInWallet": { "message": "Ha a MetaMaskon keresztül szeretne interakcióba lépni decentralizált alkalmazással, ahhoz a tárcájában Ethernek kell lennie." }, @@ -831,9 +816,6 @@ "restoreAccountWithSeed": { "message": "Fiók helyreállítása a seed mondat segítségével" }, - "restoreFromSeed": { - "message": "Visszaállítja a fiókot? " - }, "revealSeedWords": { "message": "Seed szavak megjelenítése" }, @@ -888,9 +870,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 +909,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..299c590c0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "log aktivitas" }, - "addAccount": { - "message": "Tambahkan akun" - }, "addAcquiredTokens": { "message": "Tambahkan token yang Anda peroleh menggunakan MetaMask" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "Impor akun dengan frasa pemulihan" }, - "importUsingSeed": { - "message": "Impor menggunakan frasa pemulihan akun" - }, "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" }, - "infoHelp": { - "message": "Informasi & Bantuan" - }, "initialTransactionConfirmed": { "message": "Transaksi awal Anda dikonfirmasikan oleh jaringan. Klik Oke untuk kembali." }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "Akun Saya" }, - "myWalletAccounts": { - "message": "Akun Dompet Saya" - }, - "myWalletAccountsDescription": { - "message": "Semua akun Anda yang dibuat MetaMask akan secara otomatis ditambahkan ke bagian ini." - }, "needEtherInWallet": { "message": "Untuk berinteraksi dengan aplikasi yang terdesentralisasi menggunakan MetaMask, Anda memerlukan Ether di dompet Anda." }, @@ -1273,9 +1255,6 @@ "restoreAccountWithSeed": { "message": "Memulihkan Akun dengan Frasa Pemulihan" }, - "restoreFromSeed": { - "message": "Pulihkan akun?" - }, "restoreWalletPreferences": { "message": "Cadangan data Anda dari $1 telah ditemukan. Apakah Anda ingin memulihkan preferensi dompet Anda?", "description": "$1 is the date at which the data was backed up" @@ -1346,9 +1325,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 +1376,6 @@ "sendAmount": { "message": "Kirim Jumlah" }, - "sendETH": { - "message": "Kirim ETH" - }, "sendSpecifiedTokens": { "message": "Kirim $1", "description": "Symbol of the specified token" @@ -1627,24 +1600,6 @@ "swapHighSlippageWarning": { "message": "Jumlah slippage sangat tinggi. Pastikan Anda mengetahui yang Anda kerjakan!" }, - "swapIntroLearnMoreHeader": { - "message": "Ingin mempelajari selengkapnya?" - }, - "swapIntroLearnMoreLink": { - "message": "Pelajari selengkapnya tentang Penukaran MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Sumber likuiditas mencakup:" - }, - "swapIntroPopupSubTitle": { - "message": "Sekarang, Anda bisa menukar token secara langsung di dompet MetaMask Anda. Penukaran MetaMask menggabungkan beberapa agregator penukaran terdesentralisasi, pembuat pasar profesional, dan DEX individu untuk memastikan pengguna MetaMask selalu mendapatkan harga terbaik dengan biaya jaringan terendah." - }, - "swapIntroPopupTitle": { - "message": "Penukaran token ada di sini!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Tinjau audit kontrak resmi kami" - }, "swapLowSlippageError": { "message": "Transaksi bisa gagal, slippage maks. terlalu rendah." }, @@ -1666,7 +1621,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "Biaya jaringan mencakup biaya pemrosesan penukaran Anda dan menyimpannya di jaringan Ethereum. MetaMask tidak mendapatkan keuntungan dari biaya ini." + "message": "Biaya jaringan mencakup biaya pemrosesan penukaran Anda dan menyimpannya di jaringan $1. MetaMask tidak mendapatkan keuntungan dari biaya ini." }, "swapNewQuoteIn": { "message": "Kuota baru di $1", @@ -1747,9 +1702,6 @@ "swapSourceInfo": { "message": "Kami mencari beberapa sumber likuiditas (penukaran, agregator, dan pembuat pasar profesional) untuk menemukan tarif terbaik dan biaya jaringan terendah." }, - "swapStartSwapping": { - "message": "Mulai menukar" - }, "swapSwapFrom": { "message": "Tukar dari" }, @@ -1879,9 +1831,6 @@ "tokenContractAddress": { "message": "Alamat Kontrak Token" }, - "tokenOptions": { - "message": "Opsi token" - }, "tokenSymbol": { "message": "Simbol Token" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 6f591ddd9..610583c2b 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "log attività" }, - "addAccount": { - "message": "Aggiungi un account" - }, "addAcquiredTokens": { "message": "Aggiungi i token che hai acquistato usando MetaMask" }, @@ -793,22 +790,13 @@ "importAccountSeedPhrase": { "message": "Importa un Account con una Frase Seed" }, - "importUsingSeed": { - "message": "Importa account con frase seed" - }, "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" }, - "infoHelp": { - "message": "Informazioni & Aiuto" - }, "initialTransactionConfirmed": { "message": "La transazione iniziale è stata confermata dalla rete. Clicca OK per tornare indietro." }, @@ -999,12 +987,6 @@ "myAccounts": { "message": "Miei Account" }, - "myWalletAccounts": { - "message": "I miei account di portafoglio" - }, - "myWalletAccountsDescription": { - "message": "Tutti gli account creati con MetaMask saranno automaticamente aggiunti a questa sezione." - }, "needEtherInWallet": { "message": "Per interagire con applicazioni decentralizzate con MetaMask, devi possedere Ether nel tuo portafoglio." }, @@ -1288,9 +1270,6 @@ "restoreAccountWithSeed": { "message": "Ripristina Account con la Frase Seed" }, - "restoreFromSeed": { - "message": "Ripristina da una frase seed" - }, "restoreWalletPreferences": { "message": "È stato trovato un backup dei tuoi dati da $1. Vuoi ripristinare le preferenze del portafoglio?", "description": "$1 is the date at which the data was backed up" @@ -1361,9 +1340,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 +1391,6 @@ "sendAmount": { "message": "Invia Importo" }, - "sendETH": { - "message": "Invia ETH" - }, "sendSpecifiedTokens": { "message": "Invia $1", "description": "Symbol of the specified token" @@ -1645,24 +1618,6 @@ "swapHighSlippageWarning": { "message": "L'importo di slippage è molto alto. Assicurati di sapere cosa stai facendo!" }, - "swapIntroLearnMoreHeader": { - "message": "Vuoi sapere di più?" - }, - "swapIntroLearnMoreLink": { - "message": "Scopri di più su MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Sorgenti di liquidità incluse:" - }, - "swapIntroPopupSubTitle": { - "message": "Adesso puoi scambiare token direttamente dal tuo portafgolio MetaMask. MetaMask Swaps combina vari siti di scambio decentralizzati, aggregatori e market maker professionisti per assicurare che gli utenti di MetaMask ottengano sempre il miglior prezzo con le tasse di rete minori." - }, - "swapIntroPopupTitle": { - "message": "Lo scambio di token è qui!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Esamina l'audit ufficiale dei nostri smart contracts" - }, "swapLowSlippageError": { "message": "La transazione può fallire, il massimo slippage è troppo basso." }, @@ -1688,7 +1643,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "La tassa di rete copre il costo di processamento dello scambio e della memorizzazione nella rete Ethereum. MetaMask non trae profitto da questa tassa." + "message": "La tassa di rete copre il costo di processamento dello scambio e della memorizzazione nella rete $1. MetaMask non trae profitto da questa tassa." }, "swapNewQuoteIn": { "message": "Nuove quotazioni in $1", @@ -1789,9 +1744,6 @@ "swapSourceInfo": { "message": "Cerchiamo sorgenti di liquidità multiple (siti di scambio, aggregatori, market maker professionisti) per trovare le tariffe migliori e le tasse di rete minori." }, - "swapStartSwapping": { - "message": "Inizia a scambiare" - }, "swapSwapFrom": { "message": "Scambia da" }, @@ -1938,9 +1890,6 @@ "tokenContractAddress": { "message": "Indirizzo Contratto Token" }, - "tokenOptions": { - "message": "Opzioni token" - }, "tokenSymbol": { "message": "Simbolo Token" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index a29480b24..253588c73 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "アクティビティのログ" }, - "addAccount": { - "message": "アカウントの追加" - }, "addAcquiredTokens": { "message": "MetaMaskで取得したトークンを追加する" }, @@ -790,22 +787,13 @@ "importAccountSeedPhrase": { "message": "シードフレーズを使用してアカウントをインポート" }, - "importUsingSeed": { - "message": "アカウントのシードフレーズから復元する" - }, "importWallet": { "message": "ウォレットのインポート" }, - "importYourExisting": { - "message": "12単語のシードフレーズを使用して既存のウォレットをインポートします" - }, "imported": { "message": "インポート済", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "情報とヘルプ" - }, "initialTransactionConfirmed": { "message": "最初のトランザクションがネットワークに確認されました。戻るにはOKをクリックします。" }, @@ -996,12 +984,6 @@ "myAccounts": { "message": "マイアカウント" }, - "myWalletAccounts": { - "message": "このウォレットのアカウント" - }, - "myWalletAccountsDescription": { - "message": "MetaMaskで作成したすべてのアカウントは、このセクションに自動的に追加されます。" - }, "needEtherInWallet": { "message": "MetaMaskで分散型アプリケーションを操作するには、ウォレットにEtherが必要です。" }, @@ -1282,9 +1264,6 @@ "restoreAccountWithSeed": { "message": "シードフレーズでアカウントを復元" }, - "restoreFromSeed": { - "message": "アカウントを復元しますか?" - }, "restoreWalletPreferences": { "message": "$1 のデータバックアップが見つかりました。ウォレットの基本設定を復元しますか?", "description": "$1 is the date at which the data was backed up" @@ -1355,9 +1334,6 @@ "secretBackupPhraseWarning": { "message": "警告:シードフレーズは絶対に公開しないでください。シードフレーズを使うと、誰でもアカウントからETHを盗み出せます。" }, - "secretPhrase": { - "message": "アカウント情報を復元するには、12単語で構成されたシードフレーズを入力してください。" - }, "securityAndPrivacy": { "message": "セキュリティとプライバシー" }, @@ -1409,9 +1385,6 @@ "sendAmount": { "message": "送金額" }, - "sendETH": { - "message": "ETHの送金" - }, "sendSpecifiedTokens": { "message": "$1 を送る", "description": "Symbol of the specified token" @@ -1639,24 +1612,6 @@ "swapHighSlippageWarning": { "message": "非常に大きいスリッページ額です。本当に実行するか確認してください。" }, - "swapIntroLearnMoreHeader": { - "message": "詳細を表示しますか?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask Swapsの詳細" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "流動性ソースには以下が含まれます。" - }, - "swapIntroPopupSubTitle": { - "message": "トークンをMetaMaskで直接スワップできるようになりました。MetaMask Swapsは、多数の分散型取引所アグリゲーター、専門のマーケットメーカー、DEX取引所を統合し、ユーザーは常に最低のネットワーク手数料、最適な価格で取引できます。" - }, - "swapIntroPopupTitle": { - "message": "トークンのスワップはこちら!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "MetaSwapのコントラクト監査のレビュー" - }, "swapLowSlippageError": { "message": "トランザクションが失敗する可能性があります。最大スリッページが少なすぎます。" }, @@ -1682,7 +1637,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "ネットワーク手数料には、スワップの結果をEthereumネットワークに保管する費用も含まれています。MetaMaskは手数料から利益を得ません。" + "message": "ネットワーク手数料には、スワップの結果を$1ネットワークに保管する費用も含まれています。MetaMaskは手数料から利益を得ません。" }, "swapNewQuoteIn": { "message": "見積の有効期限 $1", @@ -1777,9 +1732,6 @@ "swapSourceInfo": { "message": "最良のレートと最小のネットワーク手数料を探すため、複数の流動性ソース(取引所、アグリゲーター、専門のマーケットメーカー)を検索します。" }, - "swapStartSwapping": { - "message": "スワップの開始" - }, "swapSwapFrom": { "message": "スワップ元" }, @@ -1915,9 +1867,6 @@ "tokenContractAddress": { "message": "トークンコントラクトのアドレス" }, - "tokenOptions": { - "message": "トークンのオプション" - }, "tokenSymbol": { "message": "トークンシンボル" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index e07994172..1dba0cbc9 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "ಸೀಡ್‌ ಫ್ರೇಸ್‌ನೊಂದಿಗೆ ಖಾತೆಯನ್ನು ಆಮದು ಮಾಡಿ" }, - "importUsingSeed": { - "message": "ಸೀಡ್‌ ಫ್ರೇಸ್‌ ಖಾತೆಯನ್ನು ಬಳಸಿಕೊಂಡು ಆಮದು ಮಾಡಿ" - }, "importWallet": { "message": "ವ್ಯಾಲೆಟ್ ಅನ್ನು ಆಮದು ಮಾಡಿ" }, - "importYourExisting": { - "message": "12 ಪದದ ಸೀಡ್ ಫ್ರೇಸ್ ಅನ್ನು ಬಳಸಿಕೊಂಡು ನಿಮ್ಮ ಅಸ್ತಿತ್ವದಲ್ಲಿರುವ ವ್ಯಾಲೆಟ್ ಅನ್ನು ಆಮದು ಮಾಡಿ" - }, "imported": { "message": "ಆಮದುಮಾಡಲಾಗಿದೆ", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "ಮಾಹಿತಿ & ಸಹಾಯ" - }, "initialTransactionConfirmed": { "message": "ನಿಮ್ಮ ಆರಂಭಿಕ ವಹಿವಾಟನ್ನು ನೆಟ್‌ವರ್ಕ್ ಮೂಲಕ ಖಚಿತಪಡಿಸಲಾಗಿದೆ. ಹಿಂತಿರುಗಲು ಸರಿ ಕ್ಲಿಕ್ ಮಾಡಿ." }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "ನನ್ನ ಖಾತೆಗಳು" }, - "myWalletAccounts": { - "message": "ನನ್ನ ವ್ಯಾಲೆಟ್ ಖಾತೆಗಳು" - }, - "myWalletAccountsDescription": { - "message": "ನಿಮ್ಮ MetaMask ರಚಿಸಿದ ಎಲ್ಲಾ ಖಾತೆಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಈ ವಿಭಾಗಕ್ಕೆ ಸೇರಿಸಲಾಗುತ್ತದೆ." - }, "needEtherInWallet": { "message": "MetaMask ಬಳಸಿಕೊಂಡು ವಿಕೇಂದ್ರೀಕೃತ ಖಾತೆಗಳೊಂದಿಗೆ ಸಂವಹನ ನಡೆಸಲು, ನಿಮ್ಮ ವ್ಯಾಲೆಟ್‌ನಲ್ಲಿ ನಿಮಗೆ ಎಥರ್ ಅಗತ್ಯವಿದೆ." }, @@ -838,9 +823,6 @@ "restoreAccountWithSeed": { "message": "ಸೀಡ್ ಫ್ರೇಸ್‌ನೊಂದಿಗೆ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಮರುಸ್ಥಾಪಿಸಿ" }, - "restoreFromSeed": { - "message": "ಖಾತೆಯನ್ನು ಮರುಸ್ಥಾಪಿಸುವುದೇ?" - }, "revealSeedWords": { "message": "ಸೀಡ್ ವರ್ಡ್ಸ್ ಬಹಿರಂಗಪಡಿಸಿ" }, @@ -895,9 +877,6 @@ "secretBackupPhraseWarning": { "message": "ಎಚ್ಚರಿಕೆ: ನಿಮ್ಮ ಬ್ಯಾಕಪ್ ಫ್ರೇಸ್ ಅನ್ನು ಎಂದಿಗೂ ಬಹಿರಗಪಡಿಸಬೇಡಿ. ಈ ಫ್ರೇಸ್ ಅನ್ನು ಹೊಂದಿರುವ ಯಾರಾದರೂ ನಿಮ್ಮ ಎಥರ್ ಅನ್ನು ಶಾಶ್ವತವಾಗಿ ತೆಗೆದುಕೊಳ್ಳಬಹುದು." }, - "secretPhrase": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಮರುಸ್ಥಾಪಿಸಲು ನಿಮ್ಮ ರಹಸ್ಯ ಹನ್ನೆರಡು ಪದದ ಫ್ರೇಸ್ ಅನ್ನು ಇಲ್ಲಿ ನಮೂದಿಸಿ." - }, "securityAndPrivacy": { "message": "ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆ" }, @@ -937,9 +916,6 @@ "sendAmount": { "message": "ಮೊತ್ತವನ್ನು ಕಳುಹಿಸಿ" }, - "sendETH": { - "message": "ETH ಕಳುಹಿಸಿ" - }, "sendTokens": { "message": "ಟೋಕನ್‌ಗಳನ್ನು ಕಳುಹಿಸಿ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 59d1fcc42..055ccc767 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "활동 로그" }, - "addAccount": { - "message": "계정 추가" - }, "addAcquiredTokens": { "message": "MetaMask를 이용해 얻은 토큰 추가" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "시드 구문으로 계정 가져오기" }, - "importUsingSeed": { - "message": "계정 시드 구문으로 가져오기" - }, "importWallet": { "message": "지갑 가져오기" }, - "importYourExisting": { - "message": "12단어 시드 구문을 사용하여 지갑 가져오기" - }, "imported": { "message": "가져옴", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "정보 및 도움말" - }, "initialTransactionConfirmed": { "message": "최초 거래를 네트워크에서 확인했습니다. 확인을 클릭하여 뒤로 돌아가세요." }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "내 계정" }, - "myWalletAccounts": { - "message": "내 지갑 계정" - }, - "myWalletAccountsDescription": { - "message": "MetaMask에서 생성한 모든 계정으로 이 섹션에 자동으로 추가됩니다." - }, "needEtherInWallet": { "message": "MetaMask를 이용하는 분산형 애플리케이션과 상호작용하려면 지갑에 Ether가 있어야 합니다." }, @@ -1003,6 +985,9 @@ "networkName": { "message": "네트워크 이름" }, + "networkNameEthereum": { + "message": "이더리움" + }, "networkSettingsChainIdDescription": { "message": "체인 ID는 거래 서명에 사용합니다. 네트워크에서 반환하는 체인 ID와 일치해야 합니다. 십진수나 '0x'로 시작하는 16진수를 입력할 수 있지만, 표시될 때는 십진수로 표시됩니다." }, @@ -1270,9 +1255,6 @@ "restoreAccountWithSeed": { "message": "시드 구문으로 계정 복구" }, - "restoreFromSeed": { - "message": "계정을 복구하시겠습니까?" - }, "restoreWalletPreferences": { "message": "$1의 데이터 백업이 발견되었습니다. 지갑 환경설정을 복구하시겠습니까?", "description": "$1 is the date at which the data was backed up" @@ -1343,9 +1325,6 @@ "secretBackupPhraseWarning": { "message": "경고: 백업 구문은 절대로 공개하지 마세요. 이 구문이 있는 사람은 귀하의 Ether를 영원히 소유할 수 있습니다." }, - "secretPhrase": { - "message": "금고를 복구하려면 비밀 12단어 구문을 여기에 입력하세요." - }, "securityAndPrivacy": { "message": "보안 및 개인정보 보호" }, @@ -1397,9 +1376,6 @@ "sendAmount": { "message": "금액 보내기" }, - "sendETH": { - "message": "ETH 보내기" - }, "sendSpecifiedTokens": { "message": "$1 보내기", "description": "Symbol of the specified token" @@ -1624,24 +1600,6 @@ "swapHighSlippageWarning": { "message": "슬리패지 금액이 아주 큽니다. 현재 어떤 작업을 하고 있는지 확인하세요!" }, - "swapIntroLearnMoreHeader": { - "message": "자세한 정보를 확인하고 싶으신가요?" - }, - "swapIntroLearnMoreLink": { - "message": "MetaMask Swaps에 대해 자세히 알아보기" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "다음을 포함한 유동성 소스:" - }, - "swapIntroPopupSubTitle": { - "message": "이제 MetaMask 지갑에서 토큰을 바로 스왑할 수 있습니다. MetaMask Swaps는 다양한 분산형 교환 애그리게이터, 투자전문기관, 개별 DEX를 결합하여 MetaMask 사용자가 언제든 최저 네트워크 요금으로 최상의 가격을 얻을 수 있게 합니다." - }, - "swapIntroPopupTitle": { - "message": "토큰 스왑은 여기서 진행됩니다!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "당사의 공식 계약 감사 검토" - }, "swapLowSlippageError": { "message": "거래가 실패할 수도 있습니다. 최대 슬리패지가 너무 낮습니다." }, @@ -1663,7 +1621,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "네트워크 요금에는 스왑을 처리하고 이더리움 네트워크에 보관하는 비용이 적용됩니다. MetaMask는 이 요금을 이용해 이득을 얻지 않습니다." + "message": "네트워크 요금에는 스왑을 처리하고 $1 네트워크에 보관하는 비용이 적용됩니다. MetaMask는 이 요금을 이용해 이득을 얻지 않습니다." }, "swapNewQuoteIn": { "message": "$1 후에 새 견적", @@ -1744,9 +1702,6 @@ "swapSourceInfo": { "message": "당사에서는 여러 유동성 소스(교환, 애그리게이터, 투자전문기관)를 검색하여 최상의 요율과 최저 네트워크 요금을 찾습니다." }, - "swapStartSwapping": { - "message": "스왑 시작" - }, "swapSwapFrom": { "message": "다음에서 스왑" }, @@ -1876,9 +1831,6 @@ "tokenContractAddress": { "message": "토큰 계약 주소" }, - "tokenOptions": { - "message": "토큰 옵션" - }, "tokenSymbol": { "message": "토큰 기호" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index ec9137030..e4ca2146c 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "Importuoti paskyrą su atkūrimo fraze" }, - "importUsingSeed": { - "message": "Importuoti naudojant paskyros atkūrimo frazę" - }, "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" }, - "infoHelp": { - "message": "Informacija ir žinynas" - }, "initialTransactionConfirmed": { "message": "Jūsų pradinė operacija patvirtinta tinkle. Norėdami grįžti, spustelėkite „Gerai“." }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "Mano paskyros" }, - "myWalletAccounts": { - "message": "Mano piniginės paskyros" - }, - "myWalletAccountsDescription": { - "message": "Visos jūsų „MetaMask“ sukurtos paskyros bus automatiškai įtrauktos į šį skyrių." - }, "needEtherInWallet": { "message": "Norėdami dirbti su decentralizuotomis programomis „MetaMask“, savo piniginėje turite turėti eterių." }, @@ -838,9 +823,6 @@ "restoreAccountWithSeed": { "message": "Atkurti paskyrą naudojant atkūrimo frazę" }, - "restoreFromSeed": { - "message": "Atkurti paskyrą?" - }, "revealSeedWords": { "message": "Atskleisti atkūrimo žodžius" }, @@ -895,9 +877,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 +916,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..2a6e1dd8d 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -495,22 +495,13 @@ "importAccountSeedPhrase": { "message": "Importēt kontu ar atkopšanas frāzi" }, - "importUsingSeed": { - "message": "Importēt, izmantojot konta atkopšanas frāzi" - }, "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" }, - "infoHelp": { - "message": "Informācija un palīdzība" - }, "initialTransactionConfirmed": { "message": "Jūsu sākotnējais darījums apstiprināts tīklā. Spiediet OK, lai atgrieztos." }, @@ -611,12 +602,6 @@ "myAccounts": { "message": "Mani konti" }, - "myWalletAccounts": { - "message": "Mana maka konti" - }, - "myWalletAccountsDescription": { - "message": "Visi jūsu izveidotie MetaMask konti tiks automātiski pievienoti šajā sadaļā." - }, "needEtherInWallet": { "message": "Lai izmantotu decentralizētas lietotnes ar MetaMask, jūsu makā jābūt Ether." }, @@ -834,9 +819,6 @@ "restoreAccountWithSeed": { "message": "Atjaunojiet savu kontu ar atkopšanas frāzi" }, - "restoreFromSeed": { - "message": "Atjaunot kontu?" - }, "revealSeedWords": { "message": "Parādīt atkopšanas vārdus" }, @@ -891,9 +873,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 +912,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..017b30211 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -485,22 +485,13 @@ "importAccountSeedPhrase": { "message": "Import Akaun dengan Frasa Benih" }, - "importUsingSeed": { - "message": "Import menggunakan frasa benih akaun" - }, "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" }, - "infoHelp": { - "message": "Info & Bantuan" - }, "initialTransactionConfirmed": { "message": "Transaksi awal anda telah disahkan oleh rangkaian. Klik OK untuk kembali." }, @@ -595,12 +586,6 @@ "myAccounts": { "message": "Akaun Saya" }, - "myWalletAccounts": { - "message": "Akaun Dompet Saya" - }, - "myWalletAccountsDescription": { - "message": "Semua akaun ciptaan MetaMask anda akan ditambahkan secara automatik kepada bahagian ini." - }, "needEtherInWallet": { "message": "Untuk berinteraksi dengan aplikasi ternyahpusat menggunakan MetaMask, anda memerlukan Ether di dalam dompet anda." }, @@ -815,9 +800,6 @@ "restoreAccountWithSeed": { "message": "Pulihkan Akaun anda dengan Ungkapan Benih" }, - "restoreFromSeed": { - "message": "Pulihkan akaun?" - }, "revealSeedWords": { "message": "Dedahkan Ungkapan Benih" }, @@ -872,9 +854,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 +893,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..2366f8283 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -166,9 +166,6 @@ "message": "geïmporteerd", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Info en hulp" - }, "insufficientFunds": { "message": "Onvoldoende fondsen." }, @@ -290,9 +287,6 @@ "resetAccount": { "message": "Account opnieuw instellen" }, - "restoreFromSeed": { - "message": "Herstel vanuit back-up woorden" - }, "revealSeedWords": { "message": "Onthul back-up woorden" }, @@ -311,9 +305,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 +314,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..93055032a 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -486,22 +486,13 @@ "importAccountSeedPhrase": { "message": "Importer en konto med den mnemoniske gjenopprettingsfrasen " }, - "importUsingSeed": { - "message": "Importer ved hjelp av kontoens mnemoniske gjenopprettingsfrase" - }, "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" }, - "infoHelp": { - "message": "Informasjon og hjelp" - }, "initialTransactionConfirmed": { "message": "Din opprinnelige transaksjon ble bekreftet av nettverket. Klikk OK for å gå tilbake." }, @@ -602,12 +593,6 @@ "myAccounts": { "message": "Mine kontoer " }, - "myWalletAccounts": { - "message": "mine lommebokkontoer" - }, - "myWalletAccountsDescription": { - "message": "Alle dine opprettede MetaMask-kontoer vil automatisk bli lagt til denne seksjonen. " - }, "needEtherInWallet": { "message": "Du må ha Ether i lommeboken din for å samhandle med desentraliserte applikasjoner gjennom MateMask." }, @@ -825,9 +810,6 @@ "restoreAccountWithSeed": { "message": "Gjenopprett konto med frøfrase" }, - "restoreFromSeed": { - "message": "Gjennopprett konto? " - }, "revealSeedWords": { "message": "Vis frøord" }, @@ -879,9 +861,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/ph/messages.json b/app/_locales/ph/messages.json index ad0441e11..f15348820 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -136,9 +136,6 @@ "message": "Na-import na", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Impormasyon at Tulong" - }, "invalidAddress": { "message": "Invalid ang address" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 4a4c717a8..6e6f4fbe7 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "Zaimportuj konto za pomocą frazy seed" }, - "importUsingSeed": { - "message": "Importuj przy pomocy frazy seed konta" - }, "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" }, - "infoHelp": { - "message": "Info & pomoc" - }, "initialTransactionConfirmed": { "message": "Twoja transakcja została potwierdzona w sieci. Kliknij OK żeby wrócić." }, @@ -612,12 +603,6 @@ "myAccounts": { "message": "Moje konta" }, - "myWalletAccounts": { - "message": "Moja konta portfeli" - }, - "myWalletAccountsDescription": { - "message": "Wszystkie Twoje konta utworzone w MetaMask zostaną automatycznie dodane do tej sekcji." - }, "needEtherInWallet": { "message": "Żeby skorzystać ze zdecentraliowanych aplikacji (dApps) przy pomocy MetaMask, potrzebujesz Eteru w swoim portfelu." }, @@ -832,9 +817,6 @@ "restoreAccountWithSeed": { "message": "Przywróć konto frazą seed" }, - "restoreFromSeed": { - "message": "Przywrócić konto?" - }, "revealSeedWords": { "message": "Pokaż słowa seed" }, @@ -889,9 +871,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 +910,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..c9f598cba 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -172,9 +172,6 @@ "message": "Importado", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Informação & Ajuda" - }, "insufficientFunds": { "message": "Fundos insuficientes." }, @@ -300,9 +297,6 @@ "resetAccount": { "message": "Reinicializar Conta" }, - "restoreFromSeed": { - "message": "Restaurar a partir da frase seed" - }, "revealSeedWords": { "message": "Revelar Palavras Seed" }, @@ -321,9 +315,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 +324,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..fe3bb1fa2 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -493,22 +493,13 @@ "importAccountSeedPhrase": { "message": "Importar uma Conta com Frase-semente" }, - "importUsingSeed": { - "message": "Importe usando a seed phrase da conta" - }, "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" }, - "infoHelp": { - "message": "Informações e ajuda" - }, "initialTransactionConfirmed": { "message": "Sua transação inicial foi confirmada pela rede. Clique em OK para voltar." }, @@ -603,12 +594,6 @@ "myAccounts": { "message": "Minhas Contas" }, - "myWalletAccounts": { - "message": "Contas da Minha Carteira" - }, - "myWalletAccountsDescription": { - "message": "Todas as suas contas criadas no MetaMask serão automaticamente adicionadas a esta seção." - }, "needEtherInWallet": { "message": "Para interagir com aplicações descentralizadas usando o MetaMask, você precisará de Ether na sua carteira." }, @@ -826,9 +811,6 @@ "restoreAccountWithSeed": { "message": "Restaurar sua conta com a frase semente" }, - "restoreFromSeed": { - "message": "Restaurar conta?" - }, "revealSeedWords": { "message": "Revelar Palavras-semente" }, @@ -883,9 +865,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 +904,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..f0e938835 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -489,22 +489,13 @@ "importAccountSeedPhrase": { "message": "Importați un cont cu fraza seed" }, - "importUsingSeed": { - "message": "Importare folosind fraza inițială a contului" - }, "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" }, - "infoHelp": { - "message": "Informații și ajutor" - }, "initialTransactionConfirmed": { "message": "Tranzacția inițială a fost confirmată de rețea. Clic pe OK pentru a reveni." }, @@ -602,12 +593,6 @@ "myAccounts": { "message": "Conturile mele" }, - "myWalletAccounts": { - "message": "Conturi My Wallet" - }, - "myWalletAccountsDescription": { - "message": "Toate conturile dvs. create pe MetaMask vor fi adăugate automat la această secțiune." - }, "needEtherInWallet": { "message": "Pentru a interacționa cu aplicațiile descentralizate prin intermediul MetaMask, trebuie să aveți Ether în portofel." }, @@ -825,9 +810,6 @@ "restoreAccountWithSeed": { "message": "Restaurați-vă contul folosind fraza inițială" }, - "restoreFromSeed": { - "message": "Restabiliți contul?" - }, "revealSeedWords": { "message": "Arată cuvintele din seed" }, @@ -882,9 +864,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 +903,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..5acbe608f 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "Журнал активности" }, - "addAccount": { - "message": "Добавить счет" - }, "addAcquiredTokens": { "message": "Добавьте токены, которые вы приобрели с помощью MetaMask" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "Импортировать счет с исходной фразой" }, - "importUsingSeed": { - "message": "Импортировать с использованием исходной фразы счета" - }, "importWallet": { "message": "Импортировать кошелек" }, - "importYourExisting": { - "message": "Импортируйте существующий кошелек, используя начальную фразу из 12 слов" - }, "imported": { "message": "Импортированный", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Информация и справка" - }, "initialTransactionConfirmed": { "message": "Ваша первоначальная транзакция была подтверждена сетью. Нажмите ОК, чтобы вернуться." }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "Мои счета" }, - "myWalletAccounts": { - "message": "Счета моего кошелька" - }, - "myWalletAccountsDescription": { - "message": "Все ваши счета, созданные в MetaMask, будут автоматически добавлены в этот раздел." - }, "needEtherInWallet": { "message": "Для взаимодействия с децентрализованными приложениями с помощью MetaMask вам понадобится Ether в вашем кошельке." }, @@ -1273,9 +1255,6 @@ "restoreAccountWithSeed": { "message": "Восстановите свой счет с помощью исходной фразы" }, - "restoreFromSeed": { - "message": "Восстановить счет?" - }, "restoreWalletPreferences": { "message": "Найдена резервная копия ваших данных из $1. Хотите восстановить настройки кошелька?", "description": "$1 is the date at which the data was backed up" @@ -1346,9 +1325,6 @@ "secretBackupPhraseWarning": { "message": "ПРЕДУПРЕЖДЕНИЕ: Никогда не разглашайте резервную фразу. Любой, у кого есть эта фраза, может забрать ваш Ether навсегда." }, - "secretPhrase": { - "message": "Введите здесь свою секретную фразу из двенадцати слов, чтобы восстановить свой сейф." - }, "securityAndPrivacy": { "message": "Безопасность и конфиденциальность" }, @@ -1400,9 +1376,6 @@ "sendAmount": { "message": "Отправить сумму" }, - "sendETH": { - "message": "Отправить ETH" - }, "sendSpecifiedTokens": { "message": "Отправить $1", "description": "Symbol of the specified token" @@ -1627,24 +1600,6 @@ "swapHighSlippageWarning": { "message": "Величина проскальзывания очень велика. Убедитесь, что вы знаете, что делаете!" }, - "swapIntroLearnMoreHeader": { - "message": "Хотите узнать больше?" - }, - "swapIntroLearnMoreLink": { - "message": "Подробнее о свопах MetaMask" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Источники ликвидности включают:" - }, - "swapIntroPopupSubTitle": { - "message": "Теперь вы можете обменивать токены прямо в кошельке MetaMask. MetaMask Swaps объединяет несколько децентрализованных агрегаторов обменов, профессиональных торговцев и отдельные DEX, чтобы пользователи MetaMask всегда получали лучшую цену с минимальными комиссиями сети." - }, - "swapIntroPopupTitle": { - "message": "Обмен токенов здесь!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Ознакомьтесь с нашим официальным аудитом контрактов" - }, "swapLowSlippageError": { "message": "Транзакции могут завершиться неудачей, максимальное проскальзывание слишком мало." }, @@ -1666,7 +1621,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "Сетевая комиссия покрывает стоимость обработки вашего свопа и его хранения в сети Ethereum. MetaMask не получает прибыли от этой комиссии." + "message": "Сетевая комиссия покрывает стоимость обработки вашего свопа и его хранения в сети $1. MetaMask не получает прибыли от этой комиссии." }, "swapNewQuoteIn": { "message": "Новые котировки в $1", @@ -1747,9 +1702,6 @@ "swapSourceInfo": { "message": "Мы ищем несколько источников ликвидности (биржи, агрегаторы и профессиональные продавцы), чтобы найти лучшие цены и самые низкие сетевые комиссии." }, - "swapStartSwapping": { - "message": "Начать обмен" - }, "swapSwapFrom": { "message": "Своп с" }, @@ -1879,9 +1831,6 @@ "tokenContractAddress": { "message": "Адрес контракта токена" }, - "tokenOptions": { - "message": "Опции токена" - }, "tokenSymbol": { "message": "Символ токена" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 3124751fb..7f2e6718d 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -486,22 +486,13 @@ "importAccountSeedPhrase": { "message": "Importovať účet so seed frázou" }, - "importUsingSeed": { - "message": "Importovať pomocou seed frázy účtu" - }, "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" }, - "infoHelp": { - "message": "Informace a nápověda" - }, "initialTransactionConfirmed": { "message": "Sieť potvrdila vašu iniciálnu transakciu. Ak sa chcete vrátiť späť, kliknite na OK." }, @@ -587,12 +578,6 @@ "myAccounts": { "message": "Moje účty" }, - "myWalletAccounts": { - "message": "Účty v Mojej peňaženke" - }, - "myWalletAccountsDescription": { - "message": "Do tejto sekcie sa automaticky pridajú všetky vaše účty vytvorené pomocou MetaMask." - }, "needEtherInWallet": { "message": "Potřebujete Ether v peněžence, abyste mohli pomocí MetaMasku interagovat s decentralizovanými aplikacemi." }, @@ -807,9 +792,6 @@ "restoreAccountWithSeed": { "message": "Obnoviť účet pomocou seed frázy" }, - "restoreFromSeed": { - "message": "Obnovit z seed fráze" - }, "revealSeedWords": { "message": "Zobrazit slova klíčové fráze" }, @@ -858,9 +840,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 +879,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..ed36f5ff9 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -490,22 +490,13 @@ "importAccountSeedPhrase": { "message": "Uvoz računa z geslom seed phrase" }, - "importUsingSeed": { - "message": "Uvozi z seed phase" - }, "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" }, - "infoHelp": { - "message": "Info & Pomoč" - }, "initialTransactionConfirmed": { "message": "Vaša prvotna transakcija je bila potrjena. Pritisnite V redu in se vrnite nazaj." }, @@ -600,12 +591,6 @@ "myAccounts": { "message": "Moji računi" }, - "myWalletAccounts": { - "message": "Moji računi Denarnice" - }, - "myWalletAccountsDescription": { - "message": "Vsi vaši računi, ustvarjeni v MetaMasku, bodo samodejno dodani v ta razdelek." - }, "needEtherInWallet": { "message": "Za interakcijo z decentraliziranimi aplikacijami boste v svoji denarnici potrebovali Eter." }, @@ -823,9 +808,6 @@ "restoreAccountWithSeed": { "message": "Obnovi račun z seed phrase" }, - "restoreFromSeed": { - "message": "Obnovim račun?" - }, "revealSeedWords": { "message": "Razkrij seed words" }, @@ -877,9 +859,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 +898,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..086783b38 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -496,22 +496,13 @@ "importAccountSeedPhrase": { "message": "Uvezi račun sa frazom početnih vrednosti" }, - "importUsingSeed": { - "message": "Uvezite koristeći šifru za oporavak naloga (seed phrase)" - }, "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" }, - "infoHelp": { - "message": "Info i pomoć" - }, "initialTransactionConfirmed": { "message": "Vašu početnu transakciju je potvrdila mreža. Kliknite OK da biste se vratili." }, @@ -606,12 +597,6 @@ "myAccounts": { "message": "Moji nalozi" }, - "myWalletAccounts": { - "message": "Moji nalozi novčanika" - }, - "myWalletAccountsDescription": { - "message": "Svi vaši nalozi kreirani u aplikaciji MetaMask biće automatski dodati u ovaj deo." - }, "needEtherInWallet": { "message": "Da biste stupili u interakciju sa decentralizovanim aplikacijama koristeći MetaMask, biće vam potreban Ether u vašem novčaniku." }, @@ -829,9 +814,6 @@ "restoreAccountWithSeed": { "message": "Povratite svoj nalog uz pomoć seed fraze" }, - "restoreFromSeed": { - "message": "Obnoviti nalog?" - }, "revealSeedWords": { "message": "Otkrivanje početnih reči" }, @@ -886,9 +868,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 +907,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..9a8c4fac2 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -489,22 +489,13 @@ "importAccountSeedPhrase": { "message": "Importera konto med" }, - "importUsingSeed": { - "message": "Importera med kontots seedfras" - }, "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" }, - "infoHelp": { - "message": "Info och hjälp" - }, "initialTransactionConfirmed": { "message": "Din initiala transaktion har bekräftats av nätverket. Klicka på OK för att gå tillbaka." }, @@ -599,12 +590,6 @@ "myAccounts": { "message": "Mina konton" }, - "myWalletAccounts": { - "message": "Mina plånbokskonton" - }, - "myWalletAccountsDescription": { - "message": "Alla dina MetaMask-skapade konton läggs automatiskt till i denna avdelning." - }, "needEtherInWallet": { "message": "För att interagera med decentraliserade applikationer med MetaMask behöver du Ether i din plånbok." }, @@ -822,9 +807,6 @@ "restoreAccountWithSeed": { "message": "Återställ ditt konto med seedphrase" }, - "restoreFromSeed": { - "message": "Återställ konto?" - }, "revealSeedWords": { "message": "Visa seed-ord" }, @@ -879,9 +861,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 +900,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..588071d29 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -486,22 +486,13 @@ "importAccountSeedPhrase": { "message": "Hamisha Akaunti kwa kutumia Kirai Kianzio" }, - "importUsingSeed": { - "message": "Hamisha kwa kutumia kirai kianzio cha akaunti" - }, "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" }, - "infoHelp": { - "message": "Taarifa & Msaada" - }, "initialTransactionConfirmed": { "message": "Muamala wako wa awali ulithibitishwa na mtandao. Bofya SWA ili urudi nyuma." }, @@ -593,12 +584,6 @@ "myAccounts": { "message": "Akaunti zangu" }, - "myWalletAccounts": { - "message": "Akaunti angu za Waleti" - }, - "myWalletAccountsDescription": { - "message": "Akaunti zako zote za MetaMask zilizofunguliwa zitaongezwa kiotomatiki kwenye sehemu hii." - }, "needEtherInWallet": { "message": "Ili kuingiliana na programu zilizosambazwa kwa kutumia MetaMask, utahitaji kuwa na Ether kwenye waleti yako." }, @@ -816,9 +801,6 @@ "restoreAccountWithSeed": { "message": "Rejesha Akaunti yako kwa kutumia Kirai Kianzio." }, - "restoreFromSeed": { - "message": "Rejesha akaunti?" - }, "revealSeedWords": { "message": "Onyesha Maneno ya Kianzio" }, @@ -873,9 +855,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 +894,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..29a239a41 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -226,9 +226,6 @@ "message": "இறக்குமதியானது", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "தகவல் மற்றும் உதவி" - }, "insufficientFunds": { "message": "போதுமான பணம் இல்லை." }, @@ -402,9 +399,6 @@ "restore": { "message": "மீட்டமை" }, - "restoreFromSeed": { - "message": "விதை வாக்கியத்திலிருந்து மீட்கவும்" - }, "revealSeedWords": { "message": "விதை வார்த்தைகள் வெளிப்படுத்த" }, @@ -426,9 +420,6 @@ "searchTokens": { "message": "தேடல் டோக்கன்ஸ்" }, - "secretPhrase": { - "message": "உங்கள் பெட்டகத்தை மீட்டெடுப்பதற்காக இங்கே உங்கள் ரகசிய பன்னிரண்டு வார்த்தை சொற்றொடரை உள்ளிடவும்." - }, "seedPhraseReq": { "message": "விதை வாக்கியங்கள் 12 வார்த்தைகள் நீண்டவை" }, @@ -438,9 +429,6 @@ "send": { "message": "அனுப்பு" }, - "sendETH": { - "message": "ETH ஐ அனுப்பு" - }, "sendTokens": { "message": "டோக்கன்களை அனுப்பவும்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index b0df95826..b1d271c6f 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -244,9 +244,6 @@ "message": "นำเข้าเรียบร้อย", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "ข้อมูลและความช่วยเหลือ" - }, "insufficientFunds": { "message": "เงินทุนไม่เพียงพอ" }, @@ -399,9 +396,6 @@ "resetAccount": { "message": "รีเซ็ตบัญชี" }, - "restoreFromSeed": { - "message": "กู้คืนจากกลุ่มคำชีด" - }, "revealSeedWords": { "message": "เปิดเผยกลุ่มคำชีด" }, @@ -420,9 +414,6 @@ "search": { "message": "ค้นหา" }, - "secretPhrase": { - "message": "ป้อนกลุ่มคำสิบสองคำเพื่อกู้คืนตู้เซฟของคุณ" - }, "seedPhraseReq": { "message": "กลุ่มคำชีดมีความยาว 12 คำ" }, @@ -441,9 +432,6 @@ "sendAmount": { "message": "ส่งจำนวนเงินนี้" }, - "sendETH": { - "message": "ส่งอีเธอร์" - }, "sendTokens": { "message": "ส่งโทเค็น" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 22af3556c..6318b3b2c 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "log ng aktibidad" }, - "addAccount": { - "message": "Magdagdag ng account" - }, "addAcquiredTokens": { "message": "Idagdag ang mga token na nakuha mo gamit ang MetaMask" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "Mag-import ng account gamit ang seed phrase" }, - "importUsingSeed": { - "message": "I-import gamit ang seed phrase ng account" - }, "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" }, - "infoHelp": { - "message": "Impormasyon at Tulong" - }, "initialTransactionConfirmed": { "message": "Nakumpirma na ng network ang iyong inisyal na transaksyon. I-click ang OK para bumalik." }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "Mga Account Ko" }, - "myWalletAccounts": { - "message": "Mga Wallet Account Ko" - }, - "myWalletAccountsDescription": { - "message": "Ang lahat ng iyong ginawang account sa MetaMask ay awtomatikong maidaragdag sa seksyong ito." - }, "needEtherInWallet": { "message": "Para makaugnayan ang mga decentralized ma application gamit ang MetaMask, kakailanganin mo ang Ether sa iyong wallet." }, @@ -1270,9 +1252,6 @@ "restoreAccountWithSeed": { "message": "I-restore ang iyong Account gamit ang Seed Phrase" }, - "restoreFromSeed": { - "message": "I-restore ang account?" - }, "restoreWalletPreferences": { "message": "Nakita ang backup ng iyong data mula sa $1. Gusto mo bang i-restore ang mga kagustuhan mo sa wallet?", "description": "$1 is the date at which the data was backed up" @@ -1343,9 +1322,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 +1373,6 @@ "sendAmount": { "message": "Halaga ng Ipapadala" }, - "sendETH": { - "message": "Magpadala ng ETH" - }, "sendSpecifiedTokens": { "message": "Magpadala ng $1", "description": "Symbol of the specified token" @@ -1624,24 +1597,6 @@ "swapHighSlippageWarning": { "message": "Sobrang laki ng halaga ng slippage. Tiyaking alam mo ang ginagawa mo!" }, - "swapIntroLearnMoreHeader": { - "message": "Gusto mo bang matuto pa?" - }, - "swapIntroLearnMoreLink": { - "message": "Matuto pa tungkol sa MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Kasama sa mga pinagkunan ng liquidity ang:" - }, - "swapIntroPopupSubTitle": { - "message": "Puwede mo nang direktang i-swap ang mga token sa iyong MetaMask wallet. Pinagsasama-sama ng MetaMask Swaps ang maraming decentralized exchange aggregator, propesyonal na market maker, at indibidwal na DEX para matiyak na makukuha palagi ng mga user ng MetaMask ang pinakasulit na presyo nang may pinakamababang bayarin sa network." - }, - "swapIntroPopupTitle": { - "message": "Ito na ang pag-swap ng token!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Suriin ang aming audit ng mga opisyal na kontrata" - }, "swapLowSlippageError": { "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." }, @@ -1663,7 +1618,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "Kasama sa bayarin sa network ang gastusin sa pagproseso ng iyong pag-swap at pag-store nito sa Ethereum network. Hindi kumikita ang MetaMask mula sa bayaring ito." + "message": "Kasama sa bayarin sa network ang gastusin sa pagproseso ng iyong pag-swap at pag-store nito sa $1 network. Hindi kumikita ang MetaMask mula sa bayaring ito." }, "swapNewQuoteIn": { "message": "Mga bagong quote sa $1", @@ -1744,9 +1699,6 @@ "swapSourceInfo": { "message": "Naghahanap kami ng maraming pinagkukunan ng liquidity (mga exchange, aggregator at propesyonal na market maker) para mahanap ang mga pinakasulit na rate at pinakamababang bayarin sa network." }, - "swapStartSwapping": { - "message": "Simulang mag-swap" - }, "swapSwapFrom": { "message": "Ipalit mula sa" }, @@ -1876,9 +1828,6 @@ "tokenContractAddress": { "message": "Address ng Kontrata ng Token" }, - "tokenOptions": { - "message": "Mga opsyon ng token" - }, "tokenSymbol": { "message": "Simbolo ng Token" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 4b71110d0..d47b570ad 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -196,9 +196,6 @@ "message": "Alındı", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Bilgi ve yardım" - }, "insufficientFunds": { "message": "Yetersiz kaynak." }, @@ -342,9 +339,6 @@ "resetAccount": { "message": "Hesabı sıfıla" }, - "restoreFromSeed": { - "message": "Kaynak ifadeden geri getir. Restore from seed phrase" - }, "revealSeedWords": { "message": "Kaynak kelimelerini göster" }, @@ -366,9 +360,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 +369,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..3e5773e42 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -499,22 +499,13 @@ "importAccountSeedPhrase": { "message": "Імпортувати обліковий запис з початковою фразою" }, - "importUsingSeed": { - "message": "Імпортувати, використовуючи початкову фразу облікового запису" - }, "importWallet": { "message": "Імпортувати гаманець" }, - "importYourExisting": { - "message": "Імпортуйте ваш гаманець, що існує, використовуючи початкову фразу з 12 слів" - }, "imported": { "message": "Імпортовано", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "Інформація та допомога" - }, "initialTransactionConfirmed": { "message": "Ваша початкова транзакція підтверджена мережею. Натисніть ОК для повернення." }, @@ -615,12 +606,6 @@ "myAccounts": { "message": "Мої облікові записи" }, - "myWalletAccounts": { - "message": "Облікові записи мого гаманця" - }, - "myWalletAccountsDescription": { - "message": "Усі ваші створені облікові записи MetaMask буде автоматично додано в цей розділ." - }, "needEtherInWallet": { "message": "Щоб взаємодіяти з децентралізованими застосунками використовуючи MetaMask, вам буде потрібен Ether у вашому гаманці." }, @@ -838,9 +823,6 @@ "restoreAccountWithSeed": { "message": "Відновіть ваш обліковий запис за допомогою seed-фрази" }, - "restoreFromSeed": { - "message": "Відновити обліковий запис?" - }, "revealSeedWords": { "message": "Показати мнемонічні слова" }, @@ -895,9 +877,6 @@ "secretBackupPhraseWarning": { "message": "ЗАСТЕРЕЖЕННЯ: Ніколи не розголошуйте вашу резервну фразу. Будь-хто з цією фразою зможе забрати ваш Ether назавжди." }, - "secretPhrase": { - "message": "Введіть секретну фразу з дванадцяти слів, щоб відновити своє сховище." - }, "securityAndPrivacy": { "message": "Безпека й конфіденційність" }, @@ -937,9 +916,6 @@ "sendAmount": { "message": "Надіслати суму" }, - "sendETH": { - "message": "Надіслати ETH" - }, "sendTokens": { "message": "Надіслати токени" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index e35becf7f..cab617c40 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "nhật ký hoạt động" }, - "addAccount": { - "message": "Thêm tài khoản" - }, "addAcquiredTokens": { "message": "Thêm token mà bạn đã mua bằng MetaMask" }, @@ -781,22 +778,13 @@ "importAccountSeedPhrase": { "message": "Nhập một tài khoản bằng cụm mật khẩu gốc" }, - "importUsingSeed": { - "message": "Nhập bằng cách sử dụng cụm mật khẩu gốc của tài khoản" - }, "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" }, - "infoHelp": { - "message": "Thông tin và trợ giúp" - }, "initialTransactionConfirmed": { "message": "Mạng đã xác nhận giao dịch ban đầu của bạn. Nhấp vào OK để quay lại." }, @@ -984,12 +972,6 @@ "myAccounts": { "message": "Tài khoản của tôi" }, - "myWalletAccounts": { - "message": "Các tài khoản trong ví của tôi" - }, - "myWalletAccountsDescription": { - "message": "Tất cả các tài khoản đã tạo của bạn trong MetaMask sẽ tự động được thêm vào phần này." - }, "needEtherInWallet": { "message": "Để tương tác với các ứng dụng phi tập trung bằng MetaMask, bạn sẽ cần phải có Ether trong ví của mình." }, @@ -1273,9 +1255,6 @@ "restoreAccountWithSeed": { "message": "Khôi phục tài khoản của bạn bằng cụm mật khẩu gốc" }, - "restoreFromSeed": { - "message": "Khôi phục tài khoản?" - }, "restoreWalletPreferences": { "message": "Đã tìm thấy bản sao lưu dữ liệu của bạn từ $1. Bạn có muốn khôi phục các tùy chọn ưu tiên trong ví của mình không?", "description": "$1 is the date at which the data was backed up" @@ -1346,9 +1325,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 +1376,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" @@ -1627,24 +1600,6 @@ "swapHighSlippageWarning": { "message": "Số tiền trượt giá rất cao. Hãy chắc chắn rằng bạn hiểu những gì mình đang làm!" }, - "swapIntroLearnMoreHeader": { - "message": "Bạn muốn tìm hiểu thêm?" - }, - "swapIntroLearnMoreLink": { - "message": "Tìm hiểu thêm về MetaMask Swaps" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "Các nguồn thanh khoản bao gồm:" - }, - "swapIntroPopupSubTitle": { - "message": "Giờ đây bạn có thể hoán đổi token ngay trong ví MetaMask của mình. MetaMask Swaps quy tụ nhiều trình tổng hợp sàn giao dịch phi tập trung, các nhà tạo lập thị trường chuyên nghiệp và các sàn giao dịch phi tập trung dành cho cá nhân nhằm đảm bảo người dùng MetaMask luôn nhận được mức giá tốt nhất với phí mạng thấp nhất." - }, - "swapIntroPopupTitle": { - "message": "Tính năng hoán đổi token đã sẵn sàng!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "Xem xét quy trình kiểm tra hợp đồng chính thức của chúng tôi" - }, "swapLowSlippageError": { "message": "Giao dịch có thể không thành công, mức trượt giá tối đa quá thấp." }, @@ -1666,7 +1621,7 @@ "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, "swapNetworkFeeSummary": { - "message": "Phí mạng dùng để chi trả chi phí xử lý giao dịch hoán đổi của bạn và lưu trữ giao dịch đó trên mạng Ethereum. MetaMask không thu lợi từ khoản phí này." + "message": "Phí mạng dùng để chi trả chi phí xử lý giao dịch hoán đổi của bạn và lưu trữ giao dịch đó trên mạng $1. MetaMask không thu lợi từ khoản phí này." }, "swapNewQuoteIn": { "message": "Báo giá mới sẽ có sau $1", @@ -1747,9 +1702,6 @@ "swapSourceInfo": { "message": "Chúng tôi tìm kiếm nhiều nguồn thanh khoản (các sàn giao dịch, trình tổng hợp và nhà tạo lập thị trường) để tìm được mức tỷ lệ tốt nhất và phí mạng thấp nhất." }, - "swapStartSwapping": { - "message": "Bắt đầu hoán đổi" - }, "swapSwapFrom": { "message": "Hoán đổi từ" }, @@ -1879,9 +1831,6 @@ "tokenContractAddress": { "message": "Địa chỉ hợp đồng token" }, - "tokenOptions": { - "message": "Tùy chọn token" - }, "tokenSymbol": { "message": "Ký hiệu token" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9ca2dbe32..222cbf47f 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -43,9 +43,6 @@ "activityLog": { "message": "活动日志" }, - "addAccount": { - "message": "添加一个账户" - }, "addAcquiredTokens": { "message": "在 MetaMask 上添加获得的代币" }, @@ -790,22 +787,13 @@ "importAccountSeedPhrase": { "message": "使用账户助记词导入账户" }, - "importUsingSeed": { - "message": "使用账户助记词导入" - }, "importWallet": { "message": "导入钱包" }, - "importYourExisting": { - "message": "使用 12 个单词的账户助记词导入您现有的钱包账户。" - }, "imported": { "message": "已导入", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "信息 & 帮助" - }, "initialTransactionConfirmed": { "message": "您的初始交易已通过网络确认。请点击“确定”返回。" }, @@ -996,12 +984,6 @@ "myAccounts": { "message": "我的账户" }, - "myWalletAccounts": { - "message": "我的钱包账户" - }, - "myWalletAccountsDescription": { - "message": "所有创建的 MetaMask 账户将自动添加到此部分。" - }, "needEtherInWallet": { "message": "使用 MetaMask 与分布式应用交互,需要您的钱包里需要有 Ether。" }, @@ -1282,9 +1264,6 @@ "restoreAccountWithSeed": { "message": "使用账户助记词恢复您的账户" }, - "restoreFromSeed": { - "message": "从助记词还原" - }, "restoreWalletPreferences": { "message": "已找到于 $1 的数据备份。您想恢复您的钱包设置吗?", "description": "$1 is the date at which the data was backed up" @@ -1355,9 +1334,6 @@ "secretBackupPhraseWarning": { "message": "警告:切勿向他人透露您的账户助记词。任何人一旦持有该账户助记词,即可控制您的 Ether。" }, - "secretPhrase": { - "message": "输入 12 个单词组成的账户助记词恢复您的账户。" - }, "securityAndPrivacy": { "message": "安全与隐私" }, @@ -1409,9 +1385,6 @@ "sendAmount": { "message": "发送数额" }, - "sendETH": { - "message": "发送 ETH" - }, "sendSpecifiedTokens": { "message": "发送 $1", "description": "Symbol of the specified token" @@ -1639,24 +1612,6 @@ "swapHighSlippageWarning": { "message": "滑点数量非常大。确保您知道您的操作!" }, - "swapIntroLearnMoreHeader": { - "message": "想了解更多信息?" - }, - "swapIntroLearnMoreLink": { - "message": "了解更多关于 MetaMask Swap(兑换)" - }, - "swapIntroLiquiditySourcesLabel": { - "message": "流动资金来源包括:" - }, - "swapIntroPopupSubTitle": { - "message": "现在您可以直接在 MetaMask 钱包中兑换代币。MetaMask Swaps(兑换)结合了多个去中心化交易所聚合商、专业做市商和个人 DEX,确保 MetaMask 用户始终以最低的网络费用获得最佳价格。" - }, - "swapIntroPopupTitle": { - "message": "代币兑换来了!" - }, - "swapLearnMoreContractsAuditReview": { - "message": "查看我们的官方合约审计" - }, "swapLowSlippageError": { "message": "交易可能失败,最大滑点过低。" }, @@ -1682,7 +1637,7 @@ "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { - "message": "网络手续费包括处理您的兑换和在以太坊(Ethereum)网络上存储的成本。MetaMask 不从这笔费用中获利。" + "message": "网络手续费包括处理您的兑换和在以太坊($1)网络上存储的成本。MetaMask 不从这笔费用中获利。" }, "swapNewQuoteIn": { "message": "$1 后更新报价", @@ -1777,9 +1732,6 @@ "swapSourceInfo": { "message": "我们搜索多个流动性来源(交易所、聚合商和专业做市商),以找到最好的利率和最低的网络手续费。" }, - "swapStartSwapping": { - "message": "开始兑换" - }, "swapSwapFrom": { "message": "兑换自" }, @@ -1915,9 +1867,6 @@ "tokenContractAddress": { "message": "代币合约地址" }, - "tokenOptions": { - "message": "代币选项" - }, "tokenSymbol": { "message": "代币符号" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index d724a8647..ccabbcaa1 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -508,22 +508,13 @@ "importAccountSeedPhrase": { "message": "利用助憶詞還原" }, - "importUsingSeed": { - "message": "利用助憶詞匯入帳戶" - }, "importWallet": { "message": "匯入錢包" }, - "importYourExisting": { - "message": "使用 12 字的助記詞來匯入你現有的錢包" - }, "imported": { "message": "已匯入私鑰", "description": "status showing that an account has been fully loaded into the keyring" }, - "infoHelp": { - "message": "說明 & 資訊" - }, "initialTransactionConfirmed": { "message": "交易已確認" }, @@ -621,12 +612,6 @@ "myAccounts": { "message": "我的帳戶" }, - "myWalletAccounts": { - "message": "我的錢包帳號" - }, - "myWalletAccountsDescription": { - "message": "所有你在 MetaMask 創建的帳號將自動新增到此區塊。" - }, "needEtherInWallet": { "message": "要使用 MetaMask 存取去中心化應用服務時,您的錢包中需要有以太幣。" }, @@ -832,9 +817,6 @@ "restoreAccountWithSeed": { "message": "透過助憶詞還原您的帳戶" }, - "restoreFromSeed": { - "message": "還原帳戶?" - }, "revealSeedWords": { "message": "顯示助憶詞" }, @@ -889,9 +871,6 @@ "secretBackupPhraseWarning": { "message": "警告: 絕對不要洩漏您的助憶詞。任何人只要得知助憶詞代表他可以竊取您所有的以太幣和代幣。" }, - "secretPhrase": { - "message": "輸入您的12個助憶詞以回復金庫" - }, "securityAndPrivacy": { "message": "安全&隱私" }, @@ -925,9 +904,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/images/address-book.svg b/app/images/address-book.svg new file mode 100644 index 000000000..7559babab --- /dev/null +++ b/app/images/address-book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/mobile-link-qr.svg b/app/images/mobile-link-qr.svg new file mode 100644 index 000000000..85fa39365 --- /dev/null +++ b/app/images/mobile-link-qr.svg @@ -0,0 +1 @@ + diff --git a/app/images/support.svg b/app/images/support.svg new file mode 100644 index 000000000..4670dfb4f --- /dev/null +++ b/app/images/support.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/manifest/_base.json b/app/manifest/_base.json index 5cc5a0246..38754eb1e 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": { @@ -78,6 +71,6 @@ "notifications" ], "short_name": "__MSG_appName__", - "version": "9.4.0", + "version": "9.5.0", "web_accessible_resources": ["inpage.js", "phishing.html"] } diff --git a/test/unit/app/account-import-strategies.test.js b/app/scripts/account-import-strategies/account-import-strategies.test.js similarity index 93% rename from test/unit/app/account-import-strategies.test.js rename to app/scripts/account-import-strategies/account-import-strategies.test.js index 5dc706b90..e29561639 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 { stripHexPrefix } from 'ethereumjs-util'; +import accountImporter from '.'; describe('Account Import Strategies', function () { const privkey = @@ -13,7 +13,7 @@ describe('Account Import Strategies', function () { const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey, ]); - assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey)); + assert.equal(importPrivKey, stripHexPrefix(privkey)); }); it('throws an error for empty string private key', async function () { diff --git a/app/scripts/account-import-strategies/index.js b/app/scripts/account-import-strategies/index.js index 89fd0b8c1..ce52132aa 100644 --- a/app/scripts/account-import-strategies/index.js +++ b/app/scripts/account-import-strategies/index.js @@ -1,7 +1,12 @@ import log from 'loglevel'; import Wallet from 'ethereumjs-wallet'; import importers from 'ethereumjs-wallet/thirdparty'; -import ethUtil from 'ethereumjs-util'; +import { + toBuffer, + isValidPrivate, + bufferToHex, + stripHexPrefix, +} from 'ethereumjs-util'; import { addHexPrefix } from '../lib/util'; const accountImporter = { @@ -22,13 +27,13 @@ const accountImporter = { } const prefixed = addHexPrefix(privateKey); - const buffer = ethUtil.toBuffer(prefixed); + const buffer = toBuffer(prefixed); - if (!ethUtil.isValidPrivate(buffer)) { + if (!isValidPrivate(buffer)) { throw new Error('Cannot import invalid private key.'); } - const stripped = ethUtil.stripHexPrefix(prefixed); + const stripped = stripHexPrefix(prefixed); return stripped; }, 'JSON File': (input, password) => { @@ -47,7 +52,7 @@ const accountImporter = { function walletToPrivateKey(wallet) { const privateKeyBuffer = wallet.getPrivateKey(); - return ethUtil.bufferToHex(privateKeyBuffer); + return bufferToHex(privateKeyBuffer); } export default accountImporter; 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/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 73173bf68..261b735dc 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -22,7 +22,6 @@ export default class AppStateController extends EventEmitter { this.store = new ObservableStore({ timeoutMinutes: 0, connectedStatusPopoverHasBeenShown: true, - swapsWelcomeMessageHasBeenShown: false, defaultHomeActiveTabName: null, ...initState, }); @@ -112,15 +111,6 @@ export default class AppStateController extends EventEmitter { }); } - /** - * Record that the user has seen the swap screen welcome message - */ - setSwapsWelcomeMessageHasBeenShown() { - this.store.updateState({ - swapsWelcomeMessageHasBeenShown: true, - }); - } - /** * Sets the last active time to the current time * @returns {void} 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/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js index 015c3ee0a..a7d4c696b 100644 --- a/app/scripts/controllers/ens/index.js +++ b/app/scripts/controllers/ens/index.js @@ -1,5 +1,5 @@ import punycode from 'punycode/punycode'; -import ethUtil from 'ethereumjs-util'; +import { toChecksumAddress } from 'ethereumjs-util'; import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../../../shared/constants/network'; @@ -43,7 +43,7 @@ export default class EnsController { } reverseResolveAddress(address) { - return this._reverseResolveAddress(ethUtil.toChecksumAddress(address)); + return this._reverseResolveAddress(toChecksumAddress(address)); } async _reverseResolveAddress(address) { @@ -79,7 +79,7 @@ export default class EnsController { return undefined; } - if (ethUtil.toChecksumAddress(registeredAddress) !== address) { + if (toChecksumAddress(registeredAddress) !== address) { return undefined; } 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..24197dbd7 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'; @@ -79,7 +79,7 @@ describe('EnsController', function () { const ens = new EnsController({ ens: { reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), - lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns('0x00'), }, onNetworkDidChange, getCurrentChainId, 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/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 00e07009f..3d0b622ca 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -1,6 +1,6 @@ import { merge, omit } from 'lodash'; import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex, sha3 } from 'ethereumjs-util'; +import { bufferToHex, keccak } from 'ethereumjs-util'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { METAMETRICS_ANONYMOUS_ID, @@ -57,8 +57,6 @@ export default class MetaMetricsController { /** * @param {Object} segment - an instance of analytics-node for tracking * events that conform to the new MetaMetrics tracking plan. - * @param {Object} segmentLegacy - an instance of analytics-node for - * tracking legacy schema events. Will eventually be phased out * @param {Object} preferencesStore - The preferences controller store, used * to access and subscribe to preferences that will be attached to events * @param {function} onNetworkDidChange - Used to attach a listener to the @@ -73,7 +71,6 @@ export default class MetaMetricsController { */ constructor({ segment, - segmentLegacy, preferencesStore, onNetworkDidChange, getCurrentChainId, @@ -105,14 +102,15 @@ export default class MetaMetricsController { this.network = getNetworkIdentifier(); }); this.segment = segment; - this.segmentLegacy = segmentLegacy; } generateMetaMetricsId() { return bufferToHex( - sha3( - String(Date.now()) + - String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)), + keccak( + Buffer.from( + String(Date.now()) + + String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)), + ), ), ); } @@ -258,6 +256,12 @@ export default class MetaMetricsController { } payload[idType] = idValue; + // If this is an event on the old matomo schema, add a key to the payload + // to designate it as such + if (matomoEvent === true) { + payload.properties.legacy_event = true; + } + // Promises will only resolve when the event is sent to segment. For any // event that relies on this promise being fulfilled before performing UI // updates, or otherwise delaying user interaction, supply the @@ -276,11 +280,9 @@ export default class MetaMetricsController { return resolve(); }; - const target = matomoEvent === true ? this.segmentLegacy : this.segment; - - target.track(payload, callback); + this.segment.track(payload, callback); if (flushImmediately) { - target.flush(); + this.segment.flush(); } }); } diff --git a/test/unit/app/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js similarity index 96% rename from test/unit/app/controllers/metametrics.test.js rename to app/scripts/controllers/metametrics.test.js index 839b323c4..e30b6ad5e 100644 --- a/test/unit/app/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1,17 +1,16 @@ 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); const VERSION = '0.0.1-test'; const NETWORK = 'Mainnet'; @@ -91,7 +90,6 @@ function getMetaMetricsController({ } = {}) { return new MetaMetricsController({ segment, - segmentLegacy, getNetworkIdentifier: networkController.getNetworkIdentifier.bind( networkController, ), @@ -286,7 +284,7 @@ describe('MetaMetricsController', function () { }); it('should track a legacy event', function () { - const mock = sinon.mock(segmentLegacy); + const mock = sinon.mock(segment); const metaMetricsController = getMetaMetricsController(); mock .expects('track') @@ -297,6 +295,7 @@ describe('MetaMetricsController', function () { context: DEFAULT_TEST_CONTEXT, properties: { test: 1, + legacy_event: true, ...DEFAULT_EVENT_PROPERTIES, }, }); @@ -544,7 +543,6 @@ describe('MetaMetricsController', function () { afterEach(function () { // flush the queues manually after each test segment.flush(); - segmentLegacy.flush(); sinon.restore(); }); }); 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 f76646a9f..9470df50c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -380,7 +380,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( @@ -820,9 +820,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/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 81a55ead3..2496e9b0b 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -70,7 +70,7 @@ const initialState = { errorKey: '', topAggId: null, routeState: '', - swapsFeatureIsLive: false, + swapsFeatureIsLive: true, swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, }, }; @@ -112,8 +112,6 @@ export default class SwapsController { this.ethersProvider = new ethers.providers.Web3Provider(provider); } }); - - this._setupSwapsLivenessFetching(); } // Sets the refresh rate for quote updates from the MetaSwap API @@ -478,7 +476,6 @@ export default class SwapsController { swapsState: { ...initialState.swapsState, tokens: swapsState.tokens, - swapsFeatureIsLive: swapsState.swapsFeatureIsLive, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, }, }); @@ -686,99 +683,6 @@ export default class SwapsController { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId], ); } - - /** - * Sets up the fetching of the swaps feature liveness flag from our API. - * Performs an initial fetch when called, then fetches on a 10-minute - * interval. - * - * If the browser goes offline, the interval is cleared and swaps are disabled - * until the value can be fetched again. - */ - _setupSwapsLivenessFetching() { - const TEN_MINUTES_MS = 10 * 60 * 1000; - let intervalId = null; - - const fetchAndSetupInterval = () => { - if (window.navigator.onLine && intervalId === null) { - // Set the interval first to prevent race condition between listener and - // initial call to this function. - intervalId = setInterval( - this._fetchAndSetSwapsLiveness.bind(this), - TEN_MINUTES_MS, - ); - this._fetchAndSetSwapsLiveness(); - } - }; - - window.addEventListener('online', fetchAndSetupInterval); - window.addEventListener('offline', () => { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - - const { swapsState } = this.store.getState(); - if (swapsState.swapsFeatureIsLive) { - this.setSwapsLiveness(false); - } - } - }); - - fetchAndSetupInterval(); - } - - /** - * This function should only be called via _setupSwapsLivenessFetching. - * - * Attempts to fetch the swaps feature liveness flag from our API. Tries - * to fetch three times at 5-second intervals before giving up, in which - * case the value defaults to 'false'. - * - * Only updates state if the fetched/computed flag value differs from current - * state. - */ - async _fetchAndSetSwapsLiveness() { - const { swapsState } = this.store.getState(); - const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState; - const chainId = this._getCurrentChainId(); - - let swapsFeatureIsLive = false; - let successfullyFetched = false; - let numAttempts = 0; - - const fetchAndIncrementNumAttempts = async () => { - try { - swapsFeatureIsLive = Boolean( - await this._fetchSwapsFeatureLiveness(chainId), - ); - successfullyFetched = true; - } catch (err) { - log.error(err); - numAttempts += 1; - } - }; - - await fetchAndIncrementNumAttempts(); - - // The loop conditions are modified by fetchAndIncrementNumAttempts. - // eslint-disable-next-line no-unmodified-loop-condition - while (!successfullyFetched && numAttempts < 3) { - await new Promise((resolve) => { - setTimeout(resolve, 5000); // 5 seconds - }); - await fetchAndIncrementNumAttempts(); - } - - if (!successfullyFetched) { - log.error( - 'Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.', - ); - } - - if (swapsFeatureIsLive !== oldSwapsFeatureIsLive) { - this.setSwapsLiveness(swapsFeatureIsLive); - } - } } /** diff --git a/test/unit/app/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js similarity index 83% rename from test/unit/app/controllers/swaps.test.js rename to app/scripts/controllers/swaps.test.js index 3109fe509..4e85aa393 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, @@ -126,7 +124,7 @@ const EMPTY_INIT_STATE = { errorKey: '', topAggId: null, routeState: '', - swapsFeatureIsLive: false, + swapsFeatureIsLive: true, swapsQuoteRefreshTime: 60000, }, }; @@ -899,281 +897,6 @@ describe('SwapsController', function () { }); }); }); - - describe('_setupSwapsLivenessFetching ', function () { - let clock; - const EXPECTED_TIME = 600000; - - const getLivenessState = () => { - return swapsController.store.getState().swapsState.swapsFeatureIsLive; - }; - - // We have to do this to overwrite window.navigator.onLine - const stubWindow = () => { - sandbox.replace(global, 'window', { - addEventListener: window.addEventListener, - navigator: { onLine: true }, - dispatchEvent: window.dispatchEvent, - Event: window.Event, - }); - }; - - beforeEach(function () { - stubWindow(); - clock = sandbox.useFakeTimers(); - sandbox.spy(clock, 'setInterval'); - - sandbox - .stub(SwapsController.prototype, '_fetchAndSetSwapsLiveness') - .resolves(undefined); - - sandbox.spy(SwapsController.prototype, '_setupSwapsLivenessFetching'); - - sandbox.spy(window, 'addEventListener'); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('calls _setupSwapsLivenessFetching in constructor', function () { - swapsController = getSwapsController(); - - assert.ok( - swapsController._setupSwapsLivenessFetching.calledOnce, - 'should have called _setupSwapsLivenessFetching once', - ); - assert.ok(window.addEventListener.calledWith('online')); - assert.ok(window.addEventListener.calledWith('offline')); - assert.ok( - clock.setInterval.calledOnceWithExactly( - sinon.match.func, - EXPECTED_TIME, - ), - 'should have set an interval', - ); - }); - - it('handles browser being offline on boot, then coming online', async function () { - window.navigator.onLine = false; - - swapsController = getSwapsController(); - assert.ok( - swapsController._setupSwapsLivenessFetching.calledOnce, - 'should have called _setupSwapsLivenessFetching once', - ); - assert.ok( - swapsController._fetchAndSetSwapsLiveness.notCalled, - 'should not have called _fetchAndSetSwapsLiveness', - ); - assert.ok( - clock.setInterval.notCalled, - 'should not have set an interval', - ); - assert.strictEqual( - getLivenessState(), - false, - 'swaps feature should be disabled', - ); - - const fetchPromise = new Promise((resolve) => { - const originalFunction = swapsController._fetchAndSetSwapsLiveness; - swapsController._fetchAndSetSwapsLiveness = () => { - originalFunction(); - resolve(); - swapsController._fetchAndSetSwapsLiveness = originalFunction; - }; - }); - - // browser comes online - window.navigator.onLine = true; - window.dispatchEvent(new window.Event('online')); - await fetchPromise; - - assert.ok( - swapsController._fetchAndSetSwapsLiveness.calledOnce, - 'should have called _fetchAndSetSwapsLiveness once', - ); - assert.ok( - clock.setInterval.calledOnceWithExactly( - sinon.match.func, - EXPECTED_TIME, - ), - 'should have set an interval', - ); - }); - - it('clears interval if browser goes offline', async function () { - swapsController = getSwapsController(); - - // set feature to live - const { swapsState } = swapsController.store.getState(); - swapsController.store.updateState({ - swapsState: { ...swapsState, swapsFeatureIsLive: true }, - }); - - sandbox.spy(swapsController.store, 'updateState'); - - assert.ok( - clock.setInterval.calledOnceWithExactly( - sinon.match.func, - EXPECTED_TIME, - ), - 'should have set an interval', - ); - - const clearIntervalPromise = new Promise((resolve) => { - const originalFunction = clock.clearInterval; - clock.clearInterval = (intervalId) => { - originalFunction(intervalId); - clock.clearInterval = originalFunction; - resolve(); - }; - }); - - // browser goes offline - window.navigator.onLine = false; - window.dispatchEvent(new window.Event('offline')); - - // if this resolves, clearInterval was called - await clearIntervalPromise; - - assert.ok( - swapsController._fetchAndSetSwapsLiveness.calledOnce, - 'should have called _fetchAndSetSwapsLiveness once', - ); - assert.ok( - swapsController.store.updateState.calledOnce, - 'should have called updateState once', - ); - assert.strictEqual( - getLivenessState(), - false, - 'swaps feature should be disabled', - ); - }); - }); - - describe('_fetchAndSetSwapsLiveness', function () { - const getLivenessState = () => { - return swapsController.store.getState().swapsState.swapsFeatureIsLive; - }; - - beforeEach(function () { - fetchSwapsFeatureLivenessStub.reset(); - sandbox.stub(SwapsController.prototype, '_setupSwapsLivenessFetching'); - swapsController = getSwapsController(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('fetches feature liveness as expected when API is live', async function () { - fetchSwapsFeatureLivenessStub.resolves(true); - - assert.strictEqual( - getLivenessState(), - false, - 'liveness should be false on boot', - ); - - await swapsController._fetchAndSetSwapsLiveness(); - - assert.ok( - fetchSwapsFeatureLivenessStub.calledOnce, - 'should have called fetch function once', - ); - assert.strictEqual( - getLivenessState(), - true, - 'liveness should be true after call', - ); - }); - - it('does not update state if fetched value is same as state value', async function () { - fetchSwapsFeatureLivenessStub.resolves(false); - sandbox.spy(swapsController.store, 'updateState'); - - assert.strictEqual( - getLivenessState(), - false, - 'liveness should be false on boot', - ); - - await swapsController._fetchAndSetSwapsLiveness(); - - assert.ok( - fetchSwapsFeatureLivenessStub.calledOnce, - 'should have called fetch function once', - ); - assert.ok( - swapsController.store.updateState.notCalled, - 'should not have called store.updateState', - ); - assert.strictEqual( - getLivenessState(), - false, - 'liveness should remain false after call', - ); - }); - - it('tries three times before giving up if fetching fails', async function () { - const clock = sandbox.useFakeTimers(); - fetchSwapsFeatureLivenessStub.rejects(new Error('foo')); - sandbox.spy(swapsController.store, 'updateState'); - - assert.strictEqual( - getLivenessState(), - false, - 'liveness should be false on boot', - ); - - swapsController._fetchAndSetSwapsLiveness(); - await clock.runAllAsync(); - - assert.ok( - fetchSwapsFeatureLivenessStub.calledThrice, - 'should have called fetch function three times', - ); - assert.ok( - swapsController.store.updateState.notCalled, - 'should not have called store.updateState', - ); - assert.strictEqual( - getLivenessState(), - false, - 'liveness should remain false after call', - ); - }); - - it('sets state after fetching on successful retry', async function () { - const clock = sandbox.useFakeTimers(); - fetchSwapsFeatureLivenessStub.onCall(0).rejects(new Error('foo')); - fetchSwapsFeatureLivenessStub.onCall(1).rejects(new Error('foo')); - fetchSwapsFeatureLivenessStub.onCall(2).resolves(true); - - assert.strictEqual( - getLivenessState(), - false, - 'liveness should be false on boot', - ); - - swapsController._fetchAndSetSwapsLiveness(); - await clock.runAllAsync(); - - assert.strictEqual( - fetchSwapsFeatureLivenessStub.callCount, - 3, - 'should have called fetch function three times', - ); - assert.strictEqual( - getLivenessState(), - true, - 'liveness should be true after call', - ); - }); - }); }); describe('utils', function () { 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/token-rates.js b/app/scripts/controllers/token-rates.js index 9d597ce1b..b725b04c9 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,7 +1,7 @@ import { ObservableStore } from '@metamask/obs-store'; import log from 'loglevel'; import { normalize as normalizeAddress } from 'eth-sig-util'; -import ethUtil from 'ethereumjs-util'; +import { toChecksumAddress } from 'ethereumjs-util'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; const fetchWithTimeout = getFetchWithTimeout(30000); @@ -45,7 +45,7 @@ export default class TokenRatesController { this._tokens.forEach((token) => { const price = prices[token.address.toLowerCase()] || - prices[ethUtil.toChecksumAddress(token.address)]; + prices[toChecksumAddress(token.address)]; contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0; diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 031ed7863..3067b3e4f 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,6 +1,6 @@ import EventEmitter from 'safe-event-emitter'; import { ObservableStore } from '@metamask/obs-store'; -import ethUtil from 'ethereumjs-util'; +import { bufferToHex, keccak, toBuffer } from 'ethereumjs-util'; import Transaction from 'ethereumjs-tx'; import EthQuery from 'ethjs-query'; import { ethErrors } from 'eth-rpc-errors'; @@ -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 }; @@ -557,18 +569,18 @@ export default class TransactionController extends EventEmitter { // add r,s,v values for provider request purposes see createMetamaskMiddleware // and JSON rpc standard for further explanation - txMeta.r = ethUtil.bufferToHex(ethTx.r); - txMeta.s = ethUtil.bufferToHex(ethTx.s); - txMeta.v = ethUtil.bufferToHex(ethTx.v); + txMeta.r = bufferToHex(ethTx.r); + txMeta.s = bufferToHex(ethTx.s); + txMeta.v = bufferToHex(ethTx.v); - this.txStateManager.updateTx( + this.txStateManager.updateTransaction( txMeta, 'transactions#signTransaction: add r, s, v values', ); // set state to signed this.txStateManager.setTxStatusSigned(txMeta.id); - const rawTx = ethUtil.bufferToHex(ethTx.serialize()); + const rawTx = bufferToHex(ethTx.serialize()); return rawTx; } @@ -579,19 +591,22 @@ 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); } catch (error) { if (error.message.toLowerCase().includes('known transaction')) { - txHash = ethUtil.sha3(addHexPrefix(rawTx)).toString('hex'); + txHash = keccak(toBuffer(addHexPrefix(rawTx), 'hex')).toString('hex'); txHash = addHexPrefix(txHash); } else { throw error; @@ -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..a249c17fc 100644 --- a/test/unit/app/controllers/transactions/tx-controller.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -1,26 +1,28 @@ import { strict as assert } from 'assert'; import EventEmitter from 'events'; -import ethUtil from 'ethereumjs-util'; +import { toBuffer } 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,17 +505,20 @@ 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, ); const rawTx = await txController.signTransaction('1'); - const ethTx = new EthTx(ethUtil.toBuffer(rawTx)); + const ethTx = new EthTx(toBuffer(rawTx)); assert.equal(ethTx.getChainId(), 42); }); }); @@ -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/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 75eb60d83..1b84577b1 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -1,6 +1,6 @@ import EthQuery from 'ethjs-query'; import log from 'loglevel'; -import ethUtil from 'ethereumjs-util'; +import { addHexPrefix } from 'ethereumjs-util'; import { cloneDeep } from 'lodash'; import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util'; @@ -103,7 +103,7 @@ export default class TxGasUtil { // add additional gas buffer to our estimation for safety const gasLimit = this.addGasBuffer( - ethUtil.addHexPrefix(estimatedGasHex), + addHexPrefix(estimatedGasHex), blockGasLimit, multiplier, ); 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..56eabf552 100644 --- a/app/scripts/lib/decrypt-message-manager.js +++ b/app/scripts/lib/decrypt-message-manager.js @@ -1,12 +1,12 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; -import ethUtil from 'ethereumjs-util'; +import { bufferToHex, stripHexPrefix } from 'ethereumjs-util'; 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; @@ -337,7 +337,7 @@ export default class DecryptMessageManager extends EventEmitter { */ normalizeMsgData(data) { try { - const stripped = ethUtil.stripHexPrefix(data); + const stripped = stripHexPrefix(data); if (stripped.match(hexRe)) { return addHexPrefix(stripped); } @@ -345,6 +345,6 @@ export default class DecryptMessageManager extends EventEmitter { log.debug(`Message was not hex encoded, interpreting as utf8.`); } - return ethUtil.bufferToHex(Buffer.from(data, 'utf8')); + return bufferToHex(Buffer.from(data, 'utf8')); } } 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..bb7756b33 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,10 +1,10 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; -import ethUtil from 'ethereumjs-util'; +import { bufferToHex } 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 @@ -298,5 +298,5 @@ function normalizeMsgData(data) { return data; } // data is unicode, convert to hex - return ethUtil.bufferToHex(Buffer.from(data, 'utf8')); + return bufferToHex(Buffer.from(data, 'utf8')); } 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..6002b0709 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -1,12 +1,12 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; -import ethUtil from 'ethereumjs-util'; +import { bufferToHex, stripHexPrefix } from 'ethereumjs-util'; 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; @@ -322,7 +322,7 @@ export default class PersonalMessageManager extends EventEmitter { */ normalizeMsgData(data) { try { - const stripped = ethUtil.stripHexPrefix(data); + const stripped = stripHexPrefix(data); if (stripped.match(hexRe)) { return addHexPrefix(stripped); } @@ -330,6 +330,6 @@ export default class PersonalMessageManager extends EventEmitter { log.debug(`Message was not hex encoded, interpreting as utf8.`); } - return ethUtil.bufferToHex(Buffer.from(data, 'utf8')); + return bufferToHex(Buffer.from(data, 'utf8')); } } 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/segment.js b/app/scripts/lib/segment.js index fa88128c8..a75cf29bd 100644 --- a/app/scripts/lib/segment.js +++ b/app/scripts/lib/segment.js @@ -4,7 +4,6 @@ const isDevOrTestEnvironment = Boolean( process.env.METAMASK_DEBUG || process.env.IN_TEST, ); const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? null; -const SEGMENT_LEGACY_WRITE_KEY = process.env.SEGMENT_LEGACY_WRITE_KEY ?? null; const SEGMENT_HOST = process.env.SEGMENT_HOST ?? null; // flushAt controls how many events are sent to segment at once. Segment will @@ -90,12 +89,3 @@ export const segment = flushAt: SEGMENT_FLUSH_AT, flushInterval: SEGMENT_FLUSH_INTERVAL, }); - -export const segmentLegacy = - !SEGMENT_LEGACY_WRITE_KEY || (isDevOrTestEnvironment && !SEGMENT_HOST) - ? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL) - : new Analytics(SEGMENT_LEGACY_WRITE_KEY, { - host: SEGMENT_HOST, - flushAt: SEGMENT_FLUSH_AT, - flushInterval: SEGMENT_FLUSH_INTERVAL, - }); 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/app/scripts/lib/util.js b/app/scripts/lib/util.js index ccf8d1193..46e5b8641 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,6 +1,6 @@ import assert from 'assert'; import extension from 'extensionizer'; -import ethUtil from 'ethereumjs-util'; +import { stripHexPrefix } from 'ethereumjs-util'; import BN from 'bn.js'; import { memoize } from 'lodash'; @@ -111,7 +111,7 @@ function sufficientBalance(txParams, hexBalance) { * */ function hexToBn(inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16); + return new BN(stripHexPrefix(inputHex), 16); } /** 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..2ae3c6ef1 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'; @@ -11,7 +10,7 @@ import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager' import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware'; import KeyringController from 'eth-keyring-controller'; import { Mutex } from 'await-semaphore'; -import ethUtil from 'ethereumjs-util'; +import { toChecksumAddress, stripHexPrefix } from 'ethereumjs-util'; import log from 'loglevel'; import TrezorKeyring from 'eth-trezor-keyring'; import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring'; @@ -23,10 +22,12 @@ import { ApprovalController, CurrencyRateController, PhishingController, + NotificationController, } 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 { UI_NOTIFICATIONS } from '../../shared/notifications'; + import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; @@ -60,7 +61,8 @@ import nodeify from './lib/nodeify'; 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 { segment } 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 +116,6 @@ export default class MetamaskController extends EventEmitter { this.approvalController = new ApprovalController({ showApprovalRequest: opts.showUserConfirmation, - defaultApprovalType: 'NO_TYPE', }); this.networkController = new NetworkController(initState.NetworkController); @@ -130,7 +131,6 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController = new MetaMetricsController({ segment, - segmentLegacy, preferencesStore: this.preferencesController.store, onNetworkDidChange: this.networkController.on.bind( this.networkController, @@ -163,6 +163,11 @@ export default class MetamaskController extends EventEmitter { this.phishingController = new PhishingController(); + this.notificationController = new NotificationController( + { allNotifications: UI_NOTIFICATIONS }, + initState.NotificationController, + ); + // now we can initialize the RPC provider, which other controllers require this.initializeProvider(); this.provider = this.networkController.getProviderAndBlockTracker().provider; @@ -190,7 +195,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 +329,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 +341,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, + }, + ); } } }); @@ -412,6 +434,7 @@ export default class MetamaskController extends EventEmitter { PermissionsController: this.permissionsController.permissions, PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, + NotificationController: this.notificationController, }); this.memStore = new ComposableObservableStore(null, { @@ -440,6 +463,7 @@ export default class MetamaskController extends EventEmitter { SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, + NotificationController: this.notificationController, }); this.memStore.subscribe(this.sendUpdate.bind(this)); @@ -489,9 +513,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 +606,7 @@ export default class MetamaskController extends EventEmitter { const isInitialized = Boolean(vault); return { - ...{ isInitialized }, + isInitialized, ...this.memStore.getFlatState(), }; } @@ -588,7 +614,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. */ @@ -719,10 +745,6 @@ export default class MetamaskController extends EventEmitter { this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController, ), - setSwapsWelcomeMessageHasBeenShown: nodeify( - this.appStateController.setSwapsWelcomeMessageHasBeenShown, - this.appStateController, - ), // EnsController tryReverseResolveAddress: nodeify( @@ -748,7 +770,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), @@ -930,6 +951,12 @@ export default class MetamaskController extends EventEmitter { approvalController.reject, approvalController, ), + + // Notifications + updateViewedNotifications: nodeify( + this.notificationController.updateViewed, + this.notificationController, + ), }; } @@ -1061,10 +1088,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 @@ -1084,16 +1107,14 @@ export default class MetamaskController extends EventEmitter { // Filter ERC20 tokens const filteredAccountTokens = {}; Object.keys(accountTokens).forEach((address) => { - const checksummedAddress = ethUtil.toChecksumAddress(address); + const checksummedAddress = toChecksumAddress(address); filteredAccountTokens[checksummedAddress] = {}; Object.keys(accountTokens[address]).forEach((chainId) => { filteredAccountTokens[checksummedAddress][chainId] = chainId === MAINNET_CHAIN_ID ? accountTokens[address][chainId].filter( ({ address: tokenAddress }) => { - const checksumAddress = ethUtil.toChecksumAddress( - tokenAddress, - ); + const checksumAddress = toChecksumAddress(tokenAddress); return contractMap[checksumAddress] ? contractMap[checksumAddress].erc20 : true; @@ -1130,10 +1151,10 @@ export default class MetamaskController extends EventEmitter { const accounts = { hd: hdAccounts .filter((item, pos) => hdAccounts.indexOf(item) === pos) - .map((address) => ethUtil.toChecksumAddress(address)), + .map((address) => toChecksumAddress(address)), simpleKeyPair: simpleKeyPairAccounts .filter((item, pos) => simpleKeyPairAccounts.indexOf(item) === pos) - .map((address) => ethUtil.toChecksumAddress(address)), + .map((address) => toChecksumAddress(address)), ledger: [], trezor: [], }; @@ -1142,8 +1163,8 @@ export default class MetamaskController extends EventEmitter { let { transactions } = this.txController.store.getState(); // delete tx for other accounts that we're not importing - transactions = transactions.filter((tx) => { - const checksummedTxFrom = ethUtil.toChecksumAddress(tx.txParams.from); + transactions = Object.values(transactions).filter((tx) => { + const checksummedTxFrom = toChecksumAddress(tx.txParams.from); return accounts.hd.includes(checksummedTxFrom); }); @@ -1628,7 +1649,7 @@ export default class MetamaskController extends EventEmitter { const msgId = msgParams.metamaskId; const msg = this.decryptMessageManager.getMsg(msgId); try { - const stripped = ethUtil.stripHexPrefix(msgParams.data); + const stripped = stripHexPrefix(msgParams.data); const buff = Buffer.from(stripped, 'hex'); msgParams.data = JSON.parse(buff.toString('utf8')); @@ -1658,7 +1679,7 @@ export default class MetamaskController extends EventEmitter { msgParams, ); - const stripped = ethUtil.stripHexPrefix(cleanMsgParams.data); + const stripped = stripHexPrefix(cleanMsgParams.data); const buff = Buffer.from(stripped, 'hex'); cleanMsgParams.data = JSON.parse(buff.toString('utf8')); @@ -1861,10 +1882,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 +2011,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 +2436,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 96% rename from test/unit/app/controllers/metamask-controller.test.js rename to app/scripts/metamask-controller.test.js index d92da4ed4..ffdd6246d 100644 --- a/test/unit/app/controllers/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -2,16 +2,27 @@ import assert from 'assert'; import sinon from 'sinon'; import { cloneDeep } from 'lodash'; import nock from 'nock'; -import ethUtil from 'ethereumjs-util'; +import { pubToAddress, bufferToHex } 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'; @@ -176,11 +184,11 @@ describe('MetaMaskController', function () { const simpleKeyrings = metamaskController.keyringController.getKeyringsByType( 'Simple Key Pair', ); - const privKeyBuffer = simpleKeyrings[0].wallets[0]._privKey; - const pubKeyBuffer = simpleKeyrings[0].wallets[0]._pubKey; - const addressBuffer = ethUtil.pubToAddress(pubKeyBuffer); - const privKey = ethUtil.bufferToHex(privKeyBuffer); - const pubKey = ethUtil.bufferToHex(addressBuffer); + const privKeyBuffer = simpleKeyrings[0].wallets[0].privateKey; + const pubKeyBuffer = simpleKeyrings[0].wallets[0].publicKey; + const addressBuffer = pubToAddress(pubKeyBuffer); + const privKey = bufferToHex(privKeyBuffer); + const pubKey = bufferToHex(addressBuffer); assert.equal(privKey, addHexPrefix(importPrivkey)); assert.equal(pubKey, '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'); }); @@ -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, ); }); @@ -898,7 +906,10 @@ describe('MetaMaskController', function () { try { await metamaskController.signMessage(messages[0].msgParams); } catch (error) { - assert.equal(error.message, 'message length is invalid'); + assert.equal( + error.message, + 'Expected message to be an Uint8Array with length 32', + ); } }); }); @@ -1108,7 +1119,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/app/scripts/migrations/039.js b/app/scripts/migrations/039.js index adc8390db..7dcf904de 100644 --- a/app/scripts/migrations/039.js +++ b/app/scripts/migrations/039.js @@ -1,5 +1,5 @@ import { cloneDeep } from 'lodash'; -import ethUtil from 'ethereumjs-util'; +import { toChecksumAddress } from 'ethereumjs-util'; const version = 39; @@ -12,7 +12,7 @@ function isOldDai(token = {}) { token && typeof token === 'object' && token.symbol === DAI_V1_TOKEN_SYMBOL && - ethUtil.toChecksumAddress(token.address) === DAI_V1_CONTRACT_ADDRESS + toChecksumAddress(token.address) === DAI_V1_CONTRACT_ADDRESS ); } 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 98% rename from test/unit/migrations/048.test.js rename to app/scripts/migrations/048.test.js index cd731c38e..0b02a814b 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', @@ -352,13 +352,13 @@ describe('migration #48', function () { data: { AddressBookController: { addressBook: { - 1: { + '1': { address1: { chainId: '1', foo: 'bar', }, }, - 100: { + '100': { address1: { chainId: '100', foo: 'bar', @@ -417,7 +417,7 @@ describe('migration #48', function () { data: { AddressBookController: { addressBook: { - 2: { + '2': { address1: { chainId: '2', key2: 'kaplar', @@ -490,7 +490,7 @@ describe('migration #48', function () { AddressBookController: { addressBook: { '0x1': { foo: { bar: 'baz' } }, - kaplar: { foo: { bar: 'baz' } }, + 'kaplar': { foo: { bar: 'baz' } }, }, bar: { baz: 'buzz', @@ -506,7 +506,7 @@ describe('migration #48', function () { AddressBookController: { addressBook: { '0x1': { foo: { bar: 'baz' } }, - kaplar: { foo: { bar: 'baz' } }, + 'kaplar': { foo: { bar: 'baz' } }, }, bar: { baz: 'buzz', 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/app/scripts/migrations/050.js b/app/scripts/migrations/050.js index 0d7b16296..ba3929355 100644 --- a/app/scripts/migrations/050.js +++ b/app/scripts/migrations/050.js @@ -24,7 +24,7 @@ export default { versionedData.meta.version = version; LEGACY_LOCAL_STORAGE_KEYS.forEach((key) => - window.localStorage.removeItem(key), + window.localStorage?.removeItem(key), ); return versionedData; 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/058.js b/app/scripts/migrations/058.js new file mode 100644 index 000000000..247d11bf7 --- /dev/null +++ b/app/scripts/migrations/058.js @@ -0,0 +1,23 @@ +import { cloneDeep } from 'lodash'; + +const version = 58; + +/** + * Deletes the swapsWelcomeMessageHasBeenShown property from state + */ +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) { + delete state.AppStateController?.swapsWelcomeMessageHasBeenShown; + + return state; +} diff --git a/app/scripts/migrations/058.test.js b/app/scripts/migrations/058.test.js new file mode 100644 index 000000000..02efdcdd7 --- /dev/null +++ b/app/scripts/migrations/058.test.js @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import migration58 from './058'; + +describe('migration #58', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 57, + }, + data: {}, + }; + + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 58, + }); + }); + + describe('deleting swapsWelcomeMessageHasBeenShown', function () { + it('should delete the swapsWelcomeMessageHasBeenShown property', async function () { + const oldStorage = { + meta: {}, + data: { + AppStateController: { + swapsWelcomeMessageHasBeenShown: false, + bar: 'baz', + }, + foo: 'bar', + }, + }; + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.data.AppStateController, { bar: 'baz' }); + }); + + it('should not modify state if the AppStateController does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + foo: 'bar', + }, + }; + const newStorage = await migration58.migrate(oldStorage); + assert.deepEqual(newStorage.data, oldStorage.data); + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index b4368c1b5..b0c1716f5 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -57,6 +57,12 @@ 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, + require('./058').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..455bf5beb --- /dev/null +++ b/development/auto-changelog.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +const fs = require('fs').promises; + +const path = require('path'); +const { version } = require('../app/manifest/_base.json'); +const { updateChangelog } = require('./lib/changelog/updateChangelog'); +const { unreleased } = require('./lib/changelog/constants'); + +const REPO_URL = 'https://github.com/MetaMask/metamask-extension'; + +const command = 'yarn update-changelog'; + +const helpText = `Usage: ${command} [--rc] [-h|--help] +Update CHANGELOG.md with any changes made since the most recent release. + +Options: + --rc Add new changes to the current release header, rather than to the + '${unreleased}' section. + -h, --help Display this help and exit. + +New commits will be added to the "${unreleased}" section (or to the section for the +current release if the '--rc' flag is used) in reverse chronological order. Any +commits for PRs that are represented already in the changelog will be ignored. + +If the '--rc' flag is used and the section for the current release does not yet +exist, it will be created. +`; + +async function main() { + const args = process.argv.slice(2); + let isReleaseCandidate = false; + + for (const arg of args) { + if (arg === '--rc') { + isReleaseCandidate = true; + } else if (['--help', '-h'].includes(arg)) { + console.log(helpText); + process.exit(0); + } else { + console.error( + `Unrecognized argument: ${arg}\nTry '${command} --help' for more information.\n`, + ); + process.exit(1); + } + } + + const changelogFilename = path.resolve(__dirname, '..', 'CHANGELOG.md'); + const changelogContent = await fs.readFile(changelogFilename, { + encoding: 'utf8', + }); + + const newChangelogContent = await updateChangelog({ + changelogContent, + currentVersion: version, + repoUrl: REPO_URL, + isReleaseCandidate, + }); + + await fs.writeFile(changelogFilename, newChangelogContent); + + 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/changelog/changelog.js b/development/lib/changelog/changelog.js new file mode 100644 index 000000000..b16dca200 --- /dev/null +++ b/development/lib/changelog/changelog.js @@ -0,0 +1,305 @@ +const semver = require('semver'); + +const { orderedChangeCategories, unreleased } = require('./constants'); + +const changelogTitle = '# Changelog'; +const changelogDescription = `All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).`; + +// Stringification helpers + +function stringifyCategory(category, changes) { + const categoryHeader = `### ${category}`; + if (changes.length === 0) { + return categoryHeader; + } + const changeDescriptions = changes + .map((description) => `- ${description}`) + .join('\n'); + return `${categoryHeader}\n${changeDescriptions}`; +} + +function stringifyRelease(version, categories, { date, status } = {}) { + const releaseHeader = `## [${version}]${date ? ` - ${date}` : ''}${ + status ? ` [${status}]` : '' + }`; + const categorizedChanges = orderedChangeCategories + .filter((category) => categories[category]) + .map((category) => { + const changes = categories[category]; + return stringifyCategory(category, changes); + }) + .join('\n\n'); + if (categorizedChanges === '') { + return releaseHeader; + } + return `${releaseHeader}\n${categorizedChanges}`; +} + +function stringifyReleases(releases, changes) { + const stringifiedUnreleased = stringifyRelease( + unreleased, + changes[unreleased], + ); + const stringifiedReleases = releases.map(({ version, date, status }) => { + const categories = changes[version]; + return stringifyRelease(version, categories, { date, status }); + }); + + return [stringifiedUnreleased, ...stringifiedReleases].join('\n\n'); +} + +function withTrailingSlash(url) { + return url.endsWith('/') ? url : `${url}/`; +} + +function getCompareUrl(repoUrl, firstRef, secondRef) { + return `${withTrailingSlash(repoUrl)}compare/${firstRef}...${secondRef}`; +} + +function getTagUrl(repoUrl, tag) { + return `${withTrailingSlash(repoUrl)}releases/tag/${tag}`; +} + +function stringifyLinkReferenceDefinitions(repoUrl, releases) { + const orderedReleases = releases + .map(({ version }) => version) + .sort((a, b) => semver.gt(a, b)); + + // The "Unreleased" section represents all changes made since the *highest* + // release, not the most recent release. This is to accomodate patch releases + // of older versions that don't represent the latest set of changes. + // + // For example, if a library has a v2.0.0 but the v1.0.0 release needed a + // security update, the v1.0.1 release would then be the most recent, but the + // range of unreleased changes would remain `v2.0.0...HEAD`. + const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${getCompareUrl( + repoUrl, + `v${orderedReleases[0]}`, + 'HEAD', + )}`; + + // The "previous" release that should be used for comparison is not always + // the most recent release chronologically. The _highest_ version that is + // lower than the current release is used as the previous release, so that + // patch releases on older releases can be accomodated. + const releaseLinkReferenceDefinitions = releases + .map(({ version }) => { + if (version === orderedReleases[orderedReleases.length - 1]) { + return `[${version}]: ${getTagUrl(repoUrl, `v${version}`)}`; + } + const versionIndex = orderedReleases.indexOf(version); + const previousVersion = orderedReleases + .slice(versionIndex) + .find((releaseVersion) => { + return semver.gt(version, releaseVersion); + }); + return `[${version}]: ${getCompareUrl( + repoUrl, + `v${previousVersion}`, + `v${version}`, + )}`; + }) + .join('\n'); + return `${unreleasedLinkReferenceDefinition}\n${releaseLinkReferenceDefinitions}${ + releases.length > 0 ? '\n' : '' + }`; +} + +/** + * @typedef {import('./constants.js').Unreleased} Unreleased + * @typedef {import('./constants.js').ChangeCategories ChangeCategories} + */ +/** + * @typedef {import('./constants.js').Version} Version + */ +/** + * Release metadata. + * @typedef {Object} ReleaseMetadata + * @property {string} date - An ISO-8601 formatted date, representing the + * release date. + * @property {string} status -The status of the release (e.g. 'WITHDRAWN', 'DEPRECATED') + * @property {Version} version - The version of the current release. + */ + +/** + * Category changes. A list of changes in a single category. + * @typedef {Array} CategoryChanges + */ + +/** + * Release changes, organized by category + * @typedef {Record} ReleaseChanges + */ + +/** + * Changelog changes, organized by release and by category. + * @typedef {Record} ChangelogChanges + */ + +/** + * A changelog that complies with the ["keep a changelog" v1.1.0 guidelines]{@link https://keepachangelog.com/en/1.0.0/}. + * + * This changelog starts out completely empty, and allows new releases and + * changes to be added such that the changelog remains compliant at all times. + * This can be used to help validate the contents of a changelog, normalize + * formatting, update a changelog, or build one from scratch. + */ +class Changelog { + /** + * Construct an empty changelog + * + * @param {Object} options + * @param {string} options.repoUrl - The GitHub repository URL for the current project + */ + constructor({ repoUrl }) { + this._releases = []; + this._changes = { [unreleased]: {} }; + this._repoUrl = repoUrl; + } + + /** + * Add a release to the changelog + * + * @param {Object} options + * @param {boolean} [options.addToStart] - Determines whether the release is + * added to the top or bottom of the changelog. This defaults to 'true' + * because new releases should be added to the top of the changelog. This + * should be set to 'false' when parsing a changelog top-to-bottom. + * @param {string} [options.date] - An ISO-8601 formatted date, representing the + * release date. + * @param {string} [options.status] - The status of the release (e.g. + * 'WITHDRAWN', 'DEPRECATED') + * @param {Version} options.version - The version of the current release, + * which should be a [semver]{@link https://semver.org/spec/v2.0.0.html}- + * compatible version. + */ + addRelease({ addToStart = true, date, status, version }) { + if (!version) { + throw new Error('Version required'); + } else if (semver.valid(version) === null) { + throw new Error(`Not a valid semver version: '${version}'`); + } else if (this._changes[version]) { + throw new Error(`Release already exists: '${version}'`); + } + + this._changes[version] = {}; + const newRelease = { version, date, status }; + if (addToStart) { + this._releases.unshift(newRelease); + } else { + this._releases.push(newRelease); + } + } + + /** + * Add a change to the changelog + * + * @param {Object} options + * @param {boolean} [options.addToStart] - Determines whether the change is + * added to the top or bottom of the list of changes in this category. This + * defaults to 'true' because changes should be in reverse-chronological + * order. This should be set to 'false' when parsing a changelog top-to- + * bottom. + * @param {string} options.category - The category of the change. + * @param {string} options.description - The description of the change. + * @param {Version} [options.version] - The version this change was released + * in. If this is not given, the change is assumed to be unreleased. + */ + addChange({ addToStart = true, category, description, version }) { + if (!category) { + throw new Error('Category required'); + } else if (!orderedChangeCategories.includes(category)) { + throw new Error(`Unrecognized category: '${category}'`); + } else if (!description) { + throw new Error('Description required'); + } else if (version !== undefined && !this._changes[version]) { + throw new Error(`Specified release version does not exist: '${version}'`); + } + + const release = version + ? this._changes[version] + : this._changes[unreleased]; + + if (!release[category]) { + release[category] = []; + } + if (addToStart) { + release[category].unshift(description); + } else { + release[category].push(description); + } + } + + /** + * Migrate all unreleased changes to a release section. + * + * Changes are migrated in their existing categories, and placed above any + * pre-existing changes in that category. + * + * @param {Version} version - The release version to migrate unreleased + * changes to. + */ + migrateUnreleasedChangesToRelease(version) { + const releaseChanges = this._changes[version]; + if (!releaseChanges) { + throw new Error(`Specified release version does not exist: '${version}'`); + } + + const unreleasedChanges = this._changes[unreleased]; + + for (const category of Object.keys(unreleasedChanges)) { + if (releaseChanges[category]) { + releaseChanges[category] = [ + ...unreleasedChanges[category], + ...releaseChanges[category], + ]; + } else { + releaseChanges[category] = unreleasedChanges[category]; + } + } + this._changes[unreleased] = {}; + } + + /** + * Gets the metadata for all releases. + * @returns {Array} The metadata for each release. + */ + getReleases() { + return this._releases; + } + + /** + * Gets the changes in the given release, organized by category. + * @param {Version} version - The version of the release being retrieved. + * @returns {ReleaseChanges} The changes included in the given released. + */ + getReleaseChanges(version) { + return this._changes[version]; + } + + /** + * Gets all changes that have not yet been released + * @returns {ReleaseChanges} The changes that have not yet been released. + */ + getUnreleasedChanges() { + return this._changes[unreleased]; + } + + /** + * The stringified changelog, formatted according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + * @returns {string} The stringified changelog. + */ + toString() { + return `${changelogTitle} +${changelogDescription} + +${stringifyReleases(this._releases, this._changes)} + +${stringifyLinkReferenceDefinitions(this._repoUrl, this._releases)}`; + } +} + +module.exports = Changelog; diff --git a/development/lib/changelog/constants.js b/development/lib/changelog/constants.js new file mode 100644 index 000000000..c2b8ae008 --- /dev/null +++ b/development/lib/changelog/constants.js @@ -0,0 +1,68 @@ +/** + * Version string + * @typedef {string} Version - A [SemVer]{@link https://semver.org/spec/v2.0.0.html}- + * compatible version string. + */ + +/** + * Change categories. + * + * Most of these categories are from [Keep a Changelog]{@link https://keepachangelog.com/en/1.0.0/}. + * The "Uncategorized" category was added because we have many changes from + * older releases that would be difficult to categorize. + * + * @typedef {Record} ChangeCategories + * @property {'Added'} Added - for new features. + * @property {'Changed'} Changed - for changes in existing functionality. + * @property {'Deprecated'} Deprecated - for soon-to-be removed features. + * @property {'Fixed'} Fixed - for any bug fixes. + * @property {'Removed'} Removed - for now removed features. + * @property {'Security'} Security - in case of vulnerabilities. + * @property {'Uncategorized'} Uncategorized - for any changes that have not + * yet been categorized. + */ + +/** + * @type {ChangeCategories} + */ +const changeCategories = { + Added: 'Added', + Changed: 'Changed', + Deprecated: 'Deprecated', + Fixed: 'Fixed', + Removed: 'Removed', + Security: 'Security', + Uncategorized: 'Uncategorized', +}; + +/** + * Change categories in the order in which they should be listed in the + * changelog. + * + * @type {Array} + */ +const orderedChangeCategories = [ + 'Uncategorized', + 'Added', + 'Changed', + 'Deprecated', + 'Removed', + 'Fixed', + 'Security', +]; + +/** + * The header for the section of the changelog listing unreleased changes. + * @typedef {'Unreleased'} Unreleased + */ + +/** + * @type {Unreleased} + */ +const unreleased = 'Unreleased'; + +module.exports = { + changeCategories, + orderedChangeCategories, + unreleased, +}; diff --git a/development/lib/changelog/parseChangelog.js b/development/lib/changelog/parseChangelog.js new file mode 100644 index 000000000..228da3635 --- /dev/null +++ b/development/lib/changelog/parseChangelog.js @@ -0,0 +1,84 @@ +const Changelog = require('./changelog'); +const { unreleased } = require('./constants'); + +function truncated(line) { + return line.length > 80 ? `${line.slice(0, 80)}...` : line; +} + +/** + * Constructs a Changelog instance that represents the given changelog, which + * is parsed for release and change informatino. + * @param {Object} options + * @param {string} options.changelogContent - The changelog to parse + * @param {string} options.repoUrl - The GitHub repository URL for the current + * project. + * @returns {Changelog} A changelog instance that reflects the changelog text + * provided. + */ +function parseChangelog({ changelogContent, repoUrl }) { + const changelogLines = changelogContent.split('\n'); + const changelog = new Changelog({ repoUrl }); + + const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`); + if (unreleasedHeaderIndex === -1) { + throw new Error(`Failed to find ${unreleased} header`); + } + const unreleasedLinkReferenceDefinition = changelogLines.findIndex((line) => { + return line.startsWith(`[${unreleased}]: `); + }); + if (unreleasedLinkReferenceDefinition === -1) { + throw new Error(`Failed to find ${unreleased} link reference definition`); + } + + const contentfulChangelogLines = changelogLines + .slice(unreleasedHeaderIndex + 1, unreleasedLinkReferenceDefinition) + .filter((line) => line !== ''); + + let mostRecentRelease; + let mostRecentCategory; + for (const line of contentfulChangelogLines) { + if (line.startsWith('## [')) { + const results = line.match( + /^## \[(\d+\.\d+\.\d+)\](?: - (\d\d\d\d-\d\d-\d\d))?(?: \[(\w+)\])?/u, + ); + if (results === null) { + throw new Error(`Malformed release header: '${truncated(line)}'`); + } + mostRecentRelease = results[1]; + mostRecentCategory = undefined; + const date = results[2]; + const status = results[3]; + changelog.addRelease({ + addToStart: false, + date, + status, + version: mostRecentRelease, + }); + } else if (line.startsWith('### ')) { + const results = line.match(/^### (\w+)$\b/u); + if (results === null) { + throw new Error(`Malformed category header: '${truncated(line)}'`); + } + mostRecentCategory = results[1]; + } else if (line.startsWith('- ')) { + if (mostRecentCategory === undefined) { + throw new Error(`Category missing for change: '${truncated(line)}'`); + } + const description = line.slice(2); + changelog.addChange({ + addToStart: false, + category: mostRecentCategory, + description, + version: mostRecentRelease, + }); + } else if (mostRecentRelease === null) { + continue; + } else { + throw new Error(`Unrecognized line: '${truncated(line)}'`); + } + } + + return changelog; +} + +module.exports = { parseChangelog }; diff --git a/development/lib/changelog/updateChangelog.js b/development/lib/changelog/updateChangelog.js new file mode 100644 index 000000000..b94d6caf1 --- /dev/null +++ b/development/lib/changelog/updateChangelog.js @@ -0,0 +1,171 @@ +const assert = require('assert').strict; +const runCommand = require('../runCommand'); +const { parseChangelog } = require('./parseChangelog'); +const { changeCategories } = require('./constants'); + +async function getMostRecentTag() { + 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'); + return mostRecentTag; +} + +async function getCommits(commitHashes) { + const commits = []; + for (const commitHash of commitHashes) { + const [subject] = await runCommand('git', [ + 'show', + '-s', + '--format=%s', + commitHash, + ]); + + 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', + commitHash, + ]); + description = firstLineOfBody || subject; + } + // Otherwise: + // Normal commits: The commit subject is the description, and the PR ID is omitted. + + commits.push({ prNumber, description }); + } + return commits; +} + +function getAllChangeDescriptions(changelog) { + const releases = changelog.getReleases(); + const changeDescriptions = Object.values( + changelog.getUnreleasedChanges(), + ).flat(); + for (const release of releases) { + changeDescriptions.push( + ...Object.values(changelog.getReleaseChanges(release.version)).flat(), + ); + } + return changeDescriptions; +} + +function getAllLoggedPrNumbers(changelog) { + const changeDescriptions = getAllChangeDescriptions(changelog); + + const prNumbersWithChangelogEntries = []; + for (const description of changeDescriptions) { + const matchResults = description.match(/^\[#(\d+)\]/u); + if (matchResults === null) { + continue; + } + const prNumber = matchResults[1]; + prNumbersWithChangelogEntries.push(prNumber); + } + + return prNumbersWithChangelogEntries; +} + +/** + * @typedef {import('./constants.js').Version} Version + */ + +/** + * Update a changelog with any commits made since the last release. Commits for + * PRs that are already included in the changelog are omitted. + * @param {Object} options + * @param {string} options.changelogContent - The current changelog + * @param {Version} options.currentVersion - The current version + * @param {string} options.repoUrl - The GitHub repository URL for the current + * project. + * @param {boolean} options.isReleaseCandidate - Denotes whether the current + * project is in the midst of release preparation or not. If this is set, any + * new changes are listed under the current release header. Otherwise, they + * are listed under the 'Unreleased' section. + * @returns + */ +async function updateChangelog({ + changelogContent, + currentVersion, + repoUrl, + isReleaseCandidate, +}) { + const changelog = parseChangelog({ changelogContent, repoUrl }); + + // Ensure we have all tags on remote + await runCommand('git', ['fetch', '--tags']); + const mostRecentTag = await getMostRecentTag(); + const commitsHashesSinceLastRelease = await runCommand('git', [ + 'rev-list', + `${mostRecentTag}..HEAD`, + ]); + const commits = await getCommits(commitsHashesSinceLastRelease); + + const loggedPrNumbers = getAllLoggedPrNumbers(changelog); + const newCommits = commits.filter( + ({ prNumber }) => !loggedPrNumbers.includes(prNumber), + ); + + const hasUnreleasedChanges = changelog.getUnreleasedChanges().length !== 0; + if ( + newCommits.length === 0 && + (!isReleaseCandidate || hasUnreleasedChanges) + ) { + return undefined; + } + + // Ensure release header exists, if necessary + if ( + isReleaseCandidate && + !changelog + .getReleases() + .find((release) => release.version === currentVersion) + ) { + changelog.addRelease({ version: currentVersion }); + } + + if (isReleaseCandidate && hasUnreleasedChanges) { + changelog.migrateUnreleasedChangesToRelease(currentVersion); + } + + const newChangeEntries = newCommits.map(({ prNumber, description }) => { + if (prNumber) { + const prefix = `[#${prNumber}](${repoUrl}/pull/${prNumber})`; + return `${prefix}: ${description}`; + } + return description; + }); + + for (const description of newChangeEntries.reverse()) { + changelog.addChange({ + version: isReleaseCandidate ? currentVersion : undefined, + category: changeCategories.Uncategorized, + description, + }); + } + + return changelog.toString(); +} + +module.exports = { updateChangelog }; 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/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index d4cda2e65..a1f6e3659 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -199,7 +199,7 @@ async function start() { body: JSON_PAYLOAD, headers: { 'User-Agent': 'metamaskbot', - Authorization: `token ${GITHUB_COMMENT_TOKEN}`, + 'Authorization': `token ${GITHUB_COMMENT_TOKEN}`, }, }); if (!response.ok) { 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/jest.config.js b/jest.config.js new file mode 100644 index 000000000..df0558fcf --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + restoreMocks: true, + coverageDirectory: 'jest-coverage/', + collectCoverageFrom: ['/ui/app/**/swaps/**'], + coveragePathIgnorePatterns: ['.stories.js', '.snap'], + coverageThreshold: { + global: { + branches: 32.75, + functions: 43.31, + lines: 43.12, + statements: 43.67, + }, + }, + setupFiles: ['./test/setup.js', './test/env.js'], + setupFilesAfterEnv: ['./test/jest/setup.js'], + testMatch: ['**/ui/**/?(*.)+(test).js'], +}; 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..56892f1bf 100644 --- a/lavamoat/node/policy.json +++ b/lavamoat/node/policy.json @@ -223,11 +223,6 @@ "js-tokens": true } }, - "@babel/parser": { - "globals": { - "BigInt": true - } - }, "@babel/plugin-proposal-async-generator-functions": { "packages": { "@babel/core": true, @@ -874,7 +869,6 @@ }, "acorn": { "globals": { - "BigInt": true, "define": true } }, @@ -884,9 +878,6 @@ } }, "acorn-node": { - "globals": { - "BigInt": true - }, "packages": { "acorn": true, "acorn-dynamic-import": true, @@ -1649,10 +1640,7 @@ "es-abstract": { "globals": { "AggregateError": true, - "Atomics": true, - "BigInt": true, "FinalizationRegistry": true, - "SharedArrayBuffer": true, "WeakRef": true }, "packages": { @@ -2026,8 +2014,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 +2200,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 +2256,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, @@ -2924,45 +2910,6 @@ "setTimeout": true } }, - "node-sass": { - "builtin": { - "fs.existsSync": true, - "fs.readFileSync": true, - "fs.readdirSync": true, - "os.EOL": true, - "path.delimiter": true, - "path.join": true, - "path.resolve": 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.platform": true, - "process.sass": "write", - "process.versions.modules": true - }, - "packages": { - "lodash": true, - "mkdirp": true, - "true-case-path": true - } - }, "normalize-path": { "packages": { "remove-trailing-separator": true @@ -2990,7 +2937,6 @@ "util.inspect": true }, "globals": { - "BigInt": true, "HTMLElement": true } }, @@ -3579,6 +3525,7 @@ "readline": true }, "globals": { + "Buffer": true, "HTMLElement": true, "InternalError": true, "TextDecoder": true, @@ -3597,6 +3544,7 @@ "navigator": true, "print": true, "process": true, + "setImmediate": true, "setTimeout": true, "version": true }, @@ -4066,18 +4014,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 diff --git a/package.json b/package.json index 50d265a9e..e42abd7d8 100644 --- a/package.json +++ b/package.json @@ -14,28 +14,32 @@ "benchmark:firefox": "SELENIUM_BROWSER=firefox node test/e2e/benchmark.js", "build:test": "yarn build test", "build:test:metrics": "SEGMENT_HOST='http://localhost:9090' SEGMENT_WRITE_KEY='FAKE' SEGMENT_LEGACY_WRITE_KEY='FAKE' yarn build test", - "test": "yarn test:unit && yarn lint", + "test": "yarn lint && yarn test:unit && yarn test:unit:jest", "dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080", "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'", "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 './{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:jest": "jest", + "test:unit:jest:watch": "jest --watch", + "test:unit:jest:watch:silent": "jest --watch --silent", + "test:unit:jest:ci": "jest --maxWorkers=2", + "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --recursive './{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", "test:e2e:firefox": "SELENIUM_BROWSER=firefox test/e2e/run-all.sh", "test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox mocha test/e2e/metrics.spec.js", "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", + "test:coverage:jest": "jest --coverage --maxWorkers=2", "test:coverage:strict": "nyc --check-coverage yarn test:unit:strict", "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,snap --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 +57,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", @@ -67,6 +72,7 @@ "**/redux/symbol-observable": "^2.0.3", "**/redux-devtools-instrument/symbol-observable": "^2.0.3", "**/rxjs/symbol-observable": "^2.0.3", + "**/xmlhttprequest-ssl": "^1.6.2", "3box/ipfs/ipld-zcash/zcash-bitcore-lib/lodash": "^4.17.19", "3box/ipfs/ipld-zcash/zcash-bitcore-lib/elliptic": "^6.5.4", "3box/**/libp2p-crypto/node-forge": "^0.10.0", @@ -84,7 +90,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,14 +114,13 @@ "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", "eth-json-rpc-middleware": "^6.0.0", - "eth-keyring-controller": "^6.1.0", + "eth-keyring-controller": "^6.2.0", "eth-method-registry": "^2.0.0", "eth-phishing-detect": "^1.1.14", "eth-query": "^2.1.2", @@ -125,7 +130,7 @@ "ethereum-ens-network-map": "^1.0.2", "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "1.3.7", - "ethereumjs-util": "5.1.0", + "ethereumjs-util": "^7.0.9", "ethereumjs-wallet": "^0.6.4", "ethers": "^5.0.8", "ethjs": "^0.4.0", @@ -134,7 +139,8 @@ "ethjs-query": "^0.3.4", "extension-port-stream": "^2.0.0", "extensionizer": "^1.0.1", - "fast-json-patch": "^2.0.4", + "fast-json-patch": "^2.2.1", + "fast-safe-stringify": "^2.0.7", "fuse.js": "^3.2.0", "globalthis": "^1.0.1", "human-standard-token-abi": "^2.0.0", @@ -142,10 +148,11 @@ "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", - "luxon": "^1.24.1", + "luxon": "^1.26.0", "nanoid": "^2.1.6", "nonce-tracker": "^1.0.0", "obj-multiplex": "^1.0.0", @@ -176,7 +183,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 +194,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", @@ -198,7 +205,10 @@ "@babel/preset-react": "^7.0.0", "@babel/register": "^7.5.5", "@lavamoat/allow-scripts": "^1.0.4", - "@metamask/eslint-config": "^5.0.0", + "@metamask/eslint-config": "^6.0.0", + "@metamask/eslint-config-jest": "^6.0.0", + "@metamask/eslint-config-mocha": "^6.0.0", + "@metamask/eslint-config-nodejs": "^6.0.0", "@metamask/forwarder": "^1.1.0", "@metamask/test-dapp": "^4.0.1", "@sentry/cli": "^1.58.0", @@ -209,6 +219,7 @@ "@storybook/core": "^6.1.17", "@storybook/react": "^6.1.17", "@storybook/storybook-deployer": "^2.8.7", + "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^3.2.1", "@types/react": "^16.9.53", @@ -222,16 +233,19 @@ "copy-webpack-plugin": "^6.0.3", "cross-spawn": "^7.0.3", "css-loader": "^2.1.1", + "css-to-xpath": "^0.1.0", "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-jest": "^24.3.4", + "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,28 +255,29 @@ "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", "gulp-watch": "^5.0.1", "gulp-zip": "^4.0.0", + "jest": "^26.6.3", "jsdom": "^11.2.0", "koa": "^2.7.0", - "lavamoat": "^5.1.4", - "lavamoat-viz": "^6.0.4", + "lavamoat": "^5.3.1", + "lavamoat-viz": "^6.0.9", "lockfile-lint": "^4.0.0", "loose-envify": "^1.4.0", "mocha": "^7.2.0", "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 +286,12 @@ "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", + "semver": "^7.3.5", "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 +311,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,8 +323,6 @@ "sha3": false, "bufferutil": false, "utf-8-validate": false, - "ejs": false, - "sc-uws": false, "leveldown": false, "ursa-optional": false, "gc-stats": false, diff --git a/patches/@formatjs+intl-utils+3.3.1.patch b/patches/@formatjs+intl-utils+3.3.1.patch new file mode 100644 index 000000000..9d27b1a05 --- /dev/null +++ b/patches/@formatjs+intl-utils+3.3.1.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@formatjs/intl-utils/dist/index.js b/node_modules/@formatjs/intl-utils/dist/index.js +index cb44944..4ec2d32 100644 +--- a/node_modules/@formatjs/intl-utils/dist/index.js ++++ b/node_modules/@formatjs/intl-utils/dist/index.js +@@ -25,7 +25,7 @@ exports.toRawFixed = polyfill_utils_1.toRawFixed; + exports.toRawPrecision = polyfill_utils_1.toRawPrecision; + exports.getMagnitude = polyfill_utils_1.getMagnitude; + exports.repeat = polyfill_utils_1.repeat; +-exports.hasOwnProperty = polyfill_utils_1.hasOwnProperty; ++Object.defineProperty(exports, 'hasOwnProperty', { value: polyfill_utils_1.hasOwnProperty }); + exports.isWellFormedUnitIdentifier = polyfill_utils_1.isWellFormedUnitIdentifier; + exports.defineProperty = polyfill_utils_1.defineProperty; + var resolve_locale_1 = require("./resolve-locale"); +diff --git a/node_modules/@formatjs/intl-utils/dist/polyfill-utils.js b/node_modules/@formatjs/intl-utils/dist/polyfill-utils.js +index 9306ef0..24859ac 100644 +--- a/node_modules/@formatjs/intl-utils/dist/polyfill-utils.js ++++ b/node_modules/@formatjs/intl-utils/dist/polyfill-utils.js +@@ -5,7 +5,7 @@ var units_1 = require("./units"); + function hasOwnProperty(o, key) { + return Object.prototype.hasOwnProperty.call(o, key); + } +-exports.hasOwnProperty = hasOwnProperty; ++Object.defineProperty(exports, 'hasOwnProperty', { value: hasOwnProperty }); + /** + * https://tc39.es/ecma262/#sec-toobject + * @param arg \ No newline at end of file diff --git a/patches/@reduxjs+toolkit+1.5.0.patch b/patches/@reduxjs+toolkit+1.5.0.patch new file mode 100644 index 000000000..ae707ae5e --- /dev/null +++ b/patches/@reduxjs+toolkit+1.5.0.patch @@ -0,0 +1,82 @@ +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..ce7bd95 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;n1?t-1:0),n=1;n3?t.i-4:t.i:Array.isArray(e)?1:f(e)?2:l(e)?3:0}function u(e,t){return 2===i(e)?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function a(e,t,r){var n=i(e);2===n?e.set(t,r):3===n?(e.delete(t),e.add(r)):e[t]=r}function c(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}function f(e){return W&&e instanceof Map}function l(e){return F&&e instanceof Set}function s(e){return e.o||e.t}function d(e){if(Array.isArray(e))return Array.prototype.slice.call(e);var t=L(e);delete t[U];for(var r=V(t),n=0;n1&&(e.set=e.add=e.clear=e.delete=y),Object.freeze(e),t&&o(e,(function(e,t){return p(t,!0)}),!0),e)}function y(){t(2)}function v(e){return null==e||"object"!=typeof e||Object.isFrozen(e)}function h(e){var r=B[e];return r||t(18,e),r}function b(){return M}function g(e,t){t&&(h("Patches"),e.u=[],e.s=[],e.v=t)}function m(e){O(e),e.p.forEach(j),e.p=null}function O(e){e===M&&(M=e.l)}function w(e){return M={p:[],l:M,h:e,m:!0,_:0}}function j(e){var t=e[U];0===t.i||1===t.i?t.j():t.g=!0}function A(e,r){r._=r.p.length;var o=r.p[0],i=void 0!==e&&e!==o;return r.h.O||h("ES5").S(r,e,i),i?(o[U].P&&(m(r),t(4)),n(e)&&(e=P(r,e),r.l||E(r,e)),r.u&&h("Patches").M(o[U],e,r.u,r.s)):e=P(r,o,[]),m(r),r.u&&r.v(r.u,r.s),e!==z?e:void 0}function P(e,t,r){if(v(t))return t;var n=t[U];if(!n)return o(t,(function(o,i){return S(e,n,t,o,i,r)}),!0),t;if(n.A!==e)return t;if(!n.P)return E(e,n.t,!0),n.t;if(!n.I){n.I=!0,n.A._--;var i=4===n.i||5===n.i?n.o=d(n.k):n.o;o(3===n.i?new Set(i):i,(function(t,o){return S(e,n,i,t,o,r)})),E(e,i,!1),r&&e.u&&h("Patches").R(n,r,e.u,e.s)}return n.o}function S(e,t,o,i,c,f){if(r(c)){var l=P(e,c,f&&t&&3!==t.i&&!u(t.D,i)?f.concat(i):void 0);if(a(o,i,l),!r(l))return;e.m=!1}if(n(c)&&!v(c)){if(!e.h.N&&e._<1)return;P(e,c),t&&t.A.l||E(e,c)}}function E(e,t,r){void 0===r&&(r=!1),e.h.N&&e.m&&p(t,r)}function x(e,t){var r=e[U];return(r?s(r):e)[t]}function I(e,t){if(t in e)for(var r=Object.getPrototypeOf(e);r;){var n=Object.getOwnPropertyDescriptor(r,t);if(n)return n;r=Object.getPrototypeOf(r)}}function _(e){e.P||(e.P=!0,e.l&&_(e.l))}function k(e){e.o||(e.o=d(e.t))}function R(e,t,r){var n=f(t)?h("MapSet").T(t,r):l(t)?h("MapSet").F(t,r):e.O?function(e,t){var r=Array.isArray(e),n={i:r?1:0,A:t?t.A:b(),P:!1,I:!1,D:{},l:t,t:e,k:null,o:null,j:null,C:!1},o=n,i=X;r&&(o=[n],i=Y);var u=Proxy.revocable(o,i),a=u.revoke,c=u.proxy;return n.k=c,n.j=a,c}(t,r):h("ES5").J(t,r);return(r?r.A:b()).p.push(n),n}function D(e){return r(e)||t(22,e),function e(t){if(!n(t))return t;var r,u=t[U],c=i(t);if(u){if(!u.P&&(u.i<4||!h("ES5").K(u)))return u.t;u.I=!0,r=N(t,c),u.I=!1}else r=N(t,c);return o(r,(function(t,n){u&&function(e,t){return 2===i(e)?e.get(t):e[t]}(u.t,t)===n||a(r,t,e(n))})),3===c?new Set(r):r}(e)}function N(e,t){switch(t){case 2:return new Map(e);case 3:return Array.from(e)}return d(e)}var T,M,C="undefined"!=typeof Symbol&&"symbol"==typeof Symbol("x"),W="undefined"!=typeof Map,F="undefined"!=typeof Set,q="undefined"!=typeof Proxy&&void 0!==Proxy.revocable&&"undefined"!=typeof Reflect,z=C?Symbol.for("immer-nothing"):((T={})["immer-nothing"]=!0,T),K=C?Symbol.for("immer-draftable"):"__$immer_draftable",U=C?Symbol.for("immer-state"):"__$immer_state",V="undefined"!=typeof Reflect&&Reflect.ownKeys?Reflect.ownKeys:void 0!==Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:Object.getOwnPropertyNames,L=Object.getOwnPropertyDescriptors||function(e){var t={};return V(e).forEach((function(r){t[r]=Object.getOwnPropertyDescriptor(e,r)})),t},B={},X={get:function(e,t){if(t===U)return e;var r=s(e);if(!u(r,t))return function(e,t,r){var n,o=I(t,r);return o?"value"in o?o.value:null===(n=o.get)||void 0===n?void 0:n.call(e.k):void 0}(e,r,t);var o=r[t];return e.I||!n(o)?o:o===x(e.t,t)?(k(e),e.o[t]=R(e.A.h,o,e)):o},has:function(e,t){return t in s(e)},ownKeys:function(e){return Reflect.ownKeys(s(e))},set:function(e,t,r){var n=I(s(e),t);if(null==n?void 0:n.set)return n.set.call(e.k,r),!0;if(!e.P){var o=x(s(e),t),i=null==o?void 0:o[U];if(i&&i.t===r)return e.o[t]=r,e.D[t]=!1,!0;if(c(r,o)&&(void 0!==r||u(e.t,t)))return!0;k(e),_(e)}return e.o[t]=r,e.D[t]=!0,!0},deleteProperty:function(e,t){return void 0!==x(e.t,t)||t in e.t?(e.D[t]=!1,k(e),_(e)):delete e.D[t],e.o&&delete e.o[t],!0},getOwnPropertyDescriptor:function(e,t){var r=s(e),n=Reflect.getOwnPropertyDescriptor(r,t);return n?{writable:!0,configurable:1!==e.i||"length"!==t,enumerable:n.enumerable,value:r[t]}:n},defineProperty:function(){t(11)},getPrototypeOf:function(e){return Object.getPrototypeOf(e.t)},setPrototypeOf:function(){t(12)}},Y={};o(X,(function(e,t){Y[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}})),Y.deleteProperty=function(e,t){return X.deleteProperty.call(this,e[0],t)},Y.set=function(e,t,r){return X.set.call(this,e[0],t,r,e[0])};var J=new(function(){function e(e){this.O=q,this.N=!0,"boolean"==typeof(null==e?void 0:e.useProxies)&&this.setUseProxies(e.useProxies),"boolean"==typeof(null==e?void 0:e.autoFreeze)&&this.setAutoFreeze(e.autoFreeze),this.produce=this.produce.bind(this),this.produceWithPatches=this.produceWithPatches.bind(this)}var o=e.prototype;return o.produce=function(e,r,o){if("function"==typeof e&&"function"!=typeof r){var i=r;r=e;var u=this;return function(e){var t=this;void 0===e&&(e=i);for(var n=arguments.length,o=Array(n>1?n-1:0),a=1;a1?r-1:0),i=1;i=0;n--){var o=t[n];if(0===o.path.length&&"replace"===o.op){e=o.value;break}}var i=h("Patches").$;return r(e)?i(e,t):this.produce(e,(function(e){return i(e,t.slice(n+1))}))},e}()),$=J.produce;J.produceWithPatches.bind(J),J.setAutoFreeze.bind(J),J.setUseProxies.bind(J),J.applyPatches.bind(J),J.createDraft.bind(J),J.finishDraft.bind(J);var G=function(e){var t,r=("undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof module?module:Function("return this")()).Symbol;return"function"==typeof r?r.observable?t=r.observable:(t=r("observable"),r.observable=t):t="@@observable",t}(),H=function(){return Math.random().toString(36).substring(7).split("").join(".")},Q={INIT:"@@redux/INIT"+H(),REPLACE:"@@redux/REPLACE"+H(),PROBE_UNKNOWN_ACTION:function(){return"@@redux/PROBE_UNKNOWN_ACTION"+H()}};function Z(e){if("object"!=typeof e||null===e)return!1;for(var t=e;null!==Object.getPrototypeOf(t);)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}function ee(e,t,r){var n;if("function"==typeof t&&"function"==typeof r||"function"==typeof r&&"function"==typeof arguments[3])throw new Error("It looks like you are passing several store enhancers to createStore(). This is not supported. Instead, compose them together to a single function.");if("function"==typeof t&&void 0===r&&(r=t,t=void 0),void 0!==r){if("function"!=typeof r)throw new Error("Expected the enhancer to be a function.");return r(ee)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var o=e,i=t,u=[],a=u,c=!1;function f(){a===u&&(a=u.slice())}function l(){if(c)throw new Error("You may not call store.getState() while the reducer is executing. The reducer has already received the state as an argument. Pass it down from the top reducer instead of reading it from the store.");return i}function s(e){if("function"!=typeof e)throw new Error("Expected the listener to be a function.");if(c)throw new Error("You may not call store.subscribe() while the reducer is executing. If you would like to be notified after the store has been updated, subscribe from a component and invoke store.getState() in the callback to access the latest state. See https://redux.js.org/api-reference/store#subscribelistener for more details.");var t=!0;return f(),a.push(e),function(){if(t){if(c)throw new Error("You may not unsubscribe from a store listener while the reducer is executing. See https://redux.js.org/api-reference/store#subscribelistener for more details.");t=!1,f();var r=a.indexOf(e);a.splice(r,1),u=null}}}function d(e){if(!Z(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(c)throw new Error("Reducers may not dispatch actions.");try{c=!0,i=o(i,e)}finally{c=!1}for(var t=u=a,r=0;r1?t-1:0),n=1;n1&&void 0!==arguments[1]?arguments[1]:fe,r=null,n=null;return function(){return le(t,r,arguments)||(n=e.apply(null,arguments)),r=arguments,n}})),pe=function(){var e=de.apply(void 0,arguments),t=function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),i=1;i-1}function _e(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 ke(e,t,o,i){void 0===o&&(o=[]);var u="function"==typeof t?_e(t):[t,o,i],a=u[0],c=u[1],f=u[2];return function(t,o){void 0===t&&(t=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,t){if(t){if(r(e)){var i=t(e,o);return void 0===i?e:i}if(n(e))return $(e,(function(e){return t(e,o)}));var u=t(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}),t)}}function Re(e){return function(t,n){var o=function(t){!function(e){return we(t=e)&&"string"==typeof t.type&&Object.keys(t).every(Ie);var t}(n)?e(n,t):e(n.payload,t)};return r(t)?(o(t),t):$(t,o)}}function De(e,t){return t(e)}function Ne(e){function t(t,r){var n=De(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:ye({},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=De(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 l;if(c){if(f>=a.length)break;l=a[f++]}else{if((f=a.next()).done)break;l=f.value}var s=l,d=De(s,e);d in n.entities?u.push({id:d,changes:s}):i.push(s)}o(u,n),r(i,n)}return{removeAll:(u=function(e){Object.assign(e,{ids:[],entities:{}})},a=Re((function(e,t){return u(t)})),function(e){return a(e,void 0)}),addOne:Re(t),addMany:Re(r),setAll:Re((function(e,t){Array.isArray(e)||(e=Object.values(e)),t.ids=[],t.entities={},r(e,t)})),updateOne:Re((function(e,t){return o([e],t)})),updateMany:Re(o),upsertOne:Re((function(e,t){return i([e],t)})),upsertMany:Re(i),removeOne:Re((function(e,t){return n([e],t)})),removeMany:Re(n)};var u,a}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator")));var Te=function(e){void 0===e&&(e=21);for(var t="",r=e;r--;)t+="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW"[64*Math.random()|0];return t},Me=["name","message","stack","code"],Ce=function(e){this.payload=e,this.name="RejectWithValue",this.message="Rejected"},We=function(e){if("object"==typeof e&&null!==e){var t={},r=Me,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)}},Fe=function(e,t){return function(e){return e&&"function"==typeof e.match}(e)?e.match(t):e(t)};function qe(){for(var e=arguments.length,t=new Array(e),r=0;r-1;return r&&n}function Ue(e){return"function"==typeof e[0]&&"pending"in e[0]&&"fulfilled"in e[0]&&"rejected"in e[0]}function Ve(){for(var e=arguments.length,t=new Array(e),r=0;r=0;t--){var r=e[t][U];if(!r.P)switch(r.i){case 5:i(r)&&_(r);break;case 4:n(r)&&_(r)}}}function n(e){for(var t=e.t,r=e.k,n=V(r),o=n.length-1;o>=0;o--){var i=n[o];if(i!==U){var a=t[i];if(void 0===a&&!u(t,i))return!0;var f=r[i],l=f&&f[U];if(l?l.t!==a:!c(f,a))return!0}}var s=!!t[U];return n.length!==V(t).length+(s?0:1)}function i(e){var t=e.k;if(t.length!==e.t.length)return!0;var r=Object.getOwnPropertyDescriptor(t,t.length-1);return!(!r||r.get)}var a={};!function(e,t){B.ES5||(B.ES5=t)}(0,{J:function(t,r){var n=Array.isArray(t),o=function(t,r){if(t){for(var n=Array(r.length),o=0;o=a.length)break;l=a[f++]}else{if((f=a.next()).done)break;l=f.value}var s=l,d=De(s,e);d in r.entities?u.push({id:d,changes:s}):i.push(s)}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,l=Array.isArray(f),s=0;for(f=l?f:f[Symbol.iterator]();;){var d;if(l){if(s>=f.length)break;d=f[s++]}else{if((s=f.next()).done)break;d=s.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},e.freeze=p,e.getDefaultMiddleware=Ee,e.getType=function(e){return""+e},e.isAllOf=ze,e.isAnyOf=qe,e.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 qe.apply(void 0,t)(e)}:e()(r[0])},e.isFulfilled=function e(){for(var t=arguments.length,r=new Array(t),n=0;n1?t-1:0),n=1;n3?t.i-4:t.i:Array.isArray(e)?1:f(e)?2:l(e)?3:0}function u(e,t){return 2===i(e)?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}function a(e,t,r){var n=i(e);2===n?e.set(t,r):3===n?(e.delete(t),e.add(r)):e[t]=r}function c(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}function f(e){return W&&e instanceof Map}function l(e){return F&&e instanceof Set}function s(e){return e.o||e.t}function d(e){if(Array.isArray(e))return Array.prototype.slice.call(e);var t=L(e);delete t[U];for(var r=V(t),n=0;n1&&(e.set=e.add=e.clear=e.delete=y),Object.freeze(e),t&&o(e,(function(e,t){return p(t,!0)}),!0),e)}function y(){t(2)}function v(e){return null==e||"object"!=typeof e||Object.isFrozen(e)}function h(e){var r=B[e];return r||t(18,e),r}function b(){return M}function g(e,t){t&&(h("Patches"),e.u=[],e.s=[],e.v=t)}function m(e){O(e),e.p.forEach(j),e.p=null}function O(e){e===M&&(M=e.l)}function w(e){return M={p:[],l:M,h:e,m:!0,_:0}}function j(e){var t=e[U];0===t.i||1===t.i?t.j():t.g=!0}function A(e,r){r._=r.p.length;var o=r.p[0],i=void 0!==e&&e!==o;return r.h.O||h("ES5").S(r,e,i),i?(o[U].P&&(m(r),t(4)),n(e)&&(e=P(r,e),r.l||E(r,e)),r.u&&h("Patches").M(o[U],e,r.u,r.s)):e=P(r,o,[]),m(r),r.u&&r.v(r.u,r.s),e!==z?e:void 0}function P(e,t,r){if(v(t))return t;var n=t[U];if(!n)return o(t,(function(o,i){return S(e,n,t,o,i,r)}),!0),t;if(n.A!==e)return t;if(!n.P)return E(e,n.t,!0),n.t;if(!n.I){n.I=!0,n.A._--;var i=4===n.i||5===n.i?n.o=d(n.k):n.o;o(3===n.i?new Set(i):i,(function(t,o){return S(e,n,i,t,o,r)})),E(e,i,!1),r&&e.u&&h("Patches").R(n,r,e.u,e.s)}return n.o}function S(e,t,o,i,c,f){if(r(c)){var l=P(e,c,f&&t&&3!==t.i&&!u(t.D,i)?f.concat(i):void 0);if(a(o,i,l),!r(l))return;e.m=!1}if(n(c)&&!v(c)){if(!e.h.N&&e._<1)return;P(e,c),t&&t.A.l||E(e,c)}}function E(e,t,r){void 0===r&&(r=!1),e.h.N&&e.m&&p(t,r)}function x(e,t){var r=e[U];return(r?s(r):e)[t]}function I(e,t){if(t in e)for(var r=Object.getPrototypeOf(e);r;){var n=Object.getOwnPropertyDescriptor(r,t);if(n)return n;r=Object.getPrototypeOf(r)}}function _(e){e.P||(e.P=!0,e.l&&_(e.l))}function k(e){e.o||(e.o=d(e.t))}function R(e,t,r){var n=f(t)?h("MapSet").T(t,r):l(t)?h("MapSet").F(t,r):e.O?function(e,t){var r=Array.isArray(e),n={i:r?1:0,A:t?t.A:b(),P:!1,I:!1,D:{},l:t,t:e,k:null,o:null,j:null,C:!1},o=n,i=X;r&&(o=[n],i=Y);var u=Proxy.revocable(o,i),a=u.revoke,c=u.proxy;return n.k=c,n.j=a,c}(t,r):h("ES5").J(t,r);return(r?r.A:b()).p.push(n),n}function D(e){return r(e)||t(22,e),function e(t){if(!n(t))return t;var r,u=t[U],c=i(t);if(u){if(!u.P&&(u.i<4||!h("ES5").K(u)))return u.t;u.I=!0,r=N(t,c),u.I=!1}else r=N(t,c);return o(r,(function(t,n){u&&function(e,t){return 2===i(e)?e.get(t):e[t]}(u.t,t)===n||a(r,t,e(n))})),3===c?new Set(r):r}(e)}function N(e,t){switch(t){case 2:return new Map(e);case 3:return Array.from(e)}return d(e)}var T,M,C="undefined"!=typeof Symbol&&"symbol"==typeof Symbol("x"),W="undefined"!=typeof Map,F="undefined"!=typeof Set,q="undefined"!=typeof Proxy&&void 0!==Proxy.revocable&&"undefined"!=typeof Reflect,z=C?Symbol.for("immer-nothing"):((T={})["immer-nothing"]=!0,T),K=C?Symbol.for("immer-draftable"):"__$immer_draftable",U=C?Symbol.for("immer-state"):"__$immer_state",V="undefined"!=typeof Reflect&&Reflect.ownKeys?Reflect.ownKeys:void 0!==Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:Object.getOwnPropertyNames,L=Object.getOwnPropertyDescriptors||function(e){var t={};return V(e).forEach((function(r){t[r]=Object.getOwnPropertyDescriptor(e,r)})),t},B={},X={get:function(e,t){if(t===U)return e;var r=s(e);if(!u(r,t))return function(e,t,r){var n,o=I(t,r);return o?"value"in o?o.value:null===(n=o.get)||void 0===n?void 0:n.call(e.k):void 0}(e,r,t);var o=r[t];return e.I||!n(o)?o:o===x(e.t,t)?(k(e),e.o[t]=R(e.A.h,o,e)):o},has:function(e,t){return t in s(e)},ownKeys:function(e){return Reflect.ownKeys(s(e))},set:function(e,t,r){var n=I(s(e),t);if(null==n?void 0:n.set)return n.set.call(e.k,r),!0;if(!e.P){var o=x(s(e),t),i=null==o?void 0:o[U];if(i&&i.t===r)return e.o[t]=r,e.D[t]=!1,!0;if(c(r,o)&&(void 0!==r||u(e.t,t)))return!0;k(e),_(e)}return e.o[t]=r,e.D[t]=!0,!0},deleteProperty:function(e,t){return void 0!==x(e.t,t)||t in e.t?(e.D[t]=!1,k(e),_(e)):delete e.D[t],e.o&&delete e.o[t],!0},getOwnPropertyDescriptor:function(e,t){var r=s(e),n=Reflect.getOwnPropertyDescriptor(r,t);return n?{writable:!0,configurable:1!==e.i||"length"!==t,enumerable:n.enumerable,value:r[t]}:n},defineProperty:function(){t(11)},getPrototypeOf:function(e){return Object.getPrototypeOf(e.t)},setPrototypeOf:function(){t(12)}},Y={};o(X,(function(e,t){Y[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}})),Y.deleteProperty=function(e,t){return X.deleteProperty.call(this,e[0],t)},Y.set=function(e,t,r){return X.set.call(this,e[0],t,r,e[0])};var J=new(function(){function e(e){this.O=q,this.N=!0,"boolean"==typeof(null==e?void 0:e.useProxies)&&this.setUseProxies(e.useProxies),"boolean"==typeof(null==e?void 0:e.autoFreeze)&&this.setAutoFreeze(e.autoFreeze),this.produce=this.produce.bind(this),this.produceWithPatches=this.produceWithPatches.bind(this)}var o=e.prototype;return o.produce=function(e,r,o){if("function"==typeof e&&"function"!=typeof r){var i=r;r=e;var u=this;return function(e){var t=this;void 0===e&&(e=i);for(var n=arguments.length,o=Array(n>1?n-1:0),a=1;a1?r-1:0),i=1;i=0;n--){var o=t[n];if(0===o.path.length&&"replace"===o.op){e=o.value;break}}var i=h("Patches").$;return r(e)?i(e,t):this.produce(e,(function(e){return i(e,t.slice(n+1))}))},e}()),$=J.produce;J.produceWithPatches.bind(J),J.setAutoFreeze.bind(J),J.setUseProxies.bind(J),J.applyPatches.bind(J),J.createDraft.bind(J),J.finishDraft.bind(J);var G=function(e){var t,r=("undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof module?module:Function("return this")()).Symbol;return"function"==typeof r?r.observable?t=r.observable:(t=r("observable"),r.observable=t):t="@@observable",t}(),H=function(){return Math.random().toString(36).substring(7).split("").join(".")},Q={INIT:"@@redux/INIT"+H(),REPLACE:"@@redux/REPLACE"+H(),PROBE_UNKNOWN_ACTION:function(){return"@@redux/PROBE_UNKNOWN_ACTION"+H()}};function Z(e){if("object"!=typeof e||null===e)return!1;for(var t=e;null!==Object.getPrototypeOf(t);)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}function ee(e,t,r){var n;if("function"==typeof t&&"function"==typeof r||"function"==typeof r&&"function"==typeof arguments[3])throw new Error("It looks like you are passing several store enhancers to createStore(). This is not supported. Instead, compose them together to a single function.");if("function"==typeof t&&void 0===r&&(r=t,t=void 0),void 0!==r){if("function"!=typeof r)throw new Error("Expected the enhancer to be a function.");return r(ee)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var o=e,i=t,u=[],a=u,c=!1;function f(){a===u&&(a=u.slice())}function l(){if(c)throw new Error("You may not call store.getState() while the reducer is executing. The reducer has already received the state as an argument. Pass it down from the top reducer instead of reading it from the store.");return i}function s(e){if("function"!=typeof e)throw new Error("Expected the listener to be a function.");if(c)throw new Error("You may not call store.subscribe() while the reducer is executing. If you would like to be notified after the store has been updated, subscribe from a component and invoke store.getState() in the callback to access the latest state. See https://redux.js.org/api-reference/store#subscribelistener for more details.");var t=!0;return f(),a.push(e),function(){if(t){if(c)throw new Error("You may not unsubscribe from a store listener while the reducer is executing. See https://redux.js.org/api-reference/store#subscribelistener for more details.");t=!1,f();var r=a.indexOf(e);a.splice(r,1),u=null}}}function d(e){if(!Z(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(c)throw new Error("Reducers may not dispatch actions.");try{c=!0,i=o(i,e)}finally{c=!1}for(var t=u=a,r=0;r1?t-1:0),n=1;n1&&void 0!==arguments[1]?arguments[1]:fe,r=null,n=null;return function(){return le(t,r,arguments)||(n=e.apply(null,arguments)),r=arguments,n}})),pe=function(){var e=de.apply(void 0,arguments),t=function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),i=1;i-1}function _e(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 ke(e,t,o,i){void 0===o&&(o=[]);var u="function"==typeof t?_e(t):[t,o,i],a=u[0],c=u[1],f=u[2];return function(t,o){void 0===t&&(t=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,t){if(t){if(r(e)){var i=t(e,o);return void 0===i?e:i}if(n(e))return $(e,(function(e){return t(e,o)}));var u=t(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}),t)}}function Re(e){return function(t,n){var o=function(t){!function(e){return we(t=e)&&"string"==typeof t.type&&Object.keys(t).every(Ie);var t}(n)?e(n,t):e(n.payload,t)};return r(t)?(o(t),t):$(t,o)}}function De(e,t){return t(e)}function Ne(e){function t(t,r){var n=De(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:ye({},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=De(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 l;if(c){if(f>=a.length)break;l=a[f++]}else{if((f=a.next()).done)break;l=f.value}var s=l,d=De(s,e);d in n.entities?u.push({id:d,changes:s}):i.push(s)}o(u,n),r(i,n)}return{removeAll:(u=function(e){Object.assign(e,{ids:[],entities:{}})},a=Re((function(e,t){return u(t)})),function(e){return a(e,void 0)}),addOne:Re(t),addMany:Re(r),setAll:Re((function(e,t){Array.isArray(e)||(e=Object.values(e)),t.ids=[],t.entities={},r(e,t)})),updateOne:Re((function(e,t){return o([e],t)})),updateMany:Re(o),upsertOne:Re((function(e,t){return i([e],t)})),upsertMany:Re(i),removeOne:Re((function(e,t){return n([e],t)})),removeMany:Re(n)};var u,a}"undefined"!=typeof Symbol&&(Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator"))),"undefined"!=typeof Symbol&&(Symbol.asyncIterator||(Symbol.asyncIterator=Symbol("Symbol.asyncIterator")));var Te=function(e){void 0===e&&(e=21);for(var t="",r=e;r--;)t+="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW"[64*Math.random()|0];return t},Me=["name","message","stack","code"],Ce=function(e){this.payload=e,this.name="RejectWithValue",this.message="Rejected"},We=function(e){if("object"==typeof e&&null!==e){var t={},r=Me,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)}},Fe=function(e,t){return function(e){return e&&"function"==typeof e.match}(e)?e.match(t):e(t)};function qe(){for(var e=arguments.length,t=new Array(e),r=0;r-1;return r&&n}function Ue(e){return"function"==typeof e[0]&&"pending"in e[0]&&"fulfilled"in e[0]&&"rejected"in e[0]}function Ve(){for(var e=arguments.length,t=new Array(e),r=0;r=0;t--){var r=e[t][U];if(!r.P)switch(r.i){case 5:i(r)&&_(r);break;case 4:n(r)&&_(r)}}}function n(e){for(var t=e.t,r=e.k,n=V(r),o=n.length-1;o>=0;o--){var i=n[o];if(i!==U){var a=t[i];if(void 0===a&&!u(t,i))return!0;var f=r[i],l=f&&f[U];if(l?l.t!==a:!c(f,a))return!0}}var s=!!t[U];return n.length!==V(t).length+(s?0:1)}function i(e){var t=e.k;if(t.length!==e.t.length)return!0;var r=Object.getOwnPropertyDescriptor(t,t.length-1);return!(!r||r.get)}var a={};!function(e,t){B.ES5||(B.ES5=t)}(0,{J:function(t,r){var n=Array.isArray(t),o=function(t,r){if(t){for(var n=Array(r.length),o=0;o=a.length)break;l=a[f++]}else{if((f=a.next()).done)break;l=f.value}var s=l,d=De(s,e);d in r.entities?u.push({id:d,changes:s}):i.push(s)}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,l=Array.isArray(f),s=0;for(f=l?f:f[Symbol.iterator]();;){var d;if(l){if(s>=f.length)break;d=f[s++]}else{if((s=f.next()).done)break;d=s.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},e.freeze=p,e.getDefaultMiddleware=Ee,e.getType=function(e){return""+e},e.isAllOf=ze,e.isAnyOf=qe,e.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 qe.apply(void 0,t)(e)}:e()(r[0])},e.isFulfilled=function e(){for(var t=arguments.length,r=new Array(t),n=0;n 1) { + obj[name[0]] = obj[name[0]] || {}; +diff --git a/node_modules/web3/lib/web3/function.js b/node_modules/web3/lib/web3/function.js +index 863a10a..ffcd23c 100644 +--- a/node_modules/web3/lib/web3/function.js ++++ b/node_modules/web3/lib/web3/function.js +@@ -269,7 +269,7 @@ SolidityFunction.prototype.execute = function () { + SolidityFunction.prototype.attachToContract = function (contract) { + var execute = this.execute.bind(this); + execute.request = this.request.bind(this); +- execute.call = this.call.bind(this); ++ Reflect.defineProperty(execute, 'call', this.call.bind(this)); + execute.sendTransaction = this.sendTransaction.bind(this); + execute.estimateGas = this.estimateGas.bind(this); + execute.getData = this.getData.bind(this); +diff --git a/node_modules/web3/lib/web3/method.js b/node_modules/web3/lib/web3/method.js +index 2e3c796..be0b663 100644 +--- a/node_modules/web3/lib/web3/method.js ++++ b/node_modules/web3/lib/web3/method.js +@@ -123,7 +123,7 @@ Method.prototype.toPayload = function (args) { + + Method.prototype.attachToObject = function (obj) { + var func = this.buildCall(); +- func.call = this.call; // TODO!!! that's ugly. filter.js uses it ++ Reflect.defineProperty(func, 'call', { value: this.call }) + var name = this.name.split('.'); + if (name.length > 1) { + obj[name[0]] = obj[name[0]] || {}; \ No newline at end of file diff --git a/shared/constants/network.js b/shared/constants/network.js index 8160260e6..a9bfa10ae 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -10,12 +10,15 @@ export const ROPSTEN_NETWORK_ID = '3'; export const RINKEBY_NETWORK_ID = '4'; export const GOERLI_NETWORK_ID = '5'; export const KOVAN_NETWORK_ID = '42'; +export const LOCALHOST_NETWORK_ID = '1337'; export const MAINNET_CHAIN_ID = '0x1'; export const ROPSTEN_CHAIN_ID = '0x3'; export const RINKEBY_CHAIN_ID = '0x4'; export const GOERLI_CHAIN_ID = '0x5'; export const KOVAN_CHAIN_ID = '0x2a'; +export const LOCALHOST_CHAIN_ID = '0x539'; +export const BSC_CHAIN_ID = '0x38'; /** * The largest possible chain ID we can handle. @@ -30,6 +33,7 @@ export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; export const GOERLI_DISPLAY_NAME = 'Goerli'; export const ETH_SYMBOL = 'ETH'; +export const WETH_SYMBOL = 'WETH'; export const TEST_ETH_SYMBOL = 'TESTETH'; export const BNB_SYMBOL = 'BNB'; diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index 186df2774..3f345542b 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -5,6 +5,7 @@ import { BNB_SYMBOL, TEST_ETH_TOKEN_IMAGE_URL, BNB_TOKEN_IMAGE_URL, + BSC_CHAIN_ID, } from './network'; export const QUOTES_EXPIRED_ERROR = 'quotes-expired'; @@ -50,10 +51,12 @@ const TESTNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31'; +export const ETH_WETH_CONTRACT_ADDRESS = + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network'; -const METASWAP_BNB_API_HOST = 'https://bsc-api.metaswap.codefi.network'; -export const BNB_CHAIN_ID = '0x38'; +const METASWAP_BSC_API_HOST = 'https://bsc-api.metaswap.codefi.network'; const SWAPS_TESTNET_CHAIN_ID = '0x539'; const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network'; @@ -63,27 +66,27 @@ const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; export const ALLOWED_SWAPS_CHAIN_IDS = { [MAINNET_CHAIN_ID]: true, [SWAPS_TESTNET_CHAIN_ID]: true, - [BNB_CHAIN_ID]: true, + [BSC_CHAIN_ID]: true, }; export const METASWAP_CHAINID_API_HOST_MAP = { [MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST, [SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST, - [BNB_CHAIN_ID]: METASWAP_BNB_API_HOST, + [BSC_CHAIN_ID]: METASWAP_BSC_API_HOST, }; export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = { [MAINNET_CHAIN_ID]: MAINNET_CONTRACT_ADDRESS, [SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS, - [BNB_CHAIN_ID]: BSC_CONTRACT_ADDRESS, + [BSC_CHAIN_ID]: BSC_CONTRACT_ADDRESS, }; export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [MAINNET_CHAIN_ID]: ETH_SWAPS_TOKEN_OBJECT, [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, - [BNB_CHAIN_ID]: BNB_SWAPS_TOKEN_OBJECT, + [BSC_CHAIN_ID]: BNB_SWAPS_TOKEN_OBJECT, }; export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = { - [BNB_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, + [BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, }; diff --git a/shared/constants/transaction.js b/shared/constants/transaction.js index 833463a60..44a777a94 100644 --- a/shared/constants/transaction.js +++ b/shared/constants/transaction.js @@ -1,8 +1,26 @@ /** * Transaction Type is a MetaMask construct used internally * @typedef {Object} TransactionTypes - * @property {'standard'} STANDARD - A standard transaction, usually the first with - * a given nonce + * @property {'transfer'} TOKEN_METHOD_TRANSFER - A token transaction where the user + * is sending tokens that they own to another address + * @property {'transferfrom'} TOKEN_METHOD_TRANSFER_FROM - A token transaction + * transferring tokens from an account that the sender has an allowance of. + * For more information on allowances, see the approve type. + * @property {'approve'} TOKEN_METHOD_APPROVE - A token transaction requesting an + * allowance of the token to spend on behalf of the user + * @property {'incoming'} INCOMING - An incoming (deposit) transaction + * @property {'sentEther'} SENT_ETHER - A transaction sending ether to a recipient + * @property {'contractInteraction'} CONTRACT_INTERACTION - A transaction that is + * interacting with a smart contract's methods that we have not treated as a special + * case, such as approve, transfer, and transferfrom + * @property {'contractDeployment'} DEPLOY_CONTRACT - A transaction that deployed + * a smart contract + * @property {'swap'} SWAP - A transaction swapping one token for another through + * MetaMask Swaps + * @property {'swapApproval'} SWAP_APPROVAL - Similar to the approve type, a swap + * approval is a special case of ERC20 approve method that requests an allowance of + * the token to spend on behalf of the user for the MetaMask Swaps contract. The first + * swap for any token will have an accompanying swapApproval transaction. * @property {'cancel'} CANCEL - A transaction submitted with the same nonce as a * previous transaction, a higher gas price and a zeroed out send amount. Useful * for users who accidentally send to erroneous addresses or if they send too much. @@ -12,13 +30,27 @@ * the same nonce and higher gas fees. */ +/** + * This type will work anywhere you expect a string that can be one of the + * above transaction types. + * @typedef {TransactionTypes[keyof TransactionTypes]} TransactionTypeString + */ + /** * @type {TransactionTypes} */ export const TRANSACTION_TYPES = { - STANDARD: 'standard', CANCEL: 'cancel', RETRY: 'retry', + TOKEN_METHOD_TRANSFER: 'transfer', + TOKEN_METHOD_TRANSFER_FROM: 'transferfrom', + TOKEN_METHOD_APPROVE: 'approve', + INCOMING: 'incoming', + SENT_ETHER: 'sentEther', + CONTRACT_INTERACTION: 'contractInteraction', + DEPLOY_CONTRACT: 'contractDeployment', + SWAP: 'swap', + SWAP_APPROVAL: 'swapApproval', }; /** @@ -39,6 +71,12 @@ export const TRANSACTION_TYPES = { * @property {'confirmed'} CONFIRMED - The transaction was confirmed by the network */ +/** + * This type will work anywhere you expect a string that can be one of the + * above transaction statuses. + * @typedef {TransactionStatuses[keyof TransactionStatuses]} TransactionStatusString + */ + /** * @type {TransactionStatuses} */ @@ -53,45 +91,6 @@ export const TRANSACTION_STATUSES = { CONFIRMED: 'confirmed', }; -/** - * @typedef {Object} TransactionCategories - * @property {'transfer'} TOKEN_METHOD_TRANSFER - A token transaction where the user - * is sending tokens that they own to another address - * @property {'transferfrom'} TOKEN_METHOD_TRANSFER_FROM - A token transaction - * transferring tokens from an account that the sender has an allowance of. - * For more information on allowances, see the approve category. - * @property {'approve'} TOKEN_METHOD_APPROVE - A token transaction requesting an - * allowance of the token to spend on behalf of the user - * @property {'incoming'} INCOMING - An incoming (deposit) transaction - * @property {'sentEther'} SENT_ETHER - A transaction sending ether to a recipient - * @property {'contractInteraction'} CONTRACT_INTERACTION - A transaction that is - * interacting with a smart contract's methods that we have not treated as a special - * case, such as approve, transfer, and transferfrom - * @property {'contractDeployment'} DEPLOY_CONTRACT - A transaction that deployed - * a smart contract - * @property {'swap'} SWAP - A transaction swapping one token for another through - * MetaMask Swaps - * @property {'swapApproval'} SWAP_APPROVAL - Similar to the approve category, a swap - * approval is a special case of ERC20 approve method that requests an allowance of - * the token to spend on behalf of the user for the MetaMask Swaps contract. The first - * swap for any token will have an accompanying swapApproval transaction. - */ - -/** - * @type {TransactionCategories} - */ -export const TRANSACTION_CATEGORIES = { - TOKEN_METHOD_TRANSFER: 'transfer', - TOKEN_METHOD_TRANSFER_FROM: 'transferfrom', - TOKEN_METHOD_APPROVE: 'approve', - INCOMING: 'incoming', - SENT_ETHER: 'sentEther', - CONTRACT_INTERACTION: 'contractInteraction', - DEPLOY_CONTRACT: 'contractDeployment', - SWAP: 'swap', - SWAP_APPROVAL: 'swapApproval', -}; - /** * Transaction Group Status is a MetaMask construct to track the status of groups * of transactions. @@ -145,3 +144,54 @@ export const TRANSACTION_GROUP_CATEGORIES = { SIGNATURE_REQUEST: 'signature-request', SWAP: 'swap', }; + +/** + * @typedef {Object} TxParams + * @property {string} from - The address the transaction is sent from + * @property {string} to - The address the transaction is sent to + * @property {string} value - The amount of wei, in hexadecimal, to send + * @property {number} nonce - The transaction count for the current account/network + * @property {string} gasPrice - The amount of gwei, in hexadecimal, per unit of gas + * @property {string} gas - The max amount of gwei, in hexadecimal, the user is willing to pay + * @property {string} [data] - Hexadecimal encoded string representing calls to the EVM's ABI + */ + +/** + * @typedef {Object} TxError + * @property {string} message - The message from the encountered error. + * @property {any} rpc - The "value" of the error. + * @property {string} [stack] - the stack trace from the error, if available. + */ + +/** + * An object representing a transaction, in whatever state it is in. + * @typedef {Object} TransactionMeta + * + * @property {string} [blockNumber] - The block number this transaction was + * included in. Currently only present on incoming transactions! + * @property {number} id - An internally unique tx identifier. + * @property {number} time - Time the transaction was first suggested, in unix + * epoch time (ms). + * @property {TransactionTypeString} type - The type of transaction this txMeta + * represents. + * @property {TransactionStatusString} status - The current status of the + * transaction. + * @property {string} metamaskNetworkId - The transaction's network ID, used + * for EIP-155 compliance. + * @property {boolean} loadingDefaults - TODO: Document + * @property {TxParams} txParams - The transaction 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). + * @property {TxError} [err] - The error encountered during the transaction + */ diff --git a/test/unit/app/fetch-with-timeout.test.js b/shared/modules/fetch-with-timeout.test.js similarity index 95% rename from test/unit/app/fetch-with-timeout.test.js rename to shared/modules/fetch-with-timeout.test.js index 9a8aae403..a7b1daa1a 100644 --- a/test/unit/app/fetch-with-timeout.test.js +++ b/shared/modules/fetch-with-timeout.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import nock from 'nock'; -import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; +import getFetchWithTimeout from './fetch-with-timeout'; describe('getFetchWithTimeout', function () { it('fetches a url', async function () { diff --git a/app/scripts/lib/random-id.js b/shared/modules/random-id.js similarity index 100% rename from app/scripts/lib/random-id.js rename to shared/modules/random-id.js diff --git a/shared/notifications/index.js b/shared/notifications/index.js new file mode 100644 index 000000000..212afddbf --- /dev/null +++ b/shared/notifications/index.js @@ -0,0 +1,52 @@ +// Messages and descriptions for these locale keys are in app/_locales/en/messages.json +export const UI_NOTIFICATIONS = { + 1: { + id: 1, + date: '2021-03-17', + image: { + src: 'images/mobile-link-qr.svg', + height: '230px', + width: '230px', + placeImageBelowDescription: true, + }, + }, + 2: { + id: 2, + date: '2020-08-31', + }, + 3: { + id: 3, + date: '2021-03-8', + }, +}; + +export const getTranslatedUINoficiations = (t, locale) => { + return { + 1: { + ...UI_NOTIFICATIONS[1], + title: t('notifications1Title'), + description: t('notifications1Description'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[1].date), + ), + }, + 2: { + ...UI_NOTIFICATIONS[2], + title: t('notifications2Title'), + description: t('notifications2Description'), + actionText: t('notifications2ActionText'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[2].date), + ), + }, + 3: { + ...UI_NOTIFICATIONS[3], + title: t('notifications3Title'), + description: t('notifications3Description'), + actionText: t('notifications3ActionText'), + date: new Intl.DateTimeFormat(locale).format( + new Date(UI_NOTIFICATIONS[3].date), + ), + }, + }; +}; diff --git a/stylelint.config.js b/stylelint.config.js index b27e13ec9..04b62427e 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -73,7 +73,7 @@ module.exports = { 'function-parentheses-newline-inside': 'always-multi-line', 'function-parentheses-space-inside': 'never-single-line', 'function-whitespace-after': 'always', - indentation: 2, + 'indentation': 2, 'length-zero-no-unit': true, // 'max-empty-lines': 1, 'media-feature-colon-space-after': 'always', diff --git a/test/data/transaction-data.json b/test/data/transaction-data.json index e5ae1f5b8..37e7659dd 100644 --- a/test/data/transaction-data.json +++ b/test/data/transaction-data.json @@ -15,9 +15,8 @@ "gas": "0x5208", "gasPrice": "0x2540be400" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 12, @@ -76,9 +75,8 @@ "gas": "0x5208", "gasPrice": "0x2540be400" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 12, @@ -142,9 +140,8 @@ "gas": "0x5208", "gasPrice": "0x2540be400" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -203,9 +200,8 @@ "gas": "0x5208", "gasPrice": "0x2540be400" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -269,9 +265,8 @@ "gas": "0x5208", "gasPrice": "0x306dc4200" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -331,9 +326,8 @@ "gas": "0x5208", "gasPrice": "0x306dc4200" }, - "type": "standard", "origin": "metamask", - "transactionCategory": "sentEther", + "type": "sentEther", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -398,7 +392,7 @@ "value": "0x1043561a882930000" }, "hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116", - "transactionCategory": "incoming" + "type": "incoming" }, "primaryTransaction": { "blockNumber": "6477257", @@ -415,7 +409,7 @@ "value": "0x1043561a882930000" }, "hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116", - "transactionCategory": "incoming" + "type": "incoming" }, "hasRetried": false, "hasCancelled": false @@ -436,7 +430,7 @@ "value": "0x0" }, "hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889", - "transactionCategory": "incoming" + "type": "incoming" }, "primaryTransaction": { "blockNumber": "6454493", @@ -453,7 +447,7 @@ "value": "0x0" }, "hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889", - "transactionCategory": "incoming" + "type": "incoming" }, "hasRetried": false, "hasCancelled": false @@ -474,7 +468,7 @@ "value": "0xde0b6b3a7640000" }, "hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", - "transactionCategory": "incoming" + "type": "incoming" }, "primaryTransaction": { "blockNumber": "6195526", @@ -491,7 +485,7 @@ "value": "0xde0b6b3a7640000" }, "hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", - "transactionCategory": "incoming" + "type": "incoming" }, "hasRetried": false, "hasCancelled": false @@ -514,7 +508,7 @@ "hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", "destinationTokenSymbol": "ABC", "sourceTokenSymbol": "ETH", - "transactionCategory": "swap" + "type": "swap" }, "primaryTransaction": { "blockNumber": "6195527", @@ -531,7 +525,7 @@ "value": "0xde0b6b3a7640000" }, "hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", - "transactionCategory": "swap", + "type": "swap", "destinationTokenSymbol": "ABC", "destinationTokenAddress": "0xabca64466f257793eaa52fcfff5066894b76a149", "sourceTokenSymbol": "ETH" diff --git a/test/e2e/address-book.spec.js b/test/e2e/address-book.spec.js deleted file mode 100644 index 4ac0d1722..000000000 --- a/test/e2e/address-book.spec.js +++ /dev/null @@ -1,323 +0,0 @@ -const assert = require('assert'); -const { By, until } = require('selenium-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('MetaMask', 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(); - 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', 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/benchmark.js b/test/e2e/benchmark.js index 2f02d93cc..9c3b7db00 100644 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -3,7 +3,6 @@ const path = require('path'); const { promises: fs, constants: fsConstants } = require('fs'); const ttest = require('ttest'); -const { By, Key } = require('selenium-webdriver'); const { withFixtures } = require('./helpers'); const { PAGES } = require('./webdriver/driver'); @@ -14,10 +13,9 @@ async function measurePage(pageName) { let metrics; await withFixtures({ fixtures: 'imported-account' }, 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.findElement(By.css('.selected-account__name')); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + await driver.findElement('.selected-account__name'); await driver.navigate(pageName); await driver.delay(1000); metrics = await driver.collectMetrics(); 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..e53e3a922 --- /dev/null +++ b/test/e2e/fixtures/address-entry/state.json @@ -0,0 +1,149 @@ +{ + "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" + } + }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, + "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 94% rename from test/e2e/fixtures/personal-sign/state.json rename to test/e2e/fixtures/connected-state/state.json index dcf4624c8..a88a31a74 100644 --- a/test/e2e/fixtures/personal-sign/state.json +++ b/test/e2e/fixtures/connected-state/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "connectedStatusPopoverHasBeenShown": false, - "swapsWelcomeMessageHasBeenShown": true + "connectedStatusPopoverHasBeenShown": false }, "CachedBalancesController": { "cachedBalances": { @@ -41,6 +40,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/import-ui/state.json b/test/e2e/fixtures/import-ui/state.json new file mode 100644 index 000000000..350ed8848 --- /dev/null +++ b/test/e2e/fixtures/import-ui/state.json @@ -0,0 +1,251 @@ +{ + "data": { + "config": {}, + "PreferencesController": { + "frequentRpcListDetail": [ + { + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "nickname": "Localhost 8545", + "rpcPrefs": {} + } + ], + "accountTokens": { + "0x0cc5261ab8ce458dc977078a3623e2badd27afd3": { + "0x539": [] + }, + "0x3ed0ee22e0685ebbf07b2360a8331693c413cc59": {}, + "0xd38d853771fb546bd8b18b2f3638491bc0b0e906": { + "0x539": [] + } + }, + "accountHiddenTokens": { + "0x0cc5261ab8ce458dc977078a3623e2badd27afd3": { + "0x539": [] + }, + "0x3ed0ee22e0685ebbf07b2360a8331693c413cc59": {}, + "0xd38d853771fb546bd8b18b2f3638491bc0b0e906": { + "0x539": [] + } + }, + "assetImages": {}, + "tokens": [], + "hiddenTokens": [], + "suggestedTokens": {}, + "useBlockie": false, + "useNonceField": false, + "usePhishDetect": true, + "featureFlags": { + "showIncomingTransactions": true + }, + "knownMethodData": {}, + "firstTimeFlowType": "import", + "currentLocale": "en", + "identities": { + "0x0cc5261ab8ce458dc977078a3623e2badd27afd3": { + "name": "Account 1", + "address": "0x0cc5261ab8ce458dc977078a3623e2badd27afd3", + "lastSelected": 1618940443499 + }, + "0x3ed0ee22e0685ebbf07b2360a8331693c413cc59": { + "name": "Account 2", + "address": "0x3ed0ee22e0685ebbf07b2360a8331693c413cc59" + }, + "0xd38d853771fb546bd8b18b2f3638491bc0b0e906": { + "name": "2nd account", + "address": "0xd38d853771fb546bd8b18b2f3638491bc0b0e906", + "lastSelected": 1618940443010 + } + }, + "lostIdentities": {}, + "forgottenPassword": false, + "preferences": { + "showFiatInTestnets": false, + "useNativeCurrencyAsPrimaryCurrency": true, + "hideZeroBalanceTokens": false + }, + "completedOnboarding": true, + "ipfsGateway": "dweb.link", + "infuraBlocked": false, + "selectedAddress": "0x0cc5261ab8ce458dc977078a3623e2badd27afd3" + }, + "firstTimeInfo": { + "version": "9.3.0", + "date": 1617927806790 + }, + "NetworkController": { + "provider": { + "ticker": "ETH", + "type": "rpc", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "nickname": "Localhost 8545" + }, + "previousProviderStore": { + "ticker": "ETH", + "type": "rpc", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "nickname": "Localhost 8545" + }, + "network": "1337" + }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, + "CurrencyController": { + "conversionDate": 1618940438.187, + "conversionRate": 2254.54, + "currentCurrency": "usd", + "nativeCurrency": "ETH", + "usdConversionRate": 2254.54 + }, + "CachedBalancesController": { + "cachedBalances": { + "0x539": { + "0x0cc5261ab8ce458dc977078a3623e2badd27afd3": "0x14cffbeaf5a7d3000", + "0x3ed0ee22e0685ebbf07b2360a8331693c413cc59": "0x0", + "0xd38d853771fb546bd8b18b2f3638491bc0b0e906": "0x0" + } + } + }, + "MetaMetricsController": { + "participateInMetaMetrics": false, + "metaMetricsId": null, + "metaMetricsSendCount": 1 + }, + "PermissionsController": { + "permissionsRequests": [], + "permissionsDescriptions": {}, + "domains": {} + }, + "TransactionController": { + "transactions": { + "1374812123920442": { + "id": 1374812123920442, + "time": 1618940444708, + "status": "confirmed", + "metamaskNetworkId": "1337", + "chainId": "0x539", + "loadingDefaults": false, + "txParams": { + "from": "0x0cc5261ab8ce458dc977078a3623e2badd27afd3", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "nonce": "0x0", + "value": "0xde0b6b3a7640000", + "gas": "0x5208", + "gasPrice": "0x363fe1da00" + }, + "origin": "metamask", + "type": "sentEther", + "history": [], + "nonceDetails": { + "params": { + "highestLocallyConfirmed": 0, + "highestSuggested": 0, + "nextNetworkNonce": 0 + }, + "local": { + "name": "local", + "nonce": 0, + "details": { + "startPoint": 0, + "highest": 0 + } + }, + "network": { + "name": "network", + "nonce": 0, + "details": { + "blockNumber": "0x5", + "baseCount": 0 + } + } + }, + "r": "0x035de1d588f67547bd9c7bb9b34a8330c10085155e42af0017986911ca922e61", + "s": "0x36c3c12a323121afe6d63bc8c5929766d385e2e7b7fc892ecd0f10a7ec4ab59e", + "v": "0x0a95", + "rawTx": "0xf86e8085363fe1da00825208942f318c334780961fb129d2a6c30d0763d9a5c970880de0b6b3a764000080820a95a0035de1d588f67547bd9c7bb9b34a8330c10085155e42af0017986911ca922e61a036c3c12a323121afe6d63bc8c5929766d385e2e7b7fc892ecd0f10a7ec4ab59e", + "hash": "0xaba403a1dff459d7549b57369cfdd323c70fc79a307ed8f9cae811ddd6883a74", + "submittedTime": 1618940445208, + "txReceipt": { + "transactionHash": "0xaba403a1dff459d7549b57369cfdd323c70fc79a307ed8f9cae811ddd6883a74", + "transactionIndex": { + "negative": 0, + "words": [0, null], + "length": 1, + "red": null + }, + "blockHash": "0x734618e63369e7ea10872e68cec4d400d77c55ed525cbc295802d00d7b4bd1d2", + "blockNumber": { + "negative": 0, + "words": [6, null], + "length": 1, + "red": null + }, + "from": "0x0cc5261ab8ce458dc977078a3623e2badd27afd3", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "gasUsed": "5208", + "cumulativeGasUsed": { + "negative": 0, + "words": [21000, null], + "length": 1, + "red": null + }, + "contractAddress": null, + "logs": [], + "status": "0x1", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + } + } + }, + "KeyringController": { + "vault": "{\"data\":\"Ot+BTtJPag0xubdiv1nO9bsSvTHivHCd6CD7Lxgb1McYw3VqMjgp5rPMZmblJ1lscuMxyiqp99G52uXO9S0em6F9htpa+t/wn6qubRKTTNG9fxNzQrKXRDNhdgfYckVk5VAZ4fgl2iMZcRDvS8H/+gucVKJ33Sl6mXyPofdexXhWDCU6uR2YecnfaIum9cL2u/GqOMPE3jxzy0Wip0x2Jyp3QOKhvu8A3GIjzagLOaQ7a1APdl8=\",\"iv\":\"lbsyPeGYWU6U1+jvmW9UHg==\",\"salt\":\"Zmbhpskwxe4rYfXtELBvlcvW4HISPBATRmMqzsnZPMg=\"}" + }, + "AlertController": { + "alertEnabledness": { + "unconnectedAccount": true, + "web3ShimUsage": true + }, + "unconnectedAccountAlertShownOrigins": {}, + "web3ShimUsageOrigins": {} + }, + "OnboardingController": { + "seedPhraseBackedUp": true, + "onboardingTabs": {} + }, + "AddressBookController": { + "addressBook": { + "0x539": { + "0x2f318C334780961FB129D2a6c30D0763d9a5C970": { + "address": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", + "chainId": "0x539", + "isEns": false, + "memo": "", + "name": "" + } + } + } + }, + "AppStateController": { + "connectedStatusPopoverHasBeenShown": true, + "swapsWelcomeMessageHasBeenShown": false, + "defaultHomeActiveTabName": "Activity" + } + }, + "meta": { + "version": 57 + } +} diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index 1af251b51..a3d51e5d5 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "mkrMigrationReminderTimestamp": null, - "swapsWelcomeMessageHasBeenShown": true + "mkrMigrationReminderTimestamp": null }, "CachedBalancesController": { "cachedBalances": { @@ -37,6 +36,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/localization/state.json b/test/e2e/fixtures/localization/state.json index 8b60ffc9d..22d76151b 100644 --- a/test/e2e/fixtures/localization/state.json +++ b/test/e2e/fixtures/localization/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "mkrMigrationReminderTimestamp": null, - "swapsWelcomeMessageHasBeenShown": true + "mkrMigrationReminderTimestamp": null }, "CachedBalancesController": { "cachedBalances": { @@ -37,6 +36,19 @@ "type": "rpc" } }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/metrics-enabled/state.json b/test/e2e/fixtures/metrics-enabled/state.json index 4b0e52e70..2fa9931c5 100644 --- a/test/e2e/fixtures/metrics-enabled/state.json +++ b/test/e2e/fixtures/metrics-enabled/state.json @@ -1,8 +1,7 @@ { "data": { "AppStateController": { - "connectedStatusPopoverHasBeenShown": false, - "swapsWelcomeMessageHasBeenShown": true + "connectedStatusPopoverHasBeenShown": false }, "CachedBalancesController": { "cachedBalances": { @@ -41,6 +40,19 @@ }, "network": "1337" }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, "OnboardingController": { "onboardingTabs": {}, "seedPhraseBackedUp": false diff --git a/test/e2e/fixtures/onboarding/state.json b/test/e2e/fixtures/onboarding/state.json new file mode 100644 index 000000000..b0fbccd9a --- /dev/null +++ b/test/e2e/fixtures/onboarding/state.json @@ -0,0 +1,40 @@ +{ + "data": { + "config": {}, + "PreferencesController": { + "frequentRpcListDetail": [ + { + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "nickname": "Localhost 8545", + "rpcPrefs": {} + } + ] + }, + "firstTimeInfo": { + "version": "9.3.0", + "date": 1617927806790 + }, + "NetworkController": { + "provider": { + "ticker": "ETH", + "type": "rpc", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "nickname": "Localhost 8545" + }, + "network": "1337" + }, + "CurrencyController": { + "conversionDate": 1617927806.941, + "conversionRate": 2084.64, + "currentCurrency": "usd", + "nativeCurrency": "ETH", + "usdConversionRate": 2084.64 + } + }, + "meta": { + "version": 57 + } +} diff --git a/test/e2e/fixtures/send-edit/state.json b/test/e2e/fixtures/send-edit/state.json new file mode 100644 index 000000000..c5c548f4c --- /dev/null +++ b/test/e2e/fixtures/send-edit/state.json @@ -0,0 +1,185 @@ +{ + "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" + } + }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, + "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/fixtures/threebox-enabled/state.json b/test/e2e/fixtures/threebox-enabled/state.json new file mode 100644 index 000000000..8bac5f889 --- /dev/null +++ b/test/e2e/fixtures/threebox-enabled/state.json @@ -0,0 +1,135 @@ +{ + "data": { + "AppStateController": { + "swapsWelcomeMessageHasBeenShown": true, + "connectedStatusPopoverHasBeenShown": false + }, + "CachedBalancesController": { + "cachedBalances": { + "0x539": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": "0x15af1d78b58c40000" + } + } + }, + "CurrencyController": { + "conversionDate": 1617897791.928, + "conversionRate": 2072.49, + "currentCurrency": "usd", + "nativeCurrency": "ETH", + "usdConversionRate": 2072.49 + }, + "IncomingTransactionsController": { + "incomingTransactions": {}, + "incomingTxLastFetchedBlockByChainId": { + "0x5": null, + "0x2a": null, + "0x1": null, + "0x4": 5570536 + } + }, + "KeyringController": { + "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" + }, + "NetworkController": { + "provider": { + "nickname": "Localhost 8545", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "type": "rpc" + }, + "previousProviderStore": { + "nickname": "Localhost 8545", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "type": "rpc" + }, + "network": "1337" + }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "2": { + "isShown": true + }, + "3": { + "isShown": true + } + } + }, + "OnboardingController": { + "onboardingTabs": {}, + "seedPhraseBackedUp": true + }, + "PreferencesController": { + "frequentRpcListDetail": [ + { + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "nickname": "Localhost 8545", + "rpcPrefs": {} + } + ], + "accountTokens": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "0x4": [], + "0x3": [] + } + }, + "accountHiddenTokens": {}, + "assetImages": {}, + "tokens": [], + "hiddenTokens": [], + "suggestedTokens": {}, + "useBlockie": true, + "useNonceField": false, + "usePhishDetect": true, + "featureFlags": { + "showIncomingTransactions": true, + "transactionTime": false + }, + "knownMethodData": {}, + "firstTimeFlowType": "create", + "currentLocale": "en", + "identities": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "name": "Account 1" + } + }, + "lostIdentities": {}, + "forgottenPassword": false, + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "completedOnboarding": true, + "ipfsGateway": "dweb.link", + "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1" + }, + "config": {}, + "firstTimeInfo": { + "date": 1575697234195, + "version": "7.7.0" + }, + "MetaMetricsController": { + "metaMetricsId": null, + "participateInMetaMetrics": false, + "metaMetricsSendCount": 0 + }, + "ThreeBoxController": { + "threeBoxSyncingAllowed": true, + "showRestorePrompt": true, + "threeBoxLastUpdated": 0, + "threeBoxAddress": "0x64480aa2768ef12f3f19c5a01206ceb0f82d06b9", + "threeBoxSynced": true, + "threeBoxDisabled": false + } + }, + "meta": { + "version": 57 + } +} diff --git a/test/e2e/from-import-ui.spec.js b/test/e2e/from-import-ui.spec.js deleted file mode 100644 index fdea86cf8..000000000 --- a/test/e2e/from-import-ui.spec.js +++ /dev/null @@ -1,425 +0,0 @@ -const assert = require('assert'); -const webdriver = require('selenium-webdriver'); - -const { By, Key, 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('Using MetaMask with an existing account', function () { - let driver; - - const testSeedPhrase = - 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'; - const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3'; - const testPrivateKey2 = - '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; - const testPrivateKey3 = - 'F4EC2590A0C10DE95FBF4547845178910E40F5035320C516A18C117DE02B5669'; - - 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('Show account information', function () { - it('shows the correct account address', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__account-details"]'), - ); - await driver.findVisibleElement(By.css('.qr-code__wrapper')); - await driver.delay(regularDelayMs); - - const [address] = await driver.findElements( - By.css('.readonly-input__input'), - ); - assert.equal(await address.getAttribute('value'), testAddress); - - await driver.clickElement(By.css('.account-modal__close')); - await driver.delay(largeDelayMs); - }); - - it('shows a QR code for the account', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__account-details"]'), - ); - await driver.findVisibleElement(By.css('.qr-code__wrapper')); - const detailModal = await driver.findElement(By.css('span .modal')); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.account-modal__close')); - await driver.wait(until.stalenessOf(detailModal)); - await driver.delay(regularDelayMs); - }); - }); - - describe('Lock and unlock', function () { - it('logs out of the account', async function () { - await driver.clickElement(By.css('.account-menu__icon .identicon')); - 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('accepts the account password after lock', async function () { - const passwordField = await driver.findElement(By.id('password')); - await passwordField.sendKeys('correct horse battery staple'); - await passwordField.sendKeys(Key.ENTER); - await driver.delay(largeDelayMs); - }); - }); - - describe('Add an account', function () { - it('switches to localhost', async function () { - await driver.clickElement(By.css('.network-display')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Localhost')]`), - ); - await driver.delay(largeDelayMs); - }); - - it('choose Create Account from the account menu', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Create Account')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('set account name', async function () { - const [accountName] = await driver.findElements( - By.css('.new-account-create-form input'), - ); - await accountName.sendKeys('2nd account'); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Create')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('should show the correct account name', async function () { - const accountName = await driver.findElement( - By.css('.selected-account__name'), - ); - assert.equal(await accountName.getText(), '2nd account'); - await driver.delay(regularDelayMs); - }); - }); - - describe('Switch back to original account', function () { - it('chooses the original account from the account menu', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.account-menu__name')); - 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')); - 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('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(/-1\s*ETH/u.test(await txValues[0].getText())); - }); - }); - - describe('Imports an account with private key', function () { - it('choose Create Account from the account menu', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Import Account')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('enter private key', async function () { - const privateKeyInput = await driver.findElement( - By.css('#private-key-box'), - ); - await privateKeyInput.sendKeys(testPrivateKey2); - await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Import')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('should show the correct account name', async function () { - const accountName = await driver.findElement( - By.css('.selected-account__name'), - ); - assert.equal(await accountName.getText(), 'Account 4'); - await driver.delay(regularDelayMs); - }); - - it('should show the imported label', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - - // confirm 4th account is account 4, as expected - const accountMenuItemSelector = '.account-menu__account:nth-child(4)'; - const accountName = await driver.findElement( - By.css(`${accountMenuItemSelector} .account-menu__name`), - ); - assert.equal(await accountName.getText(), 'Account 4'); - // confirm label is present on the same menu item - const importedLabel = await driver.findElement( - By.css(`${accountMenuItemSelector} .keyring-label`), - ); - assert.equal(await importedLabel.getText(), 'IMPORTED'); - }); - }); - - describe('Imports and removes an account', function () { - it('choose Create Account from the account menu', async function () { - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Import Account')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('enter private key', async function () { - const privateKeyInput = await driver.findElement( - By.css('#private-key-box'), - ); - await privateKeyInput.sendKeys(testPrivateKey3); - await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Import')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('should see new account in account menu', async function () { - const accountName = await driver.findElement( - By.css('.selected-account__name'), - ); - assert.equal(await accountName.getText(), 'Account 5'); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - const accountListItems = await driver.findElements( - By.css('.account-menu__account'), - ); - assert.equal(accountListItems.length, 5); - - await driver.clickPoint(By.css('.account-menu__icon'), 0, 0); - }); - - it('should open the remove account modal', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - - await driver.clickElement( - By.css('[data-testid="account-options-menu__remove-account"]'), - ); - - await driver.findElement(By.css('.confirm-remove-account__account')); - }); - - it('should remove the account', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Remove')]`), - ); - - await driver.delay(regularDelayMs); - - const accountName = await driver.findElement( - By.css('.selected-account__name'), - ); - assert.equal(await accountName.getText(), 'Account 1'); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('.account-menu__icon')); - - const accountListItems = await driver.findElements( - By.css('.account-menu__account'), - ); - assert.equal(accountListItems.length, 4); - }); - }); - - describe('Connects to a Hardware wallet', function () { - it('choose Connect Hardware Wallet from the account menu', async function () { - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Connect Hardware Wallet')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('should open the TREZOR Connect popup', async function () { - await driver.clickElement(By.css('.hw-connect__btn:nth-of-type(2)')); - await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Connect')]`), - ); - await driver.delay(regularDelayMs); - const allWindows = await driver.getAllWindowHandles(); - assert.equal(allWindows.length, 2); - }); - }); -}); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 5daadbb0d..38ab4d500 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -22,6 +22,8 @@ async function withFixtures(options, testSuite) { driverOptions, mockSegment, title, + failOnConsoleError = true, + dappPath = undefined, } = options; const fixtureServer = new FixtureServer(); const ganacheServer = new Ganache(); @@ -35,15 +37,20 @@ async function withFixtures(options, testSuite) { await fixtureServer.start(); await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures)); if (dapp) { - const dappDirectory = path.resolve( - __dirname, - '..', - '..', - 'node_modules', - '@metamask', - 'test-dapp', - 'dist', - ); + let dappDirectory; + if (dappPath) { + dappDirectory = path.resolve(__dirname, dappPath); + } else { + dappDirectory = path.resolve( + __dirname, + '..', + '..', + 'node_modules', + '@metamask', + 'test-dapp', + 'dist', + ); + } dappServer = createStaticServer(dappDirectory); dappServer.listen(dappPort); await new Promise((resolve, reject) => { @@ -77,7 +84,11 @@ async function withFixtures(options, testSuite) { const errorMessage = `Errors found in browser console:\n${errorReports.join( '\n', )}`; - throw new Error(errorMessage); + if (failOnConsoleError) { + throw new Error(errorMessage); + } else { + console.error(new Error(errorMessage)); + } } } } catch (error) { diff --git a/test/e2e/incremental-security.spec.js b/test/e2e/incremental-security.spec.js deleted file mode 100644 index 7f9b99665..000000000 --- a/test/e2e/incremental-security.spec.js +++ /dev/null @@ -1,250 +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('MetaMask', function () { - let driver; - let publicAddress; - - this.timeout(0); - this.bail(true); - - before(async function () { - await ganacheServer.start({ - accounts: [ - { - secretKey: - '0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1', - balance: 0, - }, - { - 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(regularDelayMs); - }); - - 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('send to current account from dapp with different provider', function () { - let extension; - - it('switches to dapp screen', async function () { - const windowHandles = await driver.getAllWindowHandles(); - extension = windowHandles[0]; - - await driver.openNewPage('http://127.0.0.1:8080/'); - await driver.delay(regularDelayMs); - }); - - it('sends eth to the current account', async function () { - const addressInput = await driver.findElement(By.css('#address')); - await addressInput.sendKeys(publicAddress); - await driver.delay(regularDelayMs); - - await driver.clickElement(By.css('#send')); - - const txStatus = await driver.findElement(By.css('#success')); - await driver.wait(until.elementTextMatches(txStatus, /Success/u), 15000); - }); - - it('switches back to MetaMask', async function () { - await driver.switchToWindow(extension); - }); - - it('should have the correct amount of eth', async function () { - const balances = await driver.findElements( - By.css('.currency-display-component__text'), - ); - await driver.wait(until.elementTextMatches(balances[0], /1/u), 15000); - const balance = await balances[0].getText(); - - assert.equal(balance, '1'); - }); - }); - - describe('backs up the seed phrase', function () { - it('should show a backup reminder', async function () { - const backupReminder = await driver.findElements( - By.xpath( - "//div[contains(@class, 'home-notification__text') and contains(text(), 'Backup your Secret Recovery code to keep your wallet and funds secure')]", - ), - ); - assert.equal(backupReminder.length, 1); - }); - - it('should take the user to the seedphrase backup screen', async function () { - await driver.clickElement(By.css('.home-notification__accept-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('can click through the success screen', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'All Done')]`), - ); - await driver.delay(regularDelayMs); - }); - - it('should have the correct amount of eth', async function () { - const balances = await driver.findElements( - By.css('.currency-display-component__text'), - ); - await driver.wait(until.elementTextMatches(balances[0], /1/u), 15000); - const balance = await balances[0].getText(); - - assert.equal(balance, '1'); - }); - - it('should not show a backup reminder', async function () { - await driver.assertElementNotPresent(By.css('.backup-notification')); - }); - }); -}); diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js deleted file mode 100644 index eab3f498e..000000000 --- a/test/e2e/metamask-responsive-ui.spec.js +++ /dev/null @@ -1,289 +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('MetaMask', function () { - let driver; - - const testSeedPhrase = - 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'; - - this.timeout(0); - this.bail(true); - - before(async function () { - await ganacheServer.start(); - const result = await buildWebDriver({ responsive: true }); - 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', 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 "I Agree" option on the metametrics opt-in screen', async function () { - await driver.clickElement(By.css('.btn-primary')); - 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('Show account information', function () { - it('show account details dropdown menu', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - const options = await driver.findElements( - By.css('.account-options-menu .menu-item'), - ); - assert.equal(options.length, 3); // HD Wallet type does not have to show the Remove Account option - // click outside of menu to dismiss - // account menu button chosen because the menu never covers it. - await driver.clickPoint(By.css('.account-menu__icon'), 0, 0); - 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('switches to localhost', async function () { - await driver.clickElement(By.css('.network-display')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath( - `//span[contains(@class, 'network-name-item') and contains(text(), 'Localhost 8545')]`, - ), - ); - await driver.delay(largeDelayMs * 2); - }); - - it('balance renders', async function () { - const balance = await driver.findElement( - By.css('[data-testid="eth-overview__primary-currency"]'), - ); - await driver.wait(until.elementTextMatches(balance, /100\s*ETH/u)); - await driver.delay(regularDelayMs); - }); - }); - - describe('Send ETH from inside MetaMask', function () { - it('starts to send a 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'); - - const inputValue = await inputAmount.getAttribute('value'); - assert.equal(inputValue, '1'); - await driver.delay(regularDelayMs); - }); - - it('opens and closes the gas modal', async function () { - // 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')); - - await driver.clickElement(By.css('.page-container__header-close-text')); - await driver.wait(until.stalenessOf(gasModal), 10000); - await driver.delay(regularDelayMs); - }); - - it('clicks through to the confirm screen', async function () { - // 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')]`), - ); - }); - - 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); - }); - }); -}); diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 4ac2ee14d..78ed5f59d 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1,7 +1,5 @@ const assert = require('assert'); -const webdriver = require('selenium-webdriver'); -const { By, Key, until } = webdriver; const enLocaleMessages = require('../../app/_locales/en/messages.json'); const { tinyDelayMs, regularDelayMs, largeDelayMs } = require('./helpers'); const { buildWebDriver } = require('./webdriver'); @@ -49,74 +47,66 @@ describe('MetaMask', function () { 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.findElement('.welcome-page__header'); + await driver.clickElement({ + text: enLocaleMessages.getStarted.message, + tag: 'button', + }); 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.clickElement({ text: 'Create a Wallet', tag: 'button' }); 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.clickElement('.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'), + await driver.fill( + '.first-time-flow__form #create-password', + 'correct horse battery staple', ); - const passwordBoxConfirm = await driver.findElement( - By.css('.first-time-flow__form #confirm-password'), + await driver.fill( + '.first-time-flow__form #confirm-password', + 'correct horse battery staple', ); - 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('.first-time-flow__checkbox'); - await driver.clickElement(By.css('.first-time-flow__form button')); + await driver.clickElement('.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', - ); + const byRevealButton = + '.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button'; await driver.findElement(byRevealButton); await driver.clickElement(byRevealButton); await driver.delay(regularDelayMs); const revealedSeedPhrase = await driver.findElement( - By.css('.reveal-seed-phrase__secret-words'), + '.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.clickElement({ + text: enLocaleMessages.next.message, + tag: 'button', + }); await driver.delay(regularDelayMs); }); async function clickWordAndWait(word) { await driver.clickElement( - By.css( - `[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`, - ), + `[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`, ); await driver.delay(tinyDelayMs); } @@ -128,51 +118,64 @@ describe('MetaMask', function () { await clickWordAndWait(word); } - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); 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.findElement({ text: 'Congratulations', tag: 'div' }); + await driver.clickElement({ + text: enLocaleMessages.endOfFlowMessage10.message, + tag: 'button', + }); await driver.delay(regularDelayMs); }); }); + describe("Close the what's new popup", function () { + it("should show the what's new popover", async function () { + const popoverTitle = await driver.findElement( + '.popover-header__title h2', + ); + + assert.equal(await popoverTitle.getText(), "What's new"); + }); + + it("should close the what's new popup", async function () { + const popover = await driver.findElement('.popover-container'); + + await driver.clickElement('[data-testid="popover-close"]'); + + await popover.waitForElementState('hidden'); + }); + }); + describe('Show account information', function () { it('shows the QR code for the account', async function () { + await driver.clickElement('[data-testid="account-options-menu-button"]'); await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__account-details"]'), + '[data-testid="account-options-menu__account-details"]', ); - await driver.findVisibleElement(By.css('.qr-code__wrapper')); + await driver.findVisibleElement('.qr-code__wrapper'); await driver.delay(regularDelayMs); - const accountModal = await driver.findElement(By.css('span .modal')); - await driver.clickElement(By.css('.account-modal__close')); + // wait for permission modal to be visible. + const permissionModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement('.account-modal__close'); - await driver.wait(until.stalenessOf(accountModal)); + // wait for permission modal to be removed from DOM. + await permissionModal.waitForElementState('hidden'); await driver.delay(regularDelayMs); }); }); describe('Lock an unlock', function () { it('logs out of the account', async function () { - await driver.clickElement(By.css('.account-menu__icon')); + await driver.clickElement('.account-menu__icon'); await driver.delay(regularDelayMs); const lockButton = await driver.findClickableElement( - By.css('.account-menu__lock-button'), + '.account-menu__lock-button', ); assert.equal(await lockButton.getText(), 'Lock'); await lockButton.click(); @@ -180,41 +183,31 @@ describe('MetaMask', function () { }); it('accepts the account password after lock', async function () { - const passwordField = await driver.findElement(By.id('password')); - await passwordField.sendKeys('correct horse battery staple'); - await passwordField.sendKeys(Key.ENTER); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); await driver.delay(largeDelayMs * 4); }); }); describe('Add account', function () { it('choose Create Account from the account menu', async function () { - await driver.clickElement(By.css('.account-menu__icon')); + await driver.clickElement('.account-menu__icon'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Create Account')]`), - ); + await driver.clickElement({ text: 'Create Account', tag: 'div' }); await driver.delay(regularDelayMs); }); it('set account name', async function () { - const accountName = await driver.findElement( - By.css('.new-account-create-form input'), - ); - await accountName.sendKeys('2nd account'); + await driver.fill('.new-account-create-form input', '2nd account'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Create')]`), - ); + await driver.clickElement({ text: 'Create', tag: 'button' }); await driver.delay(largeDelayMs); }); it('should display correct account name', async function () { - const accountName = await driver.findElement( - By.css('.selected-account__name'), - ); + const accountName = await driver.findElement('.selected-account__name'); assert.equal(await accountName.getText(), '2nd account'); await driver.delay(regularDelayMs); }); @@ -222,11 +215,11 @@ describe('MetaMask', function () { describe('Import seed phrase', function () { it('logs out of the vault', async function () { - await driver.clickElement(By.css('.account-menu__icon')); + await driver.clickElement('.account-menu__icon'); await driver.delay(regularDelayMs); const lockButton = await driver.findClickableElement( - By.css('.account-menu__lock-button'), + '.account-menu__lock-button', ); assert.equal(await lockButton.getText(), 'Lock'); await lockButton.click(); @@ -235,78 +228,66 @@ describe('MetaMask', function () { 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', + '.unlock-page__link--import', ); + assert.equal(await restoreSeedLink.getText(), 'import using 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); + await driver.clickElement('.import-account__checkbox-container'); - const passwordInputs = await driver.findElements(By.css('input')); + await driver.fill('.import-account__secret-phrase', testSeedPhrase); 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.fill('#password', 'correct horse battery staple'); + await driver.fill('#confirm-password', 'correct horse battery staple'); + await driver.clickElement({ + text: enLocaleMessages.restore.message, + tag: 'button', + }); 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, /100\s*ETH/u)); + await driver.waitForSelector({ + css: '[data-testid="wallet-balance"] .list-item__heading', + text: '100 ETH', + }); await driver.delay(regularDelayMs); }); }); describe('Send ETH from inside MetaMask using default gas', function () { it('starts a send transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + await driver.clickElement('[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 driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', ); - await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('1000'); + const inputAmount = await driver.findElement('.unit-input__input'); + await inputAmount.fill('1000'); - const errorAmount = await driver.findElement( - By.css('.send-v2__error-amount'), - ); + const errorAmount = await driver.findElement('.send-v2__error-amount'); assert.equal( await errorAmount.getText(), 'Insufficient funds.', 'send screen should render an insufficient fund error message', ); - await inputAmount.sendKeys(Key.BACK_SPACE); + await inputAmount.press(driver.Key.BACK_SPACE); await driver.delay(50); - await inputAmount.sendKeys(Key.BACK_SPACE); + await inputAmount.press(driver.Key.BACK_SPACE); await driver.delay(50); - await inputAmount.sendKeys(Key.BACK_SPACE); + await inputAmount.press(driver.Key.BACK_SPACE); await driver.delay(tinyDelayMs); - await driver.assertElementNotPresent(By.css('.send-v2__error-amount')); + await driver.assertElementNotPresent('.send-v2__error-amount'); const amountMax = await driver.findClickableElement( - By.css('.send-v2__amount-max'), + '.send-v2__amount-max', ); await amountMax.click(); @@ -318,154 +299,139 @@ describe('MetaMask', function () { assert.equal(await inputAmount.isEnabled(), true); - await inputAmount.sendKeys('1'); + await inputAmount.fill('1'); 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.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); }); it('confirms the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); 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.clickElement('[data-testid="home__activity-tab"]'); await driver.wait(async () => { const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), + '.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); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }); }); describe('Send ETH from inside MetaMask using fast gas option', function () { it('starts a send transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + await driver.clickElement('[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 driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', ); - await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('1'); + const inputAmount = await driver.findElement('.unit-input__input'); + await inputAmount.fill('1'); const inputValue = await inputAmount.getAttribute('value'); assert.equal(inputValue, '1'); // Set the gas price - await driver.clickElement( - By.xpath(`//button/div/div[contains(text(), "Fast")]`), - ); + await driver.clickElement({ text: 'Fast', tag: 'button/div/div' }); await driver.delay(regularDelayMs); // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); }); it('confirms the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(largeDelayMs); }); 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.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-child(2)', ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }); }); }); describe('Send ETH from inside MetaMask using advanced gas modal', function () { it('starts a send transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + await driver.clickElement('[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 driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', ); - await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970'); - const inputAmount = await driver.findElement( - By.css('.unit-input__input'), - ); - await inputAmount.sendKeys('1'); + const inputAmount = await driver.findElement('.unit-input__input'); + await inputAmount.fill('1'); const inputValue = await inputAmount.getAttribute('value'); assert.equal(inputValue, '1'); // Set the gas limit - await driver.clickElement(By.css('.advanced-gas-options-btn')); + await driver.clickElement('.advanced-gas-options-btn'); await driver.delay(regularDelayMs); - const gasModal = await driver.findElement(By.css('span .modal')); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.wait(until.stalenessOf(gasModal)); + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + + await driver.clickElement({ text: 'Save', tag: 'button' }); + + // Wait for gas modal to be removed from DOM + await gasModal.waitForElementState('hidden'); await driver.delay(regularDelayMs); // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); }); it('confirms the transaction', async function () { const transactionAmounts = await driver.findElements( - By.css('.currency-display-component__text'), + '.currency-display-component__text', ); const transactionAmount = transactionAmounts[0]; assert.equal(await transactionAmount.getText(), '1'); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(largeDelayMs); }); 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 3; }, 10000); - const txValues = await driver.findElement( - By.css('.transaction-list-item__primary-currency'), + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }, + { timeout: 10000 }, ); - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/u), 10000); }); }); @@ -476,41 +442,34 @@ describe('MetaMask', function () { let dapp; it('goes to the settings screen', async function () { - await driver.clickElement(By.css('.account-menu__icon')); + await driver.clickElement('.account-menu__icon'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Settings')]`), - ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); - // await driver.findElement(By.css('.tab-bar')) + // await driver.findElement('.tab-bar') - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Advanced')]`), - ); + await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.delay(regularDelayMs); await driver.clickElement( - By.css( - '[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > div > div', - ), + '[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > div > div', ); - const advancedGasTitle = await driver.findElement( - By.xpath(`//span[contains(text(), 'Advanced gas controls')]`), - ); + const advancedGasTitle = await driver.findElement({ + text: 'Advanced gas controls', + tag: 'span', + }); await driver.scrollToElement(advancedGasTitle); await driver.clickElement( - By.css( - '[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > div > div', - ), + '[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > div > div', ); windowHandles = await driver.getAllWindowHandles(); extension = windowHandles[0]; await driver.closeAllWindowHandlesExcept([extension]); - await driver.clickElement(By.css('.app-header__logo-container')); + await driver.clickElement('.app-header__logo-container'); await driver.delay(largeDelayMs); }); @@ -519,9 +478,7 @@ describe('MetaMask', 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.clickElement({ text: 'Connect', tag: 'button' }); await driver.delay(regularDelayMs); @@ -541,10 +498,8 @@ describe('MetaMask', function () { await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Connect')]`), - ); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(dapp); @@ -552,10 +507,7 @@ describe('MetaMask', function () { }); it('initiates a send from the dapp', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Send')]`), - 10000, - ); + await driver.clickElement({ text: 'Send', tag: 'button' }, 10000); await driver.delay(2000); windowHandles = await driver.getAllWindowHandles(); @@ -565,31 +517,26 @@ describe('MetaMask', function () { ); await driver.delay(regularDelayMs); - await driver.assertElementNotPresent( - By.xpath(`//li[contains(text(), 'Data')]`), - ); + await driver.assertElementNotPresent({ text: 'Data', tag: 'li' }); const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), + '.advanced-gas-inputs__gas-edit-row__input', ); await gasPriceInput.clear(); await driver.delay(50); - await gasPriceInput.sendKeys('10'); + await gasPriceInput.fill('10'); await driver.delay(50); await driver.delay(tinyDelayMs); await driver.delay(50); - await gasLimitInput.clear(); + await gasLimitInput.fill(''); await driver.delay(50); - await gasLimitInput.sendKeys('25000'); + await gasLimitInput.fill('25000'); await driver.delay(1000); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - 10000, - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }, 10000); await driver.delay(regularDelayMs); await driver.waitUntilXWindowHandles(2); @@ -600,31 +547,29 @@ describe('MetaMask', function () { 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 4; }, 10000); - const txValue = await driver.findClickableElement( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txValue, /-3\s*ETH/u), 10000); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-3 ETH', + }); }); it('the transaction has the expected gas price', async function () { const txValue = await driver.findClickableElement( - By.css('.transaction-list-item__primary-currency'), + '.transaction-list-item__primary-currency', ); await txValue.click(); const popoverCloseButton = await driver.findClickableElement( - By.css('.popover-header__button'), - ); - const txGasPrice = await driver.findElement( - By.css('[data-testid="transaction-breakdown__gas-price"]'), + '.popover-header__button', ); - await driver.wait(until.elementTextMatches(txGasPrice, /^10$/u), 10000); + await driver.waitForSelector({ + css: '[data-testid="transaction-breakdown__gas-price"]', + text: '10', + }); await popoverCloseButton.click(); }); }); @@ -641,15 +586,17 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(largeDelayMs); - const send3eth = await driver.findClickableElement( - By.xpath(`//button[contains(text(), 'Send')]`), - ); + const send3eth = await driver.findClickableElement({ + text: 'Send', + tag: 'button', + }); await send3eth.click(); await driver.delay(largeDelayMs); - const contractDeployment = await driver.findClickableElement( - By.xpath(`//button[contains(text(), 'Deploy Contract')]`), - ); + const contractDeployment = await driver.findClickableElement({ + text: 'Deploy Contract', + tag: 'button', + }); await contractDeployment.click(); await driver.delay(largeDelayMs); @@ -661,14 +608,14 @@ describe('MetaMask', function () { await driver.switchToWindow(extension); await driver.delay(regularDelayMs); - await driver.clickElement(By.css('.transaction-list-item')); + await driver.clickElement('.transaction-list-item'); await driver.delay(largeDelayMs); }); it('navigates the transactions', async function () { - await driver.clickElement(By.css('[data-testid="next-page"]')); + await driver.clickElement('[data-testid="next-page"]'); let navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); let navigationText = await navigationElement.getText(); assert.equal( @@ -677,9 +624,9 @@ describe('MetaMask', function () { 'changed transaction right', ); - await driver.clickElement(By.css('[data-testid="next-page"]')); + await driver.clickElement('[data-testid="next-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -688,9 +635,9 @@ describe('MetaMask', function () { 'changed transaction right', ); - await driver.clickElement(By.css('[data-testid="next-page"]')); + await driver.clickElement('[data-testid="next-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -699,9 +646,9 @@ describe('MetaMask', function () { 'changed transaction right', ); - await driver.clickElement(By.css('[data-testid="first-page"]')); + await driver.clickElement('[data-testid="first-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -710,9 +657,9 @@ describe('MetaMask', function () { 'navigate to first transaction', ); - await driver.clickElement(By.css('[data-testid="last-page"]')); + await driver.clickElement('[data-testid="last-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -721,9 +668,9 @@ describe('MetaMask', function () { 'navigate to last transaction', ); - await driver.clickElement(By.css('[data-testid="previous-page"]')); + await driver.clickElement('[data-testid="previous-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -732,9 +679,9 @@ describe('MetaMask', function () { 'changed transaction left', ); - await driver.clickElement(By.css('[data-testid="previous-page"]')); + await driver.clickElement('[data-testid="previous-page"]'); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -746,7 +693,7 @@ describe('MetaMask', function () { it('adds a transaction while confirm screen is in focus', async function () { let navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); let navigationText = await navigationElement.getText(); assert.equal( @@ -762,14 +709,14 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Send')]`)); + await driver.clickElement({ text: 'Send', tag: 'button' }); await driver.delay(regularDelayMs); await driver.switchToWindow(extension); await driver.delay(regularDelayMs); navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); navigationText = await navigationElement.getText(); assert.equal( @@ -781,13 +728,11 @@ describe('MetaMask', function () { it('rejects a transaction', async function () { await driver.delay(tinyDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Reject')]`), - ); + await driver.clickElement({ text: 'Reject', tag: 'button' }); await driver.delay(largeDelayMs * 2); const navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); await driver.delay(tinyDelayMs); const navigationText = await navigationElement.getText(); @@ -796,13 +741,11 @@ describe('MetaMask', function () { it('confirms a transaction', async function () { await driver.delay(tinyDelayMs / 2); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); const navigationElement = await driver.findElement( - By.css('.confirm-page-container-navigation'), + '.confirm-page-container-navigation', ); await driver.delay(tinyDelayMs / 2); const navigationText = await navigationElement.getText(); @@ -811,19 +754,15 @@ describe('MetaMask', function () { }); it('rejects the rest of the transactions', async function () { - await driver.clickElement(By.xpath(`//a[contains(text(), 'Reject 3')]`)); + await driver.clickElement({ text: 'Reject 3', tag: 'a' }); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Reject All')]`), - ); + await driver.clickElement({ text: 'Reject All', tag: 'button' }); await driver.delay(largeDelayMs * 2); await driver.wait(async () => { const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 5; }, 10000); @@ -842,28 +781,24 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); - await driver.clickElement(By.css('#deployButton')); + await driver.clickElement('#deployButton'); await driver.delay(regularDelayMs); await driver.switchToWindow(extension); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//h2[contains(text(), 'Contract Deployment')]`), - ); + await driver.clickElement({ text: 'Contract Deployment', tag: 'h2' }); await driver.delay(largeDelayMs); }); it('displays the contract creation data', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Data')]`)); + await driver.clickElement({ text: 'Data', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.findElement( - By.xpath(`//div[contains(text(), '127.0.0.1')]`), - ); + await driver.findElement({ text: '127.0.0.1', tag: 'div' }); const confirmDataDiv = await driver.findElement( - By.css('.confirm-page-container-content__data-box'), + '.confirm-page-container-content__data-box', ); const confirmDataText = await confirmDataDiv.getText(); assert.ok(confirmDataText.includes('Origin:')); @@ -871,31 +806,24 @@ describe('MetaMask', function () { assert.ok(confirmDataText.includes('Bytes:')); assert.ok(confirmDataText.includes('675')); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Details')]`), - ); + await driver.clickElement({ text: 'Details', tag: 'button' }); await driver.delay(regularDelayMs); }); it('confirms a deploy contract transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(largeDelayMs); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), - ); - return confirmedTxes.length === 6; - }, 10000); + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(6)', + ); - const txAction = await driver.findElements(By.css('.list-item__heading')); - await driver.wait( - until.elementTextMatches(txAction[0], /Contract\sDeployment/u), - 10000, + await driver.waitForSelector( + { + css: '.list-item__title', + text: 'Contract Deployment', + }, + { timeout: 10000 }, ); await driver.delay(regularDelayMs); }); @@ -904,88 +832,81 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); - let contractStatus = await driver.findElement(By.css('#contractStatus')); - await driver.wait( - until.elementTextMatches(contractStatus, /Deployed/u), - 15000, + await driver.waitForSelector( + { + css: '#contractStatus', + text: 'Deployed', + }, + { timeout: 15000 }, ); - await driver.clickElement(By.css('#depositButton')); + await driver.clickElement('#depositButton'); await driver.delay(largeDelayMs); - contractStatus = await driver.findElement(By.css('#contractStatus')); - await driver.wait( - until.elementTextMatches(contractStatus, /Deposit\sinitiated/u), - 10000, + await driver.waitForSelector( + { + css: '#contractStatus', + text: 'Deposit initiated', + }, + { timeout: 10000 }, ); await driver.switchToWindow(extension); await driver.delay(largeDelayMs * 2); - await driver.findElements(By.css('.transaction-list-item--unconfirmed')); + await driver.findElements('.transaction-list-item--unconfirmed'); const txListValue = await driver.findClickableElement( - By.css('.transaction-list-item__primary-currency'), + '.transaction-list-item__primary-currency', ); - await driver.wait( - until.elementTextMatches(txListValue, /-4\s*ETH/u), - 10000, + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-4 ETH', + }, + { timeout: 10000 }, ); await txListValue.click(); await driver.delay(regularDelayMs); // Set the gas limit - await driver.clickElement( - By.css('.confirm-detail-row__header-text--edit'), - ); - await driver.delay(regularDelayMs); - - const gasModal = await driver.findElement(By.css('span .modal')); - await driver.delay(regularDelayMs); - await driver.clickElement(By.css('.page-container__tab:nth-of-type(2)')); + await driver.clickElement('.confirm-detail-row__header-text--edit'); + // wait for gas modal to be visible. + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement('.page-container__tab:nth-of-type(2)'); await driver.delay(regularDelayMs); const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), + '.advanced-gas-inputs__gas-edit-row__input', ); const gasLimitValue = await gasLimitInput.getAttribute('value'); assert(Number(gasLimitValue) < 100000, 'Gas Limit too high'); - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.sendKeys('10'); + await gasPriceInput.fill('10'); await driver.delay(50); - await gasLimitInput.clear(); - await driver.delay(50); - await gasLimitInput.sendKeys('60001'); + await gasLimitInput.fill('60001'); await driver.delay(1000); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.wait(until.stalenessOf(gasModal)); + // wait for gas modal to be detached from DOM + await gasModal.waitForElementState('hidden'); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), - ); - return confirmedTxes.length === 7; - }, 10000); - - const txValues = await driver.findElements( - By.css('.transaction-list-item__primary-currency'), + await driver.waitForSelector( + '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(7)', + { timeout: 10000 }, ); - await driver.wait( - until.elementTextMatches(txValues[0], /-4\s*ETH/u), - 10000, + await driver.waitForSelector( + { + css: + '.transaction-list__completed-transactions .transaction-list-item__primary-currency', + text: '-4 ETH', + }, + { timeout: 10000 }, ); }); @@ -993,50 +914,46 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs); - await driver.clickElement(By.css('#withdrawButton')); + await driver.clickElement('#withdrawButton'); await driver.delay(regularDelayMs); await driver.switchToWindow(extension); await driver.delay(largeDelayMs * 2); await driver.clickElement( - By.css( - '.transaction-list__pending-transactions .transaction-list-item', - ), + '.transaction-list__pending-transactions .transaction-list-item', ); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); await driver.wait(async () => { const confirmedTxes = await driver.findElements( - By.css( - '.transaction-list__completed-transactions .transaction-list-item', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 8; }, 10000); - const txValues = await driver.findElement( - By.css('.transaction-list-item__primary-currency'), + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-0 ETH', + }, + { timeout: 10000 }, ); - await driver.wait(until.elementTextMatches(txValues, /-0\s*ETH/u), 10000); await driver.closeAllWindowHandlesExcept([extension, dapp]); await driver.switchToWindow(extension); }); it('renders the correct ETH balance', async function () { - const balance = await driver.findElement( - By.css('[data-testid="eth-overview__primary-currency"]'), - ); - await driver.delay(regularDelayMs); - await driver.wait( - until.elementTextMatches(balance, /^87.*\s*ETH.*$/u), - 10000, + const balance = await driver.waitForSelector( + { + css: '[data-testid="eth-overview__primary-currency"]', + text: '87.', + }, + { timeout: 10000 }, ); const tokenAmount = await balance.getText(); assert.ok(/^87.*\s*ETH.*$/u.test(tokenAmount)); @@ -1054,46 +971,38 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(regularDelayMs * 2); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Create Token')]`), - ); + await driver.clickElement({ text: 'Create Token', tag: 'button' }); windowHandles = await driver.waitUntilXWindowHandles(3); const popup = windowHandles[2]; await driver.switchToWindow(popup); await driver.delay(regularDelayMs); - await driver.clickElement( - By.css('.confirm-detail-row__header-text--edit'), - ); + await driver.clickElement('.confirm-detail-row__header-text--edit'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Advanced')]`), - ); + await driver.clickElement({ text: 'Advanced', tag: 'button' }); await driver.delay(tinyDelayMs); const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), + '.advanced-gas-inputs__gas-edit-row__input', ); assert(gasPriceInput.getAttribute('value'), 20); assert(gasLimitInput.getAttribute('value'), 4700000); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); + await driver.clickElement({ text: 'Save', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); await driver.switchToWindow(dapp); await driver.delay(tinyDelayMs); - const tokenContractAddress = await driver.findElement( - By.css('#tokenAddress'), - ); - await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/u)); + const tokenContractAddress = await driver.waitForSelector({ + css: '#tokenAddress', + text: '0x', + }); tokenAddress = await tokenContractAddress.getText(); await driver.delay(regularDelayMs); @@ -1104,101 +1013,85 @@ describe('MetaMask', function () { }); it('clicks on the Add Token button', async function () { - await driver.clickElement(By.css(`[data-testid="home__asset-tab"]`)); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Add Token')]`), - ); + await driver.clickElement(`[data-testid="home__asset-tab"]`); + await driver.clickElement({ text: 'Add Token', tag: 'button' }); await driver.delay(regularDelayMs); }); it('picks the newly created Test token', async function () { - await driver.clickElement( - By.xpath("//button[contains(text(), 'Custom Token')]"), - ); + await driver.clickElement({ + text: 'Custom Token', + tag: 'button', + }); await driver.delay(regularDelayMs); - const newTokenAddress = await driver.findElement( - By.css('#custom-address'), - ); - await newTokenAddress.sendKeys(tokenAddress); + await driver.fill('#custom-address', tokenAddress); await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Add Tokens')]`), - ); + await driver.clickElement({ text: 'Add Tokens', tag: 'button' }); await driver.delay(regularDelayMs); }); it('renders the balance for the new token', async function () { - const balance = await driver.findElement( - By.css('.wallet-overview .token-overview__primary-balance'), - ); - await driver.wait(until.elementTextMatches(balance, /^10\s*TST\s*$/u)); - const tokenAmount = await balance.getText(); - assert.ok(/^10\s*TST\s*$/u.test(tokenAmount)); + await driver.waitForSelector({ + css: '.wallet-overview .token-overview__primary-balance', + text: '10 TST', + }); await driver.delay(regularDelayMs); }); }); describe('Send token from inside MetaMask', function () { - let gasModal; it('starts to send a transaction', async function () { - await driver.clickElement(By.css('[data-testid="eth-overview-send"]')); + await driver.clickElement('[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 driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', ); - await inputAmount.sendKeys('1'); - - // Set the gas limit - await driver.clickElement(By.css('.advanced-gas-options-btn')); - await driver.delay(regularDelayMs); - gasModal = await driver.findElement(By.css('span .modal')); - await driver.delay(regularDelayMs); + driver.fill('.unit-input__input', '1'); }); - it('opens customize gas modal', async function () { - await driver.findElement(By.css('.page-container__title')); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.delay(regularDelayMs); + it('opens customize gas modal and saves options to continue', async function () { + await driver.clickElement('.advanced-gas-options-btn'); + + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.findElement('.page-container__title'); + await driver.clickElement({ text: 'Save', tag: 'button' }); + // wait for gas modal to be removed from DOM. + await gasModal.waitForElementState('hidden'); }); it('transitions to the confirm screen', async function () { - await driver.wait(until.stalenessOf(gasModal)); - // Continue to next screen - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); }); it('displays the token transfer data', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Data')]`)); + await driver.clickElement({ text: 'Data', tag: 'button' }); await driver.delay(regularDelayMs); const functionType = await driver.findElement( - By.css('.confirm-page-container-content__function-type'), + '.confirm-page-container-content__function-type', ); const functionTypeText = await functionType.getText(); assert.equal(functionTypeText, 'Transfer'); const tokenAmount = await driver.findElement( - By.css('.confirm-page-container-summary__title-text'), + '.confirm-page-container-summary__title-text', ); const tokenAmountText = await tokenAmount.getText(); assert.equal(tokenAmountText, '1 TST'); const confirmDataDiv = await driver.findElement( - By.css('.confirm-page-container-content__data-box'), + '.confirm-page-container-content__data-box', ); const confirmDataText = await confirmDataDiv.getText(); @@ -1209,50 +1102,39 @@ describe('MetaMask', function () { ), ); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Details')]`), - ); + await driver.clickElement({ text: 'Details', tag: 'button' }); await driver.delay(regularDelayMs); }); it('submits the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); }); 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', - ), + '.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); - await driver.wait( - until.elementTextMatches(txValues[0], /-1\s*TST/u), - 10000, + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 TST', + }, + { timeout: 10000 }, ); - const txStatuses = await driver.findElements( - By.css('.list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Send\sTST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.list-item__heading', + text: 'Send TST', + }); }); }); describe('Send a custom token from dapp', function () { - let gasModal; it('sends an already created token', async function () { const windowHandles = await driver.getAllWindowHandles(); const extension = windowHandles[0]; @@ -1265,65 +1147,56 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(tinyDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Transfer Tokens')]`), - ); + await driver.clickElement({ text: 'Transfer Tokens', tag: 'button' }); await driver.switchToWindow(extension); await driver.delay(largeDelayMs); - await driver.findElements( - By.css('.transaction-list__pending-transactions'), + await driver.findElements('.transaction-list__pending-transactions'); + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }, + { timeout: 10000 }, ); - const txListValue = await driver.findClickableElement( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait( - until.elementTextMatches(txListValue, /-1.5\s*TST/u), - 10000, - ); - await txListValue.click(); + await driver.clickElement('.transaction-list-item__primary-currency'); await driver.delay(regularDelayMs); const transactionAmounts = await driver.findElements( - By.css('.currency-display-component__text'), + '.currency-display-component__text', ); const transactionAmount = transactionAmounts[0]; assert(await transactionAmount.getText(), '1.5 TST'); - - // Set the gas limit - await driver.clickElement( - By.css('.confirm-detail-row__header-text--edit'), - ); - await driver.delay(regularDelayMs); - - gasModal = await driver.findElement(By.css('span .modal')); }); it('customizes gas', async function () { - await driver.clickElement(By.css('.page-container__tab:nth-of-type(2)')); + // Set the gas limit + await driver.clickElement('.confirm-detail-row__header-text--edit'); + await driver.delay(regularDelayMs); + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement('.page-container__tab:nth-of-type(2)'); await driver.delay(regularDelayMs); const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), + '.advanced-gas-inputs__gas-edit-row__input', ); - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.sendKeys('10'); + await gasPriceInput.fill('10'); await driver.delay(50); - await gasLimitInput.clear(); - await driver.delay(50); - await gasLimitInput.sendKeys('60000'); + await gasLimitInput.fill('60000'); await driver.delay(1000); - await driver.clickElement(By.css('.page-container__footer-button')); - await driver.wait(until.stalenessOf(gasModal)); + await driver.clickElement('.page-container__footer-button'); + + // wait for gas modal to be removed from DOM. + await gasModal.waitForElementState('hidden'); const gasFeeInputs = await driver.findElements( - By.css('.confirm-detail-row__primary'), + '.confirm-detail-row__primary', ); const renderedGasFee = await gasFeeInputs[0].getText(); assert.equal(renderedGasFee, '0.0006'); @@ -1331,51 +1204,41 @@ describe('MetaMask', function () { it('submits the transaction', async function () { const tokenAmount = await driver.findElement( - By.css('.confirm-page-container-summary__title-text'), + '.confirm-page-container-summary__title-text', ); const tokenAmountText = await tokenAmount.getText(); assert.equal(tokenAmountText, '1.5 TST'); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); }); 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 2; }, 10000); - const txValues = await driver.findElements( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/u)); - const txStatuses = await driver.findElements( - By.css('.list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Send\sTST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); - const tokenBalanceAmount = await driver.findElements( - By.css('.token-overview__primary-balance'), - ); - await driver.wait( - until.elementTextMatches(tokenBalanceAmount[0], /7.5\s*TST/u), - 10000, - ); + await driver.waitForSelector({ + css: '.list-item__heading', + text: 'Send TST', + }); + + await driver.waitForSelector({ + css: '.token-overview__primary-balance', + text: '7.5 TST', + }); }); }); describe('Approves a custom token from dapp', function () { - let gasModal; it('approves an already created token', async function () { const windowHandles = await driver.getAllWindowHandles(); const extension = windowHandles[0]; @@ -1389,48 +1252,43 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(tinyDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Approve Tokens')]`), - ); + await driver.clickElement({ text: 'Approve Tokens', tag: 'button' }); await driver.switchToWindow(extension); await driver.delay(regularDelayMs); await driver.wait(async () => { const pendingTxes = await driver.findElements( - By.css( - '.transaction-list__pending-transactions .transaction-list-item', - ), + '.transaction-list__pending-transactions .transaction-list-item', ); return pendingTxes.length === 1; }, 10000); - const [txtListHeading] = await driver.findElements( - By.css('.transaction-list-item .list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txtListHeading, /Approve TST spend limit/u), - ); - await driver.clickElement(By.css('.transaction-list-item')); + await driver.waitForSelector({ + // Selects only the very first transaction list item immediately following the 'Pending' header + css: + '.transaction-list__pending-transactions .transaction-list__header + .transaction-list-item .list-item__heading', + text: 'Approve TST spend limit', + }); + + await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); it('displays the token approval data', async function () { await driver.clickElement( - By.css('.confirm-approve-content__view-full-tx-button'), + '.confirm-approve-content__view-full-tx-button', ); await driver.delay(regularDelayMs); const functionType = await driver.findElement( - By.css( - '.confirm-approve-content__data .confirm-approve-content__small-text', - ), + '.confirm-approve-content__data .confirm-approve-content__small-text', ); const functionTypeText = await functionType.getText(); assert.equal(functionTypeText, 'Function: Approve'); const confirmDataDiv = await driver.findElement( - By.css('.confirm-approve-content__data__data-block'), + '.confirm-approve-content__data__data-block', ); const confirmDataText = await confirmDataDiv.getText(); assert( @@ -1440,99 +1298,84 @@ describe('MetaMask', function () { ); }); - it('opens the gas edit modal', async function () { - await driver.clickElement( - By.css('.confirm-approve-content__small-blue-text.cursor-pointer'), - ); + it('customizes gas', async function () { + await driver.clickElement('.confirm-approve-content__small-blue-text'); await driver.delay(regularDelayMs); - gasModal = await driver.findElement(By.css('span .modal')); - }); - - it('customizes gas', async function () { - await driver.clickElement(By.css('.page-container__tab:nth-of-type(2)')); + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement('.page-container__tab:nth-of-type(2)'); await driver.delay(regularDelayMs); const [gasPriceInput, gasLimitInput] = await driver.findElements( - By.css('.advanced-gas-inputs__gas-edit-row__input'), + '.advanced-gas-inputs__gas-edit-row__input', ); - await gasPriceInput.clear(); - await driver.delay(50); - await gasPriceInput.sendKeys('10'); + await gasPriceInput.fill('10'); await driver.delay(50); - await gasLimitInput.clear(); - await driver.delay(50); - await gasLimitInput.sendKeys('60001'); + await gasLimitInput.fill('60001'); await driver.delay(1000); - await driver.clickElement(By.css('.page-container__footer-button')); - await driver.wait(until.stalenessOf(gasModal)); + await driver.clickElement('.page-container__footer-button'); + + // wait for gas modal to be removed from DOM. + await gasModal.waitForElementState('hidden'); const gasFeeInEth = await driver.findElement( - By.css( - '.confirm-approve-content__transaction-details-content__secondary-fee', - ), + '.confirm-approve-content__transaction-details-content__secondary-fee', ); assert.equal(await gasFeeInEth.getText(), '0.0006 ETH'); }); it('edits the permission', async function () { const editButtons = await driver.findClickableElements( - By.css('.confirm-approve-content__small-blue-text.cursor-pointer'), + '.confirm-approve-content__small-blue-text', ); - await editButtons[1].click(); - await driver.delay(regularDelayMs); - - const permissionModal = await driver.findElement(By.css('span .modal')); + await editButtons[2].click(); + // wait for permission modal to be visible + const permissionModal = await driver.findVisibleElement('span .modal'); const radioButtons = await driver.findClickableElements( - By.css('.edit-approval-permission__edit-section__radio-button'), + '.edit-approval-permission__edit-section__radio-button', ); await radioButtons[1].click(); - const customInput = await driver.findElement(By.css('input')); - await driver.delay(50); - await customInput.sendKeys('5'); + await driver.fill('input', '5'); await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Save')]`)); - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.wait(until.stalenessOf(permissionModal)); + // wait for permission modal to be removed from DOM. + await permissionModal.waitForElementState('hidden'); const permissionInfo = await driver.findElements( - By.css('.confirm-approve-content__medium-text'), + '.confirm-approve-content__medium-text', ); const amountDiv = permissionInfo[0]; assert.equal(await amountDiv.getText(), '5 TST'); }); it('submits the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); }); 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 3; }, 10000); - const txStatuses = await driver.findElements( - By.css('.list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + // Select only the heading of the first entry in the transaction list. + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Approve TST spend limit', + }); }); }); @@ -1549,56 +1392,55 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Transfer Tokens Without Gas')]`), - ); + await driver.clickElement({ + text: 'Transfer Tokens Without Gas', + tag: 'button', + }); await driver.switchToWindow(extension); await driver.delay(regularDelayMs); await driver.wait(async () => { const pendingTxes = await driver.findElements( - By.css( - '.transaction-list__pending-transactions .transaction-list-item', - ), + '.transaction-list__pending-transactions .transaction-list-item', ); return pendingTxes.length === 1; }, 10000); - const [txListValue] = await driver.findElements( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txListValue, /-1.5\s*TST/u)); - await driver.clickElement(By.css('.transaction-list-item')); + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); + await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); it('submits the transaction', async function () { await driver.delay(largeDelayMs * 2); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 4; }, 10000); - const txValues = await driver.findElements( - By.css('.transaction-list-item__primary-currency'), - ); - await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/u)); - const txStatuses = await driver.findElements( - By.css('.list-item__heading'), - ); - await driver.wait(until.elementTextMatches(txStatuses[0], /Send TST/u)); + await driver.waitForSelector({ + // Select the heading of the first transaction list item in the + // completed transaction list with text matching Send TST + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Send TST', + }); + + await driver.waitForSelector({ + css: '.transaction-list-item__primary-currency', + text: '-1.5 TST', + }); }); }); @@ -1616,40 +1458,40 @@ describe('MetaMask', function () { await driver.switchToWindow(dapp); await driver.delay(tinyDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Approve Tokens Without Gas')]`), - ); + await driver.clickElement({ + text: 'Approve Tokens Without Gas', + tag: 'button', + }); await driver.switchToWindow(extension); await driver.delay(regularDelayMs); await driver.wait(async () => { const pendingTxes = await driver.findElements( - By.css( - '.transaction-list__pending-transactions .transaction-list-item', - ), + '.transaction-list__pending-transactions .transaction-list-item', ); return pendingTxes.length === 1; }, 10000); - const [txtListHeading] = await driver.findElements( - By.css('.transaction-list-item .list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txtListHeading, /Approve TST spend limit/u), - ); - await driver.clickElement(By.css('.transaction-list-item')); + await driver.waitForSelector({ + // Selects only the very first transaction list item immediately following the 'Pending' header + css: + '.transaction-list__pending-transactions .transaction-list__header + .transaction-list-item .list-item__heading', + text: 'Approve TST spend limit', + }); + + await driver.clickElement('.transaction-list-item'); await driver.delay(regularDelayMs); }); it('shows the correct recipient', async function () { await driver.clickElement( - By.css('.confirm-approve-content__view-full-tx-button'), + '.confirm-approve-content__view-full-tx-button', ); await driver.delay(regularDelayMs); const permissionInfo = await driver.findElements( - By.css('.confirm-approve-content__medium-text'), + '.confirm-approve-content__medium-text', ); const recipientDiv = permissionInfo[1]; assert.equal(await recipientDiv.getText(), '0x2f318C33...C970'); @@ -1657,79 +1499,69 @@ describe('MetaMask', function () { it('submits the transaction', async function () { await driver.delay(1000); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Confirm')]`), - ); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); }); 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', - ), + '.transaction-list__completed-transactions .transaction-list-item', ); return confirmedTxes.length === 5; }, 10000); - const txStatuses = await driver.findElements( - By.css('.list-item__heading'), - ); - await driver.wait( - until.elementTextMatches(txStatuses[0], /Approve TST spend limit/u), - ); + await driver.waitForSelector({ + css: + '.transaction-list__completed-transactions .transaction-list-item:first-child .list-item__heading', + text: 'Approve TST spend limit', + }); }); }); describe('Hide token', function () { it('hides the token when clicked', async function () { - await driver.clickElement( - By.css('[data-testid="token-options__button"]'), - ); + await driver.clickElement('[data-testid="asset-options__button"]'); - await driver.clickElement(By.css('[data-testid="token-options__hide"]')); + await driver.clickElement('[data-testid="asset-options__hide"]'); - const confirmHideModal = await driver.findElement(By.css('span .modal')); + // wait for confirm hide modal to be visible + const confirmHideModal = await driver.findVisibleElement('span .modal'); await driver.clickElement( - By.css('[data-testid="hide-token-confirmation__hide"]'), + '[data-testid="hide-token-confirmation__hide"]', ); - await driver.wait(until.stalenessOf(confirmHideModal)); + // wait for confirm hide modal to be removed from DOM. + await confirmHideModal.waitForElementState('hidden'); }); }); describe('Add existing token using search', function () { it('clicks on the Add Token button', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Add Token')]`), - ); + await driver.clickElement({ text: 'Add Token', tag: 'button' }); await driver.delay(regularDelayMs); }); it('can pick a token from the existing options', async function () { - const tokenSearch = await driver.findElement(By.css('#search-tokens')); - await tokenSearch.sendKeys('BAT'); + await driver.fill('#search-tokens', 'BAT'); await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath("//span[contains(text(), 'BAT')]")); + await driver.clickElement({ text: 'BAT', tag: 'span' }); await driver.delay(regularDelayMs); - await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`)); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Add Tokens')]`), - ); + await driver.clickElement({ text: 'Add Tokens', tag: 'button' }); await driver.delay(largeDelayMs); }); it('renders the balance for the chosen token', async function () { - const balance = await driver.findElement( - By.css('.token-overview__primary-balance'), - ); - await driver.wait(until.elementTextMatches(balance, /0\s*BAT/u)); + await driver.waitForSelector({ + css: '.token-overview__primary-balance', + text: '0 BAT', + }); await driver.delay(regularDelayMs); }); }); @@ -1739,19 +1571,15 @@ describe('MetaMask', function () { const rpcUrl = 'http://127.0.0.1:8545/1'; const chainId = '0x539'; // Ganache default, decimal 1337 - await driver.clickElement(By.css('.network-display')); + await driver.clickElement('.network-display'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Custom RPC')]`), - ); + await driver.clickElement({ text: 'Custom RPC', tag: 'span' }); await driver.delay(regularDelayMs); - await driver.findElement(By.css('.settings-page__sub-header-text')); + await driver.findElement('.settings-page__sub-header-text'); - const customRpcInputs = await driver.findElements( - By.css('input[type="text"]'), - ); + const customRpcInputs = await driver.findElements('input[type="text"]'); const rpcUrlInput = customRpcInputs[1]; const chainIdInput = customRpcInputs[2]; @@ -1761,29 +1589,23 @@ describe('MetaMask', function () { await chainIdInput.clear(); await chainIdInput.sendKeys(chainId); - await driver.clickElement(By.css('.network-form__footer .btn-secondary')); - await driver.findElement( - By.xpath(`//div[contains(text(), '${rpcUrl}')]`), - ); + await driver.clickElement('.network-form__footer .btn-secondary'); + await driver.findElement({ text: rpcUrl, tag: 'div' }); }); it(`creates second custom RPC entry`, async function () { const rpcUrl = 'http://127.0.0.1:8545/2'; const chainId = '0x539'; // Ganache default, decimal 1337 - await driver.clickElement(By.css('.network-display')); + await driver.clickElement('.network-display'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Custom RPC')]`), - ); + await driver.clickElement({ text: 'Custom RPC', tag: 'span' }); await driver.delay(regularDelayMs); - await driver.findElement(By.css('.settings-page__sub-header-text')); + await driver.findElement('.settings-page__sub-header-text'); - const customRpcInputs = await driver.findElements( - By.css('input[type="text"]'), - ); + const customRpcInputs = await driver.findElements('input[type="text"]'); const rpcUrlInput = customRpcInputs[1]; const chainIdInput = customRpcInputs[2]; @@ -1793,63 +1615,57 @@ describe('MetaMask', function () { await chainIdInput.clear(); await chainIdInput.sendKeys(chainId); - await driver.clickElement(By.css('.network-form__footer .btn-secondary')); - await driver.findElement( - By.xpath(`//div[contains(text(), '${rpcUrl}')]`), - ); + await driver.clickElement('.network-form__footer .btn-secondary'); + await driver.findElement({ text: rpcUrl, tag: 'div' }); }); it('selects another provider', async function () { - await driver.clickElement(By.css('.network-display')); + await driver.clickElement('.network-display'); await driver.delay(regularDelayMs); - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Ethereum Mainnet')]`), - ); + await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'span' }); await driver.delay(largeDelayMs * 2); }); it('finds all recent RPCs in history', async function () { - await driver.clickElement(By.css('.network-display')); + await driver.clickElement('.network-display'); await driver.delay(regularDelayMs); // only recent 3 are found and in correct order (most recent at the top) - const customRpcs = await driver.findElements( - By.xpath(`//span[contains(text(), 'http://127.0.0.1:8545/')]`), - ); + const customRpcs = await driver.findElements({ + text: 'http://127.0.0.1:8545/', + tag: 'span', + }); // click Mainnet to dismiss network dropdown - await driver.clickElement( - By.xpath(`//span[contains(text(), 'Ethereum Mainnet')]`), - ); + await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'span' }); assert.equal(customRpcs.length, 2); }); it('deletes a custom RPC', async function () { const networkListItems = await driver.findClickableElements( - By.css('.networks-tab__networks-list-name'), + '.networks-tab__networks-list-name', ); const lastNetworkListItem = networkListItems[networkListItems.length - 1]; await lastNetworkListItem.click(); await driver.delay(100); - await driver.clickElement(By.css('.btn-danger')); + await driver.clickElement('.btn-danger'); await driver.delay(regularDelayMs); - const confirmDeleteNetworkModal = await driver.findElement( - By.css('span .modal'), - ); + // wait for confirm delete modal to be visible + const confirmDeleteModal = await driver.findVisibleElement('span .modal'); - const byConfirmDeleteNetworkButton = By.css( + await driver.clickElement( '.button.btn-danger.modal-container__footer-button', ); - await driver.clickElement(byConfirmDeleteNetworkButton); - await driver.wait(until.stalenessOf(confirmDeleteNetworkModal)); + // wait for confirm delete modal to be removed from DOM. + await confirmDeleteModal.waitForElementState('hidden'); const newNetworkListItems = await driver.findElements( - By.css('.networks-tab__networks-list-name'), + '.networks-tab__networks-list-name', ); assert.equal(networkListItems.length - 1, newNetworkListItems.length); diff --git a/test/e2e/metrics.spec.js b/test/e2e/metrics.spec.js index 4346f59b1..fb54eea5d 100644 --- a/test/e2e/metrics.spec.js +++ b/test/e2e/metrics.spec.js @@ -1,5 +1,4 @@ const { strict: assert } = require('assert'); -const { By, Key } = require('selenium-webdriver'); const waitUntilCalled = require('../lib/wait-until-called'); const { withFixtures } = require('./helpers'); @@ -33,9 +32,8 @@ describe('Segment metrics', function () { }); 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.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); await threeSegmentEventsReceived(); diff --git a/test/e2e/mock-3box/server.js b/test/e2e/mock-3box/server.js deleted file mode 100644 index cffb15f4d..000000000 --- a/test/e2e/mock-3box/server.js +++ /dev/null @@ -1,38 +0,0 @@ -const http = require('http'); - -const port = 8889; - -const database = {}; - -const requestHandler = (request, response) => { - response.setHeader('Content-Type', 'application/json'); - if (request.method === 'POST') { - let body = ''; - request.on('data', (chunk) => { - body += chunk.toString(); // convert Buffer to string - }); - request.on('end', () => { - const { key, data } = JSON.parse(body); - - database[key] = data; - response.setHeader('Access-Control-Allow-Headers', '*'); - response.end('ok'); - }); - } else if (request.method === 'GET') { - const key = new URL(request.url, 'https://example.org/').searchParams.get( - 'key', - ); - response.setHeader('Access-Control-Allow-Headers', '*'); - response.end(JSON.stringify(database[key] || '')); - } else { - response.end('unknown request'); - } -}; - -const server = http.createServer(requestHandler); - -server.listen(port, (err) => { - if (err) { - console.log('mock 3box server error: ', err); - } -}); diff --git a/test/e2e/mock-3box/threebox-mock-server.js b/test/e2e/mock-3box/threebox-mock-server.js new file mode 100644 index 000000000..e6308bada --- /dev/null +++ b/test/e2e/mock-3box/threebox-mock-server.js @@ -0,0 +1,57 @@ +const http = require('http'); + +const PORT = 8889; + +class ThreeboxMockServer { + constructor() { + this.server = http.createServer(this.requestHandler); + this.database = {}; + } + + async start() { + return new Promise((resolve, reject) => { + this.server = this.server.listen(PORT); + this.server.once('error', reject); + this.server.once('listening', resolve); + }); + } + + async stop() { + if (!this.server) { + return; + } + + await new Promise((resolve, reject) => { + this.server.close(); + this.server.once('error', reject); + this.server.once('close', resolve); + }); + } + + requestHandler = (request, response) => { + response.setHeader('Content-Type', 'application/json'); + if (request.method === 'POST') { + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + request.on('end', () => { + const { key, data } = JSON.parse(body); + this.database[key] = data; + response.setHeader('Access-Control-Allow-Headers', '*'); + response.end('ok'); + }); + } else if (request.method === 'GET') { + const key = new URL(request.url, 'https://example.org/').searchParams.get( + 'key', + ); + + response.setHeader('Access-Control-Allow-Headers', '*'); + response.end(JSON.stringify(this.database[key] || '')); + } else { + response.end('unknown request'); + } + }; +} + +module.exports = ThreeboxMockServer; diff --git a/test/e2e/permissions.spec.js b/test/e2e/permissions.spec.js deleted file mode 100644 index 522f625dd..000000000 --- a/test/e2e/permissions.spec.js +++ /dev/null @@ -1,199 +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('sets permissions', 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.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(extension); - await driver.delay(regularDelayMs); - }); - - it('shows connected sites', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__connected-sites"]'), - ); - - await driver.findElement( - By.xpath(`//h2[contains(text(), 'Connected sites')]`), - ); - - const domains = await driver.findClickableElements( - By.css('.connected-sites-list__domain-name'), - ); - assert.equal(domains.length, 1); - }); - - it('can get accounts within the dapp', async function () { - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'eth_accounts')]`), - ); - - const getAccountsResult = await driver.findElement( - By.css('#getAccountsResult'), - ); - assert.equal( - (await getAccountsResult.getText()).toLowerCase(), - publicAddress.toLowerCase(), - ); - }); - }); -}); diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 0e604a6bb..18c3443c9 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -35,64 +35,3 @@ retry concurrently --kill-others \ 'yarn dapp' \ 'mocha test/e2e/metamask-ui.spec' -retry concurrently --kill-others \ - --names 'dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/metamask-responsive-ui.spec' - -retry concurrently --kill-others \ - --names 'dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/signature-request.spec' - -retry concurrently --kill-others \ - --names 'e2e' \ - --prefix '[{time}][{name}]' \ - --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}]' \ - --success first \ - 'yarn dapp' \ - 'mocha test/e2e/permissions.spec' - -retry concurrently --kill-others \ - --names 'sendwithprivatedapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - '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}]' \ - --success first \ - 'node test/e2e/mock-3box/server.js' \ - 'yarn dapp' \ - 'mocha test/e2e/threebox.spec' 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/signature-request.spec.js b/test/e2e/signature-request.spec.js deleted file mode 100644 index da05aa00d..000000000 --- a/test/e2e/signature-request.spec.js +++ /dev/null @@ -1,172 +0,0 @@ -const assert = require('assert'); -const path = require('path'); -const webdriver = require('selenium-webdriver'); - -const { By, Key, until } = webdriver; -const { regularDelayMs, largeDelayMs } = require('./helpers'); -const { buildWebDriver } = require('./webdriver'); -const Ganache = require('./ganache'); -const FixtureServer = require('./fixture-server'); - -const fixtureServer = new FixtureServer(); - -const ganacheServer = new Ganache(); - -describe('MetaMask', function () { - let driver; - let publicAddress; - - this.timeout(0); - this.bail(true); - - before(async function () { - await ganacheServer.start(); - await fixtureServer.start(); - await fixtureServer.loadState( - path.join(__dirname, 'fixtures', 'imported-account'), - ); - publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; - 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 fixtureServer.stop(); - await driver.quit(); - }); - - describe('successfully signs typed data', function () { - let extension; - let popup; - let dapp; - let windowHandles; - - it('accepts the account password after lock', async function () { - await driver.delay(1000); - const passwordField = await driver.findElement(By.id('password')); - await passwordField.sendKeys('correct horse battery staple'); - await passwordField.sendKeys(Key.ENTER); - await driver.delay(largeDelayMs * 4); - }); - - 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); - 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); - }); - - it('creates a sign typed data signature request', async function () { - await driver.clickElement(By.id('signTypedDataV4'), 10000); - await driver.delay(largeDelayMs); - - await driver.delay(regularDelayMs); - windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - 'MetaMask Notification', - windowHandles, - ); - await driver.delay(regularDelayMs); - - const title = await driver.findElement( - By.css('.signature-request-content__title'), - ); - const name = await driver.findElement( - By.css('.signature-request-content__info--bolded'), - ); - const content = await driver.findElements( - By.css('.signature-request-content__info'), - ); - const origin = content[0]; - const address = content[1]; - assert.equal(await title.getText(), 'Signature Request'); - assert.equal(await name.getText(), 'Ether Mail'); - assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); - assert.equal( - await address.getText(), - `${publicAddress.slice(0, 8)}...${publicAddress.slice( - publicAddress.length - 8, - )}`, - ); - }); - - it('signs the transaction', async function () { - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Sign')]`), - 10000, - ); - await driver.delay(regularDelayMs); - - extension = windowHandles[0]; - await driver.switchToWindow(extension); - }); - - it('gets the current accounts address', async function () { - await driver.clickElement( - By.css('[data-testid="account-options-menu-button"]'), - ); - await driver.clickElement( - By.css('[data-testid="account-options-menu__account-details"]'), - ); - await driver.delay(regularDelayMs); - - const addressInput = await driver.findElement( - By.css('.readonly-input__input'), - ); - const newPublicAddress = 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); - assert.equal(newPublicAddress.toLowerCase(), publicAddress); - }); - }); -}); diff --git a/test/e2e/tests/address-book.spec.js b/test/e2e/tests/address-book.spec.js new file mode 100644 index 000000000..4f047fe1b --- /dev/null +++ b/test/e2e/tests/address-book.spec.js @@ -0,0 +1,121 @@ +const { strict: assert } = require('assert'); +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(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('[data-testid="eth-overview-send"]'); + + await driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + + await driver.clickElement('.dialog.send__dialog.dialog--message'); + + // wait for address book modal to be visible + const addressModal = await driver.findElement('span .modal'); + + await driver.findElement('.add-to-address-book-modal'); + await driver.fill('.add-to-address-book-modal__input', 'Test Name 1'); + await driver.clickElement( + '.add-to-address-book-modal__footer .btn-primary', + ); + // wait for address book modal to be removed from DOM + await addressModal.waitForElementState('hidden'); + + const inputAmount = await driver.findElement('.unit-input__input'); + await inputAmount.fill('1'); + + const inputValue = await inputAmount.getAttribute('value'); + assert.equal(inputValue, '1'); + + await driver.clickElement({ text: 'Next', tag: 'button' }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }, + { timeout: 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(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('[data-testid="eth-overview-send"]'); + const recipientRowTitle = await driver.findElement( + '.send__select-recipient-wrapper__group-item__title', + ); + const recipientRowTitleString = await recipientRowTitle.getText(); + assert.equal(recipientRowTitleString, 'Test Name 1'); + await driver.clickElement( + '.send__select-recipient-wrapper__group-item', + ); + + await driver.fill('.unit-input__input', '2'); + + await driver.clickElement({ text: 'Next', tag: 'button' }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-2 ETH', + }, + { timeout: 10000 }, + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js new file mode 100644 index 000000000..4a8441937 --- /dev/null +++ b/test/e2e/tests/from-import-ui.spec.js @@ -0,0 +1,302 @@ +const { strict: assert } = require('assert'); +const { withFixtures, regularDelayMs } = require('../helpers'); +const enLocaleMessages = require('../../../app/_locales/en/messages.json'); + +describe('Metamask Import UI', function () { + it('Importing wallet using seed phrase', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: 25000000000000000000, + }, + ], + }; + const testSeedPhrase = + 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'; + const testAddress = '0x0Cc5261AB8cE458dc977078A3623E2BaDD27afD3'; + + await withFixtures( + { + fixtures: 'onboarding', + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + // clicks the continue button on the welcome screen + await driver.findElement('.welcome-page__header'); + await driver.clickElement({ + text: enLocaleMessages.getStarted.message, + tag: 'button', + }); + + // clicks the "Import Wallet" option + await driver.clickElement({ text: 'Import wallet', tag: 'button' }); + + // clicks the "No thanks" option on the metametrics opt-in screen + await driver.clickElement('.btn-default'); + + // Import seed phrase + await driver.fill( + 'input[placeholder="Paste seed phrase from clipboard"]', + testSeedPhrase, + ); + + await driver.fill('#password', 'correct horse battery staple'); + await driver.fill('#confirm-password', 'correct horse battery staple'); + + await driver.clickElement('.first-time-flow__terms'); + + await driver.clickElement({ text: 'Import', tag: 'button' }); + + // clicks through the success screen + await driver.findElement({ text: 'Congratulations', tag: 'div' }); + await driver.clickElement({ + text: enLocaleMessages.endOfFlowMessage10.message, + tag: 'button', + }); + + // close the what's new popup + const popover = await driver.findElement('.popover-container'); + await driver.clickElement('[data-testid="popover-close"]'); + await popover.waitForElementState('hidden'); + + // Show account information + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu__account-details"]', + ); + await driver.findVisibleElement('.qr-code__wrapper'); + // shows a QR code for the account + const detailsModal = await driver.findVisibleElement('span .modal'); + // shows the correct account address + const [address] = await driver.findElements('.readonly-input__input'); + assert.equal(await address.getAttribute('value'), testAddress); + + await driver.clickElement('.account-modal__close'); + await detailsModal.waitForElementState('hidden'); + + // logs out of the account + await driver.clickElement('.account-menu__icon .identicon'); + const lockButton = await driver.findClickableElement( + '.account-menu__lock-button', + ); + assert.equal(await lockButton.getText(), 'Lock'); + await lockButton.click(); + + // accepts the account password after lock + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // Create a new account + // switches to locakhost + await driver.clickElement('.network-display'); + await driver.clickElement({ text: 'Localhost', tag: 'span' }); + + // choose Create Account from the account menu + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Create Account', tag: 'div' }); + + // set account name + await driver.fill('.new-account-create-form input', '2nd account'); + await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Create', tag: 'button' }); + + // should show the correct account name + const accountName = await driver.findElement('.selected-account__name'); + assert.equal(await accountName.getText(), '2nd account'); + + // Switch back to original account + // chooses the original account from the account menu + await driver.clickElement('.account-menu__icon'); + await driver.clickElement('.account-menu__name'); + + // Send ETH from inside MetaMask + // starts a send transaction + await driver.clickElement('[data-testid="eth-overview-send"]'); + await driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + await driver.fill('.unit-input__input', '1'); + + // Set the gas limit + await driver.clickElement('.advanced-gas-options-btn'); + + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement({ text: 'Save', tag: 'button' }); + // wait for gas modal to be removed from DOM + await gasModal.waitForElementState('hidden'); + + // Continue to next screen + await driver.clickElement({ text: 'Next', tag: 'button' }); + + // confirms the transaction + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // finds the transaction in the transactions list + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + + const txValues = await driver.findElements( + '.transaction-list-item__primary-currency', + ); + assert.equal(txValues.length, 1); + assert.ok(/-1\s*ETH/u.test(await txValues[0].getText())); + }, + ); + }); + + it('Import Account using private key', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: 25000000000000000000, + }, + ], + }; + const testPrivateKey1 = + '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; + const testPrivateKey2 = + 'F4EC2590A0C10DE95FBF4547845178910E40F5035320C516A18C117DE02B5669'; + + await withFixtures( + { + fixtures: 'import-ui', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // Imports an account with private key + // choose Create Account from the account menu + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Import Account', tag: 'div' }); + + // enter private key', + await driver.fill('#private-key-box', testPrivateKey1); + await driver.clickElement({ text: 'Import', tag: 'button' }); + + // should show the correct account name + const importedAccountName = await driver.findElement( + '.selected-account__name', + ); + assert.equal(await importedAccountName.getText(), 'Account 4'); + + // should show the imported label + await driver.clickElement('.account-menu__icon'); + // confirm 4th account is account 4, as expected + const accountMenuItemSelector = '.account-menu__account:nth-child(4)'; + const fourthAccountName = await driver.findElement( + `${accountMenuItemSelector} .account-menu__name`, + ); + assert.equal(await fourthAccountName.getText(), 'Account 4'); + // confirm label is present on the same menu item + const importedLabel = await driver.findElement( + `${accountMenuItemSelector} .keyring-label`, + ); + assert.equal(await importedLabel.getText(), 'IMPORTED'); + + // Imports and removes an account + // choose Create Account from the account menu + await driver.clickElement({ text: 'Import Account', tag: 'div' }); + // enter private key + await driver.fill('#private-key-box', testPrivateKey2); + await driver.clickElement({ text: 'Import', tag: 'button' }); + + // should see new account in account menu + const importedAccount2Name = await driver.findElement( + '.selected-account__name', + ); + assert.equal(await importedAccount2Name.getText(), 'Account 5'); + await driver.clickElement('.account-menu__icon'); + const accountListItems = await driver.findElements( + '.account-menu__account', + ); + assert.equal(accountListItems.length, 5); + + await driver.clickPoint('.account-menu__icon', 0, 0); + + // should open the remove account modal + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu__remove-account"]', + ); + await driver.findElement('.confirm-remove-account__account'); + + // should remove the account + await driver.clickElement({ text: 'Remove', tag: 'button' }); + + const currentActiveAccountName = await driver.findElement( + '.selected-account__name', + ); + assert.equal(await currentActiveAccountName.getText(), 'Account 1'); + await driver.delay(regularDelayMs); + await driver.clickElement('.account-menu__icon'); + + const accountListItemsAgfterRemoval = await driver.findElements( + '.account-menu__account', + ); + assert.equal(accountListItemsAgfterRemoval.length, 4); + }, + ); + }); + it('Connects to a Hardware wallet', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: 25000000000000000000, + }, + ], + }; + + await withFixtures( + { + fixtures: 'import-ui', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // choose Connect Hardware Wallet from the account menu + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ + text: 'Connect Hardware Wallet', + tag: 'div', + }); + await driver.delay(regularDelayMs); + + // should open the TREZOR Connect popup + await driver.clickElement('.hw-connect__btn:nth-of-type(2)'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + const allWindows = await driver.getAllWindowHandles(); + assert.equal(allWindows.length, 2); + }, + ); + }); +}); diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js new file mode 100644 index 000000000..6ecd5580e --- /dev/null +++ b/test/e2e/tests/incremental-security.spec.js @@ -0,0 +1,179 @@ +const { strict: assert } = require('assert'); +const { withFixtures, tinyDelayMs } = require('../helpers'); +const enLocaleMessages = require('../../../app/_locales/en/messages.json'); + +describe('Incremental Security', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1', + balance: 0, + }, + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: 25000000000000000000, + }, + ], + }; + it('Back up seed phrase from backup reminder', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'onboarding', + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + dappPath: 'send-eth-with-private-key-test', + }, + async ({ driver }) => { + await driver.navigate(); + await driver.delay(tinyDelayMs); + + // clicks the continue button on the welcome screen + await driver.findElement('.welcome-page__header'); + await driver.clickElement({ + text: enLocaleMessages.getStarted.message, + tag: 'button', + }); + + // clicks the "Create New Wallet" option + await driver.clickElement({ text: 'Create a Wallet', tag: 'button' }); + + // clicks the "No thanks" option on the metametrics opt-in screen + await driver.clickElement('.btn-default'); + + // accepts a secure password + await driver.fill( + '.first-time-flow__form #create-password', + 'correct horse battery staple', + ); + await driver.fill( + '.first-time-flow__form #confirm-password', + 'correct horse battery staple', + ); + await driver.clickElement('.first-time-flow__checkbox'); + await driver.clickElement('.first-time-flow__form button'); + + // skips the seed phrase challenge + await driver.clickElement({ + text: enLocaleMessages.remindMeLater.message, + tag: 'button', + }); + + // closes the what's new popup + const popover = await driver.findElement('.popover-container'); + + await driver.clickElement('[data-testid="popover-close"]'); + + await popover.waitForElementState('hidden'); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu__account-details"]', + ); + + // gets the current accounts address + const addressInput = await driver.findElement('.readonly-input__input'); + const publicAddress = await addressInput.getAttribute('value'); + + // wait for account modal to be visible + const accountModal = await driver.findVisibleElement('span .modal'); + + await driver.clickElement('.account-modal__close'); + + // wait for account modal to be removed from DOM + await accountModal.waitForElementState('hidden'); + + // send to current account from dapp with different provider + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + + // switched to Dapp + await driver.openNewPage('http://127.0.0.1:8080/'); + + // sends eth to the current account + await driver.fill('#address', publicAddress); + await driver.clickElement('#send'); + + await driver.waitForSelector( + { css: '#success', text: 'Success' }, + { timeout: 15000 }, + ); + + // switch to extension + await driver.switchToWindow(extension); + + // should have the correct amount of eth + let currencyDisplay = await driver.waitForSelector({ + css: '.currency-display-component__text', + text: '1', + }); + let balance = await currencyDisplay.getText(); + assert.strictEqual(balance, '1'); + + // backs up the seed phrase + // should show a backup reminder + const backupReminder = await driver.findElements({ + xpath: + "//div[contains(@class, 'home-notification__text') and contains(text(), 'Backup your Secret Recovery code to keep your wallet and funds secure')]", + }); + assert.equal(backupReminder.length, 1); + + // should take the user to the seedphrase backup screen + await driver.clickElement('.home-notification__accept-button'); + + // reveals the seed phrase + await driver.clickElement( + '.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button', + ); + + const revealedSeedPhrase = await driver.findElement( + '.reveal-seed-phrase__secret-words', + ); + const seedPhrase = await revealedSeedPhrase.getText(); + assert.equal(seedPhrase.split(' ').length, 12); + + await driver.clickElement({ + text: enLocaleMessages.next.message, + tag: 'button', + }); + + // selecting the words from seedphrase + async function clickWordAndWait(word) { + await driver.clickElement( + `[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`, + ); + await driver.delay(tinyDelayMs); + } + + // can retype the seed phrase + const words = seedPhrase.split(' '); + + for (const word of words) { + await clickWordAndWait(word); + } + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // can click through the success screen + await driver.clickElement({ text: 'All Done', tag: 'button' }); + + // should have the correct amount of eth + currencyDisplay = await driver.waitForSelector({ + css: '.currency-display-component__text', + text: '1', + }); + balance = await currencyDisplay.getText(); + + assert.strictEqual(balance, '1'); + + // should not show a backup reminder + await driver.assertElementNotPresent('.backup-notification'); + }, + ); + }); +}); diff --git a/test/e2e/tests/localization.spec.js b/test/e2e/tests/localization.spec.js index 27b93a269..e9d6e0f7e 100644 --- a/test/e2e/tests/localization.spec.js +++ b/test/e2e/tests/localization.spec.js @@ -1,5 +1,4 @@ const { strict: assert } = require('assert'); -const { By, Key } = require('selenium-webdriver'); const { withFixtures } = require('../helpers'); describe('Localization', function () { @@ -17,11 +16,10 @@ describe('Localization', function () { { fixtures: 'localization', 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.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); const secondaryBalance = await driver.findElement( - By.css('[data-testid="eth-overview__secondary-currency"]'), + '[data-testid="eth-overview__secondary-currency"]', ); const secondaryBalanceText = await secondaryBalance.getText(); const [fiatAmount, fiatUnit] = secondaryBalanceText diff --git a/test/e2e/tests/metamask-responsive-ui.spec.js b/test/e2e/tests/metamask-responsive-ui.spec.js new file mode 100644 index 000000000..8627b20d0 --- /dev/null +++ b/test/e2e/tests/metamask-responsive-ui.spec.js @@ -0,0 +1,204 @@ +const { strict: assert } = require('assert'); +const { withFixtures, tinyDelayMs } = require('../helpers'); +const enLocaleMessages = require('../../../app/_locales/en/messages.json'); + +describe('Metamask Responsive UI', function () { + it('Creating a new wallet', async function () { + const driverOptions = { responsive: true }; + + await withFixtures( + { + fixtures: 'onboarding', + driverOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + // clicks the continue button on the welcome screen + await driver.findElement('.welcome-page__header'); + await driver.clickElement({ + text: enLocaleMessages.getStarted.message, + tag: 'button', + }); + await driver.delay(tinyDelayMs); + + // clicks the "Create New Wallet" option + await driver.clickElement({ text: 'Create a Wallet', tag: 'button' }); + + // clicks the "I Agree" option on the metametrics opt-in screen + await driver.clickElement('.btn-primary'); + + // accepts a secure password + await driver.fill( + '.first-time-flow__form #create-password', + 'correct horse battery staple', + ); + await driver.fill( + '.first-time-flow__form #confirm-password', + 'correct horse battery staple', + ); + await driver.clickElement('.first-time-flow__checkbox'); + await driver.clickElement('.first-time-flow__form button'); + + // reveals the seed phrase + await driver.clickElement( + '.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button', + ); + const revealedSeedPhrase = await driver.findElement( + '.reveal-seed-phrase__secret-words', + ); + const seedPhrase = await revealedSeedPhrase.getText(); + assert.equal(seedPhrase.split(' ').length, 12); + + await driver.clickElement({ + text: enLocaleMessages.next.message, + tag: 'button', + }); + + async function clickWordAndWait(word) { + await driver.clickElement( + `[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`, + ); + await driver.delay(tinyDelayMs); + } + + // can retype the seed phrase + const words = seedPhrase.split(' '); + for (const word of words) { + await clickWordAndWait(word); + } + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // clicks through the success screen + await driver.findElement({ text: 'Congratulations', tag: 'div' }); + await driver.clickElement({ + text: enLocaleMessages.endOfFlowMessage10.message, + tag: 'button', + }); + + // Show account information + // balance renders + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: '0 ETH', + }); + }, + ); + }); + + it('Importing existing wallet from lock page', async function () { + const driverOptions = { responsive: true }; + const testSeedPhrase = + 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'; + + await withFixtures( + { + fixtures: 'imported-account', + driverOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + + // Import seed phrase + const restoreSeedLink = await driver.findClickableElement( + '.unlock-page__link--import', + ); + assert.equal( + await restoreSeedLink.getText(), + 'import using seed phrase', + ); + await restoreSeedLink.click(); + + await driver.clickElement('.import-account__checkbox-container'); + + await driver.fill('.import-account__secret-phrase', testSeedPhrase); + + await driver.fill('#password', 'correct horse battery staple'); + await driver.fill('#confirm-password', 'correct horse battery staple'); + await driver.clickElement({ + text: enLocaleMessages.restore.message, + tag: 'button', + }); + + // balance renders + await driver.waitForSelector({ + css: '[data-testid="eth-overview__primary-currency"]', + text: '100 ETH', + }); + }, + ); + }); + + it('Send Transaction from responsive window', async function () { + const driverOptions = { responsive: true }; + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + await withFixtures( + { + fixtures: 'imported-account', + driverOptions, + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // Send ETH from inside MetaMask + // starts to send a transaction + await driver.clickElement('[data-testid="eth-overview-send"]'); + + await driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + + const inputAmount = await driver.fill('.unit-input__input', '1'); + + const inputValue = await inputAmount.getAttribute('value'); + assert.equal(inputValue, '1'); + + // opens and closes the gas modal + await driver.clickElement('.advanced-gas-options-btn'); + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + await driver.clickElement('.page-container__header-close-text'); + // wait for gas modal to be removed from dom + await gasModal.waitForElementState('hidden'); + + // confirming transcation + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // finds the transaction in the transactions list + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + + await driver.waitForSelector( + { + css: '.transaction-list-item__primary-currency', + text: '-1 ETH', + }, + { timeout: 10000 }, + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/permissions.spec.js b/test/e2e/tests/permissions.spec.js new file mode 100644 index 000000000..afc860731 --- /dev/null +++ b/test/e2e/tests/permissions.spec.js @@ -0,0 +1,88 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Permissions', function () { + it('sets permissions and connect to Dapp', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindow(extension); + + // shows connected sites + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu__connected-sites"]', + ); + + await driver.findElement({ + text: 'Connected sites', + tag: 'h2', + }); + + const domains = await driver.findClickableElements( + '.connected-sites-list__domain-name', + ); + assert.equal(domains.length, 1); + + // can get accounts within the dapp + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + + await driver.clickElement({ + text: 'eth_accounts', + tag: 'button', + }); + + const getAccountsResult = await driver.findElement( + '#getAccountsResult', + ); + assert.equal( + (await getAccountsResult.getText()).toLowerCase(), + publicAddress.toLowerCase(), + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/personal-sign.spec.js b/test/e2e/tests/personal-sign.spec.js index e9efc5ea4..7f3480aa7 100644 --- a/test/e2e/tests/personal-sign.spec.js +++ b/test/e2e/tests/personal-sign.spec.js @@ -1,5 +1,4 @@ const { strict: assert } = require('assert'); -const { By, Key } = require('selenium-webdriver'); const { withFixtures } = require('../helpers'); describe('Personal sign', function () { @@ -16,18 +15,17 @@ describe('Personal sign', function () { await withFixtures( { dapp: true, - fixtures: 'personal-sign', + 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.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); await driver.openNewPage('http://127.0.0.1:8080/'); - await driver.clickElement(By.id('personalSign')); + await driver.clickElement('#personalSign'); await driver.waitUntilXWindowHandles(3); @@ -38,14 +36,12 @@ describe('Personal sign', function () { ); const personalMessageRow = await driver.findElement( - By.css('.request-signature__row-value'), + '.request-signature__row-value', ); const personalMessage = await personalMessageRow.getText(); assert.equal(personalMessage, 'Example `personal_sign` message'); - await driver.clickElement( - By.css('[data-testid="request-signature__sign"]'), - ); + await driver.clickElement('[data-testid="request-signature__sign"]'); await driver.waitUntilXWindowHandles(2); }, diff --git a/test/e2e/tests/provider-events.spec.js b/test/e2e/tests/provider-events.spec.js new file mode 100644 index 000000000..0949dbf46 --- /dev/null +++ b/test/e2e/tests/provider-events.spec.js @@ -0,0 +1,55 @@ +const { strict: assert } = require('assert'); +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(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.openNewPage('http://127.0.0.1:8080/'); + const networkDiv = await driver.findElement('#network'); + const chainIdDiv = await driver.findElement('#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('.network-display'); + await driver.clickElement({ text: 'Ropsten', tag: 'span' }); + await driver.delay(regularDelayMs); + + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + const switchedNetworkDiv = await driver.findElement('#network'); + const switchedChainIdDiv = await driver.findElement('#chainId'); + const accountsDiv = await driver.findElement('#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..c4b8a5ad5 --- /dev/null +++ b/test/e2e/tests/send-edit.spec.js @@ -0,0 +1,96 @@ +const { strict: assert } = require('assert'); +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(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + const transactionAmounts = await driver.findElements( + '.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( + '.confirm-page-container-header__back-button', + ); + await driver.fill('.unit-input__input', '2.2'); + + await driver.clickElement('.advanced-gas-options-btn'); + await driver.delay(regularDelayMs); + + // wait for gas modal to be visible + const gasModal = await driver.findVisibleElement('span .modal'); + + const [gasPriceInput, gasLimitInput] = await driver.findElements( + '.advanced-gas-inputs__gas-edit-row__input', + ); + + await gasPriceInput.fill('8'); + await driver.delay(tinyDelayMs); + + await gasLimitInput.fill('100000'); + await driver.delay(largeDelayMs); + + await driver.clickElement({ text: 'Save', tag: 'button' }); + // Wait for gas modal to be removed from DOM + await gasModal.waitForElementState('hidden'); + await driver.clickElement({ text: 'Next', tag: 'button' }); + + // has correct updated value on the confirm screen the transaction + const editedTransactionAmounts = await driver.findElements( + '.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({ text: 'Confirm', tag: 'button' }); + await driver.delay(regularDelayMs); + + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + + const txValues = await driver.findElements( + '.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/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js new file mode 100644 index 000000000..3c59f7c69 --- /dev/null +++ b/test/e2e/tests/signature-request.spec.js @@ -0,0 +1,74 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Signature Request', function () { + it('can initiate and confirm a Signature Request', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + const publicAddress = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; + await withFixtures( + { + dapp: true, + fixtures: 'connected-state', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.openNewPage('http://127.0.0.1:8080/'); + + // creates a sign typed data signature request + await driver.clickElement('#signTypedDataV4', 10000); + + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + + const title = await driver.findElement( + '.signature-request-content__title', + ); + const name = await driver.findElement( + '.signature-request-content__info--bolded', + ); + const content = await driver.findElements( + '.signature-request-content__info', + ); + const origin = content[0]; + const address = content[1]; + assert.equal(await title.getText(), 'Signature Request'); + assert.equal(await name.getText(), 'Ether Mail'); + assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); + assert.equal( + await address.getText(), + `${publicAddress.slice(0, 8)}...${publicAddress.slice( + publicAddress.length - 8, + )}`, + ); + + // Approve signing typed data + await driver.clickElement({ text: 'Sign', tag: 'button' }, 10000); + + // switch to the Dapp and verify the signed addressed + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.clickElement('#signTypedDataV4Verify', 10000); + const recoveredAddress = await driver.findElement( + '#signTypedDataV4VerifyResult', + ); + assert.equal(await recoveredAddress.getText(), publicAddress); + }, + ); + }); +}); diff --git a/test/e2e/tests/simple-send.spec.js b/test/e2e/tests/simple-send.spec.js index e7564a071..21502e836 100644 --- a/test/e2e/tests/simple-send.spec.js +++ b/test/e2e/tests/simple-send.spec.js @@ -1,4 +1,3 @@ -const { By, Key } = require('selenium-webdriver'); const { withFixtures } = require('../helpers'); describe('Simple send', function () { @@ -16,28 +15,18 @@ describe('Simple send', function () { { 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 recipientAddressField = await driver.findElement( - By.css('[data-testid="ens-input"]'), - ); - await recipientAddressField.sendKeys( + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + await driver.clickElement('[data-testid="eth-overview-send"]'); + await driver.fill( + '[data-testid="ens-input"]', '0x985c30949c92df7a0bd42e0f3e3d539ece98db24', ); - const amountField = await driver.findElement( - By.css('.unit-input__input'), - ); - await amountField.sendKeys('1'); - await driver.clickElement( - By.css('[data-testid="page-container-footer-next"]'), - ); - await driver.clickElement( - By.css('[data-testid="page-container-footer-next"]'), - ); - await driver.clickElement(By.css('[data-testid="home__activity-tab"]')); - await driver.findElement(By.css('.transaction-list-item')); + await driver.fill('.unit-input__input', '1'); + await driver.clickElement('[data-testid="page-container-footer-next"]'); + await driver.clickElement('[data-testid="page-container-footer-next"]'); + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.findElement('.transaction-list-item'); }, ); }); diff --git a/test/e2e/tests/threebox.spec.js b/test/e2e/tests/threebox.spec.js new file mode 100644 index 000000000..cae0ebca8 --- /dev/null +++ b/test/e2e/tests/threebox.spec.js @@ -0,0 +1,97 @@ +const { strict: assert } = require('assert'); +const { withFixtures, largeDelayMs } = require('../helpers'); +const ThreeboxMockServer = require('../mock-3box/threebox-mock-server'); + +describe('Threebox', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + let threeboxServer; + before(async function () { + threeboxServer = new ThreeboxMockServer(); + await threeboxServer.start(); + }); + after(async function () { + await threeboxServer.stop(); + }); + it('Set up data to be restored by 3box', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // turns on threebox syncing + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + // turns on threebox syncing + await driver.clickElement({ text: 'Advanced', tag: 'div' }); + await driver.clickElement( + '[data-testid="advanced-setting-3box"] .toggle-button div', + ); + + // updates settings and address book + // navigates to General settings + await driver.clickElement({ text: 'General', tag: 'div' }); + + // turns on use of blockies + await driver.clickElement('.toggle-button > div'); + + // adds an address to the contact list + await driver.clickElement({ text: 'Contacts', tag: 'div' }); + + await driver.clickElement('.address-book__link'); + await driver.fill('#nickname', 'Test User Name 11'); + await driver.fill( + 'input[placeholder="Search, public address (0x), or ENS"]', + '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + ); + await driver.delay(largeDelayMs * 2); + await driver.clickElement({ text: 'Save', tag: 'button' }); + await driver.findElement({ text: 'Test User Name 11', tag: 'div' }); + }, + ); + }); + it('Restore from 3box', async function () { + await withFixtures( + { + fixtures: 'threebox-enabled', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // confirms the 3box restore notification + await driver.clickElement('.home-notification__accept-button'); + + // goes to the settings screen + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + // finds the blockies toggle turned on + const toggleLabel = await driver.findElement('.toggle-button__status'); + const toggleLabelText = await toggleLabel.getText(); + assert.equal(toggleLabelText, 'ON'); + + // finds the restored address in the contact list + await driver.clickElement({ text: 'Contacts', tag: 'div' }); + await driver.findElement({ text: 'Test User Name 11', tag: 'div' }); + }, + ); + }); +}); diff --git a/test/e2e/threebox.spec.js b/test/e2e/threebox.spec.js deleted file mode 100644 index 207f15afd..000000000 --- a/test/e2e/threebox.spec.js +++ /dev/null @@ -1,302 +0,0 @@ -const assert = require('assert'); -const webdriver = require('selenium-webdriver'); -const getPort = require('get-port'); - -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('MetaMask', 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({ port: await getPort() }); - 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('set up data to be restored by 3box', function () { - 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); - }); - - 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('turns on threebox syncing', function () { - it('goes to the settings screen', async function () { - await driver.clickElement(By.css('.account-menu__icon')); - await driver.delay(regularDelayMs); - - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Settings')]`), - ); - }); - - it('turns on threebox syncing', async function () { - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Advanced')]`), - ); - await driver.clickElement( - By.css('[data-testid="advanced-setting-3box"] .toggle-button div'), - ); - }); - }); - - describe('updates settings and address book', function () { - it('navigates to General settings', async function () { - await driver.clickElement( - By.xpath(`//div[contains(text(), 'General')]`), - ); - }); - - it('turns on use of blockies', async function () { - await driver.clickElement(By.css('.toggle-button > div')); - }); - - it('adds an address to the contact list', async function () { - await driver.clickElement( - By.xpath(`//div[contains(text(), 'Contacts')]`), - ); - - await driver.clickElement(By.css('.address-book-add-button__button')); - await driver.delay(tinyDelayMs); - - const addAddressInputs = await driver.findElements(By.css('input')); - await addAddressInputs[0].sendKeys('Test User Name 11'); - - await driver.delay(tinyDelayMs); - - await addAddressInputs[1].sendKeys( - '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - ); - - await driver.delay(largeDelayMs * 2); - - await driver.clickElement( - By.xpath(`//button[contains(text(), 'Save')]`), - ); - - await driver.findElement( - By.xpath(`//div[contains(text(), 'Test User Name 11')]`), - ); - await driver.delay(regularDelayMs); - }); - }); - }); - - describe('restoration from 3box', function () { - let driver2; - - before(async function () { - const result = await buildWebDriver({ port: await getPort() }); - driver2 = result.driver; - await driver2.navigate(); - }); - - after(async function () { - await driver2.quit(); - }); - - describe('First time flow starting from an existing seed phrase', function () { - it('clicks the continue button on the welcome screen', async function () { - await driver2.findElement(By.css('.welcome-page__header')); - await driver2.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`, - ), - ); - await driver2.delay(largeDelayMs); - }); - - it('clicks the "Import Wallet" option', async function () { - await driver2.clickElement( - By.xpath(`//button[contains(text(), 'Import wallet')]`), - ); - await driver2.delay(largeDelayMs); - }); - - it('clicks the "No thanks" option on the metametrics opt-in screen', async function () { - await driver2.clickElement(By.css('.btn-default')); - await driver2.delay(largeDelayMs); - }); - - it('imports a seed phrase', async function () { - const [seedTextArea] = await driver2.findElements( - By.css('input[placeholder="Paste seed phrase from clipboard"]'), - ); - await seedTextArea.sendKeys(testSeedPhrase); - await driver2.delay(regularDelayMs); - - const [password] = await driver2.findElements(By.id('password')); - await password.sendKeys('correct horse battery staple'); - const [confirmPassword] = await driver2.findElements( - By.id('confirm-password'), - ); - confirmPassword.sendKeys('correct horse battery staple'); - - await driver2.clickElement(By.css('.first-time-flow__terms')); - - await driver2.clickElement( - By.xpath(`//button[contains(text(), 'Import')]`), - ); - await driver2.delay(regularDelayMs); - }); - - it('clicks through the success screen', async function () { - await driver2.findElement( - By.xpath(`//div[contains(text(), 'Congratulations')]`), - ); - await driver2.clickElement( - By.xpath( - `//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`, - ), - ); - await driver2.delay(regularDelayMs); - }); - - it('balance renders', async function () { - const balance = await driver2.findElement( - By.css('[data-testid="wallet-balance"] .list-item__heading'), - ); - await driver2.wait(until.elementTextMatches(balance, /25\s*ETH/u)); - await driver2.delay(regularDelayMs); - }); - }); - - describe('restores 3box data', function () { - it('confirms the 3box restore notification', async function () { - await driver2.clickElement(By.css('.home-notification__accept-button')); - }); - - it('goes to the settings screen', async function () { - await driver2.clickElement(By.css('.account-menu__icon')); - await driver2.delay(regularDelayMs); - - await driver2.clickElement( - By.xpath(`//div[contains(text(), 'Settings')]`), - ); - }); - - it('finds the blockies toggle turned on', async function () { - await driver2.delay(regularDelayMs); - const toggleLabel = await driver2.findElement( - By.css('.toggle-button__status'), - ); - const toggleLabelText = await toggleLabel.getText(); - assert.equal(toggleLabelText, 'ON'); - }); - - it('finds the restored address in the contact list', async function () { - await driver2.clickElement( - By.xpath(`//div[contains(text(), 'Contacts')]`), - ); - await driver2.delay(regularDelayMs); - - await driver2.findElement( - By.xpath(`//div[contains(text(), 'Test User Name 11')]`), - ); - await driver2.delay(regularDelayMs); - }); - }); - }); -}); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index f1da84a21..0188c8257 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1,6 +1,33 @@ 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'); +const cssToXPath = require('css-to-xpath'); + +/** + * Temporary workaround to patch selenium's element handle API with methods + * that match the playwright API for Elements + * @param {Object} element - Selenium Element + * @returns {Object} modified Selenium Element + */ +function wrapElementWithAPI(element, driver) { + element.press = (key) => element.sendKeys(key); + element.fill = async (input) => { + // The 'fill' method in playwright replaces existing input + await element.clear(); + await element.sendKeys(input); + }; + element.waitForElementState = async (state, timeout) => { + switch (state) { + case 'hidden': + return await driver.wait(until.stalenessOf(element), timeout); + case 'visible': + return await driver.wait(until.elementIsVisible(element), timeout); + default: + throw new Error(`Provided state: '${state}' is not supported`); + } + }; + return element; +} class Driver { /** @@ -13,6 +40,72 @@ class Driver { this.browser = browser; this.extensionUrl = extensionUrl; this.timeout = timeout; + // The following values are found in + // https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110 + // These should be replaced with string constants 'Enter' etc for playwright. + this.Key = { + BACK_SPACE: '\uE003', + ENTER: '\uE007', + }; + } + + 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 or css prop, will use + // xpath to look for an element with the tag that has matching text. + if (locator.css) { + // When providing css prop we use cssToXPath to build a xpath string + // We provide two cases to check for, first a text node of the + // element that matches the text provided OR we test the stringified + // contents of the element in the case where text is split across + // multiple children. In the later case non literal spaces are stripped + // so we do the same with the input to provide a consistent API. + const xpath = cssToXPath + .parse(locator.css) + .where( + cssToXPath.xPathBuilder + .string() + .contains(locator.text) + .or( + cssToXPath.xPathBuilder + .string() + .contains(locator.text.split(' ').join('')), + ), + ) + .toXPath(); + return By.xpath(xpath); + } + // The tag prop is optional and further refines which elements match + return By.xpath( + `//${locator.tag ?? '*'}[contains(text(), '${locator.text}')]`, + ); + } + throw new Error( + `The locator '${locator}' is not supported by the E2E test driver`, + ); + } + + async fill(rawLocator, input) { + const element = await this.findElement(rawLocator); + await element.fill(input); + return element; + } + + async press(rawLocator, keys) { + const element = await this.findElement(rawLocator); + await element.press(keys); + return element; } async delay(time) { @@ -23,36 +116,73 @@ class Driver { await this.driver.wait(condition, timeout); } + async waitForSelector( + rawLocator, + { timeout = this.timeout, state = 'visible' } = {}, + ) { + // Playwright has a waitForSelector method that will become a shallow + // replacement for the implementation below. It takes an option options + // bucket that can include the state attribute to wait for elements that + // match the selector to be removed from the DOM. + const selector = this.buildLocator(rawLocator); + let element; + if (!['visible', 'detached'].includes(state)) { + throw new Error(`Provided state selector ${state} is not supported`); + } + if (state === 'visible') { + element = await this.driver.wait(until.elementLocated(selector), timeout); + } else if (state === 'detached') { + element = await this.driver.wait( + until.stalenessOf(await this.findElement(selector)), + timeout, + ); + } + return wrapElementWithAPI(element, this); + } + async quit() { await this.driver.quit(); } // Element interactions - async findElement(locator) { - return await this.driver.wait(until.elementLocated(locator), this.timeout); + async findElement(rawLocator) { + const locator = this.buildLocator(rawLocator); + const element = await this.driver.wait( + until.elementLocated(locator), + this.timeout, + ); + return wrapElementWithAPI(element, this); } - 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; + return wrapElementWithAPI(element, this); } - 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), this.driver.wait(until.elementIsEnabled(element), this.timeout), ]); - return element; + return wrapElementWithAPI(element, this); } - async findElements(locator) { - return await this.driver.wait(until.elementsLocated(locator), this.timeout); + async findElements(rawLocator) { + const locator = this.buildLocator(rawLocator); + const elements = await this.driver.wait( + until.elementsLocated(locator), + this.timeout, + ); + return elements.map((element) => wrapElementWithAPI(element, this)); } - async findClickableElements(locator) { + async findClickableElements(rawLocator) { + const locator = this.buildLocator(rawLocator); const elements = await this.findElements(locator); await Promise.all( elements.reduce((acc, element) => { @@ -63,15 +193,17 @@ class Driver { return acc; }, []), ); - return elements; + return elements.map((element) => wrapElementWithAPI(element, this)); } - 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 +219,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/jest/background.js b/test/jest/background.js new file mode 100644 index 000000000..49a6bdf8f --- /dev/null +++ b/test/jest/background.js @@ -0,0 +1,5 @@ +import * as actions from '../../ui/app/store/actions'; + +export const setBackgroundConnection = (backgroundConnection = {}) => { + actions._setBackgroundConnection(backgroundConnection); +}; diff --git a/test/jest/constants.js b/test/jest/constants.js new file mode 100644 index 000000000..2ed360574 --- /dev/null +++ b/test/jest/constants.js @@ -0,0 +1 @@ +export const METASWAP_BASE_URL = 'https://api.metaswap.codefi.network'; diff --git a/test/jest/index.js b/test/jest/index.js new file mode 100644 index 000000000..098877489 --- /dev/null +++ b/test/jest/index.js @@ -0,0 +1,5 @@ +export { createSwapsMockStore } from './mock-store'; +export { renderWithProvider } from './rendering'; +export { setBackgroundConnection } from './background'; +export * as MOCKS from './mocks'; +export * as CONSTANTS from './constants'; diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js new file mode 100644 index 000000000..88d486193 --- /dev/null +++ b/test/jest/mock-store.js @@ -0,0 +1,79 @@ +import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; + +export const createSwapsMockStore = () => { + return { + swaps: { + customGas: { + fallBackPrice: 5, + }, + fromToken: 'ETH', + }, + metamask: { + provider: { + chainId: MAINNET_CHAIN_ID, + }, + cachedBalances: { + [MAINNET_CHAIN_ID]: 5, + }, + preferences: { + showFiatInTestnets: true, + }, + currentCurrency: 'ETH', + conversionRate: 1, + contractExchangeRates: { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2, + '0x1111111111111111111111111111111111111111': 0.1, + }, + accounts: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + balance: '0x0', + }, + '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + balance: '0x0', + }, + }, + selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + frequentRpcListDetail: [], + tokens: [ + { + erc20: true, + symbol: 'BAT', + decimals: 18, + address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', + }, + { + erc20: true, + symbol: 'USDT', + decimals: 6, + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + }, + ], + swapsState: { + quotes: {}, + fetchParams: { + metaData: { + sourceTokenInfo: { + symbol: 'BAT', + }, + destinationTokenInfo: { + symbol: 'ETH', + }, + }, + }, + tradeTxId: null, + approveTxId: null, + quotesLastFetched: null, + customMaxGas: '', + customGasPrice: null, + selectedAggId: null, + customApproveTxData: '', + errorKey: '', + topAggId: null, + routeState: '', + swapsFeatureIsLive: false, + }, + }, + }; +}; diff --git a/test/jest/mocks.js b/test/jest/mocks.js new file mode 100644 index 000000000..c57b2068c --- /dev/null +++ b/test/jest/mocks.js @@ -0,0 +1,61 @@ +export const TOP_ASSETS_GET_RESPONSE = [ + { + symbol: 'LINK', + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + }, + { + symbol: 'UMA', + address: '0x04fa0d235c4abf4bcf4787af4cf447de572ef828', + }, + { + symbol: 'YFI', + address: '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e', + }, + { + symbol: 'LEND', + address: '0x80fb784b7ed66730e8b1dbd9820afd29931aab03', + }, + { + symbol: 'SNX', + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + }, +]; + +export const REFRESH_TIME_GET_RESPONSE = { + seconds: 3600, +}; + +export const AGGREGATOR_METADATA_GET_RESPONSE = {}; + +export const GAS_PRICES_GET_RESPONSE = { + SafeGasPrice: '10', + ProposeGasPrice: '20', + FastGasPrice: '30', +}; + +export const TOKENS_GET_RESPONSE = [ + { + erc20: true, + symbol: 'META', + decimals: 18, + address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', + }, + { + erc20: true, + symbol: 'ZRX', + decimals: 18, + address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', + }, + { + erc20: true, + symbol: 'AST', + decimals: 4, + address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', + }, + { + erc20: true, + symbol: 'BAT', + decimals: 18, + address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', + }, +]; diff --git a/test/jest/rendering.js b/test/jest/rendering.js new file mode 100644 index 000000000..d215cbc0d --- /dev/null +++ b/test/jest/rendering.js @@ -0,0 +1,59 @@ +import React, { useMemo } from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +import { I18nContext, LegacyI18nProvider } from '../../ui/app/contexts/i18n'; +import { getMessage } from '../../ui/app/helpers/utils/i18n-helper'; +import * as en from '../../app/_locales/en/messages.json'; + +export const I18nProvider = (props) => { + const { currentLocale, current, en: eng } = props; + + const t = useMemo(() => { + return (key, ...args) => + getMessage(currentLocale, current, key, ...args) || + getMessage(currentLocale, eng, key, ...args); + }, [currentLocale, current, eng]); + + return ( + {props.children} + ); +}; + +I18nProvider.propTypes = { + currentLocale: PropTypes.string, + current: PropTypes.object, + en: PropTypes.object, + children: PropTypes.node, +}; + +I18nProvider.defaultProps = { + children: undefined, +}; + +export function renderWithProvider(component, store) { + const Wrapper = ({ children }) => { + const WithoutStore = () => ( + + + {children} + + + ); + return store ? ( + + + + ) : ( + + ); + }; + + Wrapper.propTypes = { + children: PropTypes.node, + }; + + return render(component, { wrapper: Wrapper }); +} diff --git a/test/jest/setup.js b/test/jest/setup.js new file mode 100644 index 000000000..6176cfc66 --- /dev/null +++ b/test/jest/setup.js @@ -0,0 +1,2 @@ +// This file is for Jest-specific setup only and runs before our Jest tests. +import '@testing-library/jest-dom'; diff --git a/test/lib/render-helpers.js b/test/lib/render-helpers.js index 9d6bbe881..fc4621015 100644 --- a/test/lib/render-helpers.js +++ b/test/lib/render-helpers.js @@ -1,10 +1,12 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Provider } from 'react-redux'; import { render } from '@testing-library/react'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { LegacyI18nProvider } from '../../ui/app/contexts/i18n'; +import { I18nContext, LegacyI18nProvider } from '../../ui/app/contexts/i18n'; +import { getMessage } from '../../ui/app/helpers/utils/i18n-helper'; +import * as en from '../../app/_locales/en/messages.json'; export function mountWithRouter(component, store = {}, pathname = '/') { // Instantiate router context @@ -42,12 +44,48 @@ export function mountWithRouter(component, store = {}, pathname = '/') { return mount(, createContext()); } -export function renderWithProvider(component, store) { - const Wrapper = () => ( - - {component} - +export const I18nProvider = (props) => { + const { currentLocale, current, en: eng } = props; + + const t = useMemo(() => { + return (key, ...args) => + getMessage(currentLocale, current, key, ...args) || + getMessage(currentLocale, eng, key, ...args); + }, [currentLocale, current, eng]); + + return ( + {props.children} ); +}; + +I18nProvider.propTypes = { + currentLocale: PropTypes.string, + current: PropTypes.object, + en: PropTypes.object, + children: PropTypes.node, +}; + +I18nProvider.defaultProps = { + children: undefined, +}; + +export function renderWithProvider(component, store) { + const Wrapper = ({ children }) => + store ? ( + + + + {children} + + + + ) : ( + {children} + ); + + Wrapper.propTypes = { + children: PropTypes.node, + }; - return render(); + return render(component, { wrapper: Wrapper }); } diff --git a/test/lib/tick.js b/test/lib/tick.js new file mode 100644 index 000000000..1fadfb07f --- /dev/null +++ b/test/lib/tick.js @@ -0,0 +1,5 @@ +export function tick() { + return new Promise((resolve) => { + setTimeout(resolve, 0); + }); +} 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/__mocks__/react-router-dom.js b/ui/app/__mocks__/react-router-dom.js new file mode 100644 index 000000000..dd15434a1 --- /dev/null +++ b/ui/app/__mocks__/react-router-dom.js @@ -0,0 +1,11 @@ +const originalModule = jest.requireActual('react-router-dom'); + +module.exports = { + ...originalModule, + useHistory: jest.fn(), + useLocation: jest.fn(() => { + return { + pathname: '/swaps/build-quote', + }; + }), +}; 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 53% 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..11d5ba6eb 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 @@ -1,16 +1,15 @@ -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 () { +describe('AccountListItem Component', () => { let wrapper, propsMethodSpies, checksumAddressStub; - describe('render', function () { - before(function () { + describe('render', () => { + beforeAll(() => { checksumAddressStub = sinon .stub(utils, 'checksumAddress') .returns('mockCheckSumAddress'); @@ -18,7 +17,7 @@ describe('AccountListItem Component', function () { handleClick: sinon.spy(), }; }); - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { propsMethodSpies.handleClick.resetHistory(); checksumAddressStub.resetHistory(); }); - after(function () { + afterAll(() => { sinon.restore(); }); - it('should render a div with the passed className', function () { - assert.strictEqual(wrapper.find('.mockClassName').length, 1); - assert(wrapper.find('.mockClassName').is('div')); - assert(wrapper.find('.mockClassName').hasClass('account-list-item')); + it('should render a div with the passed className', () => { + expect(wrapper.find('.mockClassName')).toHaveLength(1); + expect(wrapper.find('.mockClassName').is('div')).toStrictEqual(true); + expect( + wrapper.find('.mockClassName').hasClass('account-list-item'), + ).toStrictEqual(true); }); - it('should call handleClick with the expected props when the root div is clicked', function () { + it('should call handleClick with the expected props when the root div is clicked', () => { const { onClick } = wrapper.find('.mockClassName').props(); - assert.strictEqual(propsMethodSpies.handleClick.callCount, 0); + expect(propsMethodSpies.handleClick.callCount).toStrictEqual(0); onClick(); - assert.strictEqual(propsMethodSpies.handleClick.callCount, 1); - assert.deepStrictEqual(propsMethodSpies.handleClick.getCall(0).args, [ + expect(propsMethodSpies.handleClick.callCount).toStrictEqual(1); + expect(propsMethodSpies.handleClick.getCall(0).args).toStrictEqual([ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }, ]); }); - it('should have a top row div', function () { - assert.strictEqual( - wrapper.find('.mockClassName > .account-list-item__top-row').length, - 1, - ); - assert( + it('should have a top row div', () => { + expect( + wrapper.find('.mockClassName > .account-list-item__top-row'), + ).toHaveLength(1); + expect( wrapper.find('.mockClassName > .account-list-item__top-row').is('div'), - ); + ).toStrictEqual(true); }); - it('should have an identicon, name and icon in the top row', function () { + it('should have an identicon, name and icon in the top row', () => { const topRow = wrapper.find( '.mockClassName > .account-list-item__top-row', ); - assert.strictEqual(topRow.find(Identicon).length, 1); - assert.strictEqual( - topRow.find('.account-list-item__account-name').length, - 1, - ); - assert.strictEqual(topRow.find('.account-list-item__icon').length, 1); + expect(topRow.find(Identicon)).toHaveLength(1); + expect(topRow.find('.account-list-item__account-name')).toHaveLength(1); + expect(topRow.find('.account-list-item__icon')).toHaveLength(1); }); - it('should show the account name if it exists', function () { + it('should show the account name if it exists', () => { const topRow = wrapper.find( '.mockClassName > .account-list-item__top-row', ); - assert.strictEqual( + expect( topRow.find('.account-list-item__account-name').text(), - 'mockName', - ); + ).toStrictEqual('mockName'); }); - it('should show the account address if there is no name', function () { + it('should show the account address if there is no name', () => { wrapper.setProps({ account: { address: 'addressButNoName' } }); const topRow = wrapper.find( '.mockClassName > .account-list-item__top-row', ); - assert.strictEqual( + expect( topRow.find('.account-list-item__account-name').text(), - 'addressButNoName', - ); + ).toStrictEqual('addressButNoName'); }); - it('should render the passed icon', function () { + it('should render the passed icon', () => { const topRow = wrapper.find( '.mockClassName > .account-list-item__top-row', ); - assert(topRow.find('.account-list-item__icon').childAt(0).is('i')); - assert( + expect( + topRow.find('.account-list-item__icon').childAt(0).is('i'), + ).toStrictEqual(true); + expect( topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon'), - ); + ).toStrictEqual(true); }); - it('should not render an icon if none is passed', function () { + it('should not render an icon if none is passed', () => { wrapper.setProps({ icon: null }); const topRow = wrapper.find( '.mockClassName > .account-list-item__top-row', ); - assert.strictEqual(topRow.find('.account-list-item__icon').length, 0); + expect(topRow.find('.account-list-item__icon')).toHaveLength(0); }); - it('should render the account address as a checksumAddress if displayAddress is true and name is provided', function () { + it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => { wrapper.setProps({ displayAddress: true }); - assert.strictEqual( - wrapper.find('.account-list-item__account-address').length, + expect(wrapper.find('.account-list-item__account-address')).toHaveLength( 1, ); - assert.strictEqual( + expect( wrapper.find('.account-list-item__account-address').text(), - 'mockCheckSumAddress', - ); - assert.deepStrictEqual(checksumAddressStub.getCall(0).args, [ + ).toStrictEqual('mockCheckSumAddress'); + expect(checksumAddressStub.getCall(0).args).toStrictEqual([ 'mockAddress', ]); }); - it('should not render the account address as a checksumAddress if displayAddress is false', function () { + it('should not render the account address as a checksumAddress if displayAddress is false', () => { wrapper.setProps({ displayAddress: false }); - assert.strictEqual( - wrapper.find('.account-list-item__account-address').length, + expect(wrapper.find('.account-list-item__account-address')).toHaveLength( 0, ); }); - it('should not render the account address as a checksumAddress if name is not provided', function () { + it('should not render the account address as a checksumAddress if name is not provided', () => { wrapper.setProps({ account: { address: 'someAddressButNoName' } }); - assert.strictEqual( - wrapper.find('.account-list-item__account-address').length, + expect(wrapper.find('.account-list-item__account-address')).toHaveLength( 0, ); }); diff --git a/ui/app/components/app/account-menu/account-menu.component.js b/ui/app/components/app/account-menu/account-menu.component.js index 4c57f4c8d..fe3ebffe6 100644 --- a/ui/app/components/app/account-menu/account-menu.component.js +++ b/ui/app/components/app/account-menu/account-menu.component.js @@ -12,7 +12,6 @@ import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display import { PRIMARY } from '../../../helpers/constants/common'; import { SETTINGS_ROUTE, - ABOUT_US_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, @@ -411,12 +410,12 @@ export default class AccountMenu extends Component {
    { - toggleAccountMenu(); - history.push(ABOUT_US_ROUTE); + global.platform.openTab({ url: 'https://support.metamask.io' }); }} - icon={{t('infoHelp')}} - text={t('infoHelp')} + icon={{t('support')}} + text={t('support')} /> + { toggleAccountMenu(); 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 50% 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..44666899d 100644 --- a/ui/app/components/app/account-menu/tests/account-menu.test.js +++ b/ui/app/components/app/account-menu/account-menu.test.js @@ -1,12 +1,11 @@ -import assert from 'assert'; 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 () { +describe('Account Menu', () => { let wrapper; const mockStore = { @@ -27,12 +26,12 @@ describe('Account Menu', function () { addressConnectedDomainMap: {}, accounts: [ { - address: '0xAddress', + address: '0x00', name: 'Account 1', balance: '0x0', }, { - address: '0xImportedAddress', + address: '0x1', name: 'Imported Account 1', balance: '0x0', }, @@ -44,7 +43,7 @@ describe('Account Menu', function () { }, { type: 'Simple Key Pair', - accounts: ['0xImportedAddress'], + accounts: ['0x1'], }, ], prevIsAccountMenuOpen: false, @@ -57,7 +56,7 @@ describe('Account Menu', function () { }, }; - before(function () { + beforeAll(() => { wrapper = mountWithRouter( @@ -66,136 +65,134 @@ describe('Account Menu', function () { ); }); - afterEach(function () { + afterEach(() => { props.toggleAccountMenu.resetHistory(); props.history.push.resetHistory(); }); - describe('Render Content', function () { - it('returns account name from identities', function () { + describe('Render Content', () => { + it('returns account name from identities', () => { const accountName = wrapper.find('.account-menu__name'); - assert.strictEqual(accountName.length, 2); + expect(accountName).toHaveLength(2); }); - it('renders user preference currency display balance from account balance', function () { + it('renders user preference currency display balance from account balance', () => { const accountBalance = wrapper.find( '.currency-display-component.account-menu__balance', ); - assert.strictEqual(accountBalance.length, 2); + expect(accountBalance).toHaveLength(2); }); - it('simulate click', function () { + it('simulate click', () => { const click = wrapper.find( '.account-menu__account.account-menu__item--clickable', ); click.first().simulate('click'); - assert(props.showAccountDetail.calledOnce); - assert.strictEqual( - props.showAccountDetail.getCall(0).args[0], - '0xAddress', - ); + expect(props.showAccountDetail.calledOnce).toStrictEqual(true); + expect(props.showAccountDetail.getCall(0).args[0]).toStrictEqual('0x00'); }); - it('render imported account label', function () { + it('render imported account label', () => { const importedAccount = wrapper.find('.keyring-label.allcaps'); - assert.strictEqual(importedAccount.text(), 'imported'); + expect(importedAccount.text()).toStrictEqual('imported'); }); }); - describe('Log Out', function () { + describe('Log Out', () => { let logout; - it('logout', function () { + it('logout', () => { logout = wrapper.find('.account-menu__lock-button'); - assert.strictEqual(logout.length, 1); + expect(logout).toHaveLength(1); }); - it('simulate click', function () { + it('simulate click', () => { logout.simulate('click'); - assert(props.lockMetamask.calledOnce); - assert.strictEqual(props.history.push.getCall(0).args[0], '/'); + expect(props.lockMetamask.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual('/'); }); }); - describe('Create Account', function () { + describe('Create Account', () => { let createAccount; - it('renders create account item', function () { + it('renders create account item', () => { createAccount = wrapper.find({ text: 'createAccount' }); - assert.strictEqual(createAccount.length, 1); + expect(createAccount).toHaveLength(1); }); - it('calls toggle menu and push new-account route to history', function () { + it('calls toggle menu and push new-account route to history', () => { createAccount.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); - assert.strictEqual(props.history.push.getCall(0).args[0], '/new-account'); + expect(props.toggleAccountMenu.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual( + '/new-account', + ); }); }); - describe('Import Account', function () { + describe('Import Account', () => { let importAccount; - it('renders import account item', function () { + it('renders import account item', () => { importAccount = wrapper.find({ text: 'importAccount' }); - assert.strictEqual(importAccount.length, 1); + expect(importAccount).toHaveLength(1); }); - it('calls toggle menu and push /new-account/import route to history', function () { + it('calls toggle menu and push /new-account/import route to history', () => { importAccount.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); - assert(props.history.push.getCall(0).args[0], '/new-account/import'); + expect(props.toggleAccountMenu.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual( + '/new-account/import', + ); }); }); - describe('Connect Hardware Wallet', function () { + describe('Connect Hardware Wallet', () => { let connectHardwareWallet; - it('renders import account item', function () { + it('renders import account item', () => { connectHardwareWallet = wrapper.find({ text: 'connectHardwareWallet' }); - assert.strictEqual(connectHardwareWallet.length, 1); + expect(connectHardwareWallet).toHaveLength(1); }); - it('calls toggle menu and push /new-account/connect route to history', function () { + it('calls toggle menu and push /new-account/connect route to history', () => { connectHardwareWallet.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.toggleAccountMenu.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/new-account/connect', ); }); }); - describe('Info & Help', function () { - let infoHelp; + describe('Support', () => { + let support; + global.platform = { openTab: sinon.spy() }; - it('renders import account item', function () { - infoHelp = wrapper.find({ text: 'infoHelp' }); - assert.strictEqual(infoHelp.length, 1); + it('renders import account item', () => { + support = wrapper.find({ text: 'support' }); + expect(support).toHaveLength(1); }); - it('calls toggle menu and push /new-account/connect route to history', function () { - infoHelp.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); - assert.strictEqual( - props.history.push.getCall(0).args[0], - '/settings/about-us', - ); + it('opens support link when clicked', () => { + support = wrapper.find({ text: 'support' }); + support.simulate('click'); + expect(global.platform.openTab.calledOnce).toStrictEqual(true); }); }); - describe('Settings', function () { + describe('Settings', () => { let settings; - it('renders import account item', function () { + it('renders import account item', () => { settings = wrapper.find({ text: 'settings' }); - assert.strictEqual(settings.length, 1); + expect(settings).toHaveLength(1); }); - it('calls toggle menu and push /new-account/connect route to history', function () { + it('calls toggle menu and push /new-account/connect route to history', () => { settings.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); - assert.strictEqual(props.history.push.getCall(0).args[0], '/settings'); + expect(props.toggleAccountMenu.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual('/settings'); }); }); }); 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 76% 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..dda361f4a 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 @@ -1,4 +1,3 @@ -import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; @@ -7,13 +6,14 @@ 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 { tick } from '../../../../../../test/lib/tick'; +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 () { +describe('Unconnected Account Alert', () => { const selectedAddress = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; const identities = { @@ -105,11 +105,11 @@ describe('Unconnected Account Alert', function () { }, }; - afterEach(function () { + afterEach(() => { sinon.restore(); }); - it('checks that checkbox is checked', function () { + it('checks that checkbox is checked', () => { const store = configureMockStore()(mockState); const { getByRole } = renderWithProvider( @@ -119,12 +119,12 @@ describe('Unconnected Account Alert', function () { const dontShowCheckbox = getByRole('checkbox'); - assert.strictEqual(dontShowCheckbox.checked, false); + expect(dontShowCheckbox.checked).toStrictEqual(false); fireEvent.click(dontShowCheckbox); - assert.strictEqual(dontShowCheckbox.checked, true); + expect(dontShowCheckbox.checked).toStrictEqual(true); }); - it('clicks dismiss button and calls dismissAlert action', function () { + it('clicks dismiss button and calls dismissAlert action', () => { const store = configureMockStore()(mockState); const { getByText } = renderWithProvider( @@ -132,16 +132,15 @@ describe('Unconnected Account Alert', function () { store, ); - const dismissButton = getByText(/dismiss/u); + const dismissButton = getByText(/Dismiss/u); fireEvent.click(dismissButton); - assert.strictEqual( - store.getActions()[0].type, + expect(store.getActions()[0].type).toStrictEqual( 'unconnectedAccount/dismissAlert', ); }); - it('clicks Dont Show checkbox and dismiss to call disable alert request action', async function () { + it('clicks Dont Show checkbox and dismiss to call disable alert request action', async () => { sinon.stub(actions, 'setAlertEnabledness').returns(() => Promise.resolve()); const store = configureMockStore([thunk])(mockState); @@ -151,21 +150,19 @@ describe('Unconnected Account Alert', function () { store, ); - const dismissButton = getByText(/dismiss/u); + const dismissButton = getByText(/Dismiss/u); const dontShowCheckbox = getByRole('checkbox'); fireEvent.click(dontShowCheckbox); fireEvent.click(dismissButton); - setImmediate(() => { - assert.strictEqual( - store.getActions()[0].type, - 'unconnectedAccount/disableAlertRequested', - ); - assert.strictEqual( - store.getActions()[1].type, - 'unconnectedAccount/disableAlertSucceeded', - ); - }); + await tick(); + + expect(store.getActions()[0].type).toStrictEqual( + 'unconnectedAccount/disableAlertRequested', + ); + expect(store.getActions()[1].type).toStrictEqual( + 'unconnectedAccount/disableAlertSucceeded', + ); }); }); diff --git a/ui/app/components/app/app-components.scss b/ui/app/components/app/app-components.scss index 07eac6ce1..4f407569d 100644 --- a/ui/app/components/app/app-components.scss +++ b/ui/app/components/app/app-components.scss @@ -36,3 +36,4 @@ @import 'transaction-list/index'; @import 'transaction-status/index'; @import 'wallet-overview/index'; +@import 'whats-new-popup/index'; 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 61% 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..4417ebd58 100644 --- a/ui/app/components/app/app-header/tests/app-header.test.js +++ b/ui/app/components/app/app-header/app-header.test.js @@ -1,12 +1,11 @@ -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 () { +describe('App Header', () => { let wrapper; const props = { @@ -26,7 +25,7 @@ describe('App Header', function () { isUnlocked: true, }; - beforeEach(function () { + beforeEach(() => { wrapper = shallow(, { context: { t: (str) => str, @@ -35,32 +34,31 @@ describe('App Header', function () { }); }); - afterEach(function () { + afterEach(() => { props.toggleAccountMenu.resetHistory(); }); - describe('App Header Logo', function () { - it('routes to default route when logo is clicked', function () { + describe('App Header Logo', () => { + it('routes to default route when logo is clicked', () => { const appLogo = wrapper.find(MetaFoxLogo); appLogo.simulate('click'); - assert(props.history.push.calledOnce); - assert.strictEqual(props.history.push.getCall(0).args[0], '/'); + expect(props.history.push.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual('/'); }); }); - describe('Network', function () { - it('shows network dropdown when networkDropdownOpen is false', function () { + describe('Network', () => { + it('shows network dropdown when networkDropdownOpen is false', () => { const network = wrapper.find(NetworkDisplay); - network.simulate('click', { preventDefault: () => undefined, stopPropagation: () => undefined, }); - assert(props.showNetworkDropdown.calledOnce); + expect(props.showNetworkDropdown.calledOnce).toStrictEqual(true); }); - it('hides network dropdown when networkDropdownOpen is true', function () { + it('hides network dropdown when networkDropdownOpen is true', () => { wrapper.setProps({ networkDropdownOpen: true }); const network = wrapper.find(NetworkDisplay); @@ -69,28 +67,28 @@ describe('App Header', function () { stopPropagation: () => undefined, }); - assert(props.hideNetworkDropdown.calledOnce); + expect(props.hideNetworkDropdown.calledOnce).toStrictEqual(true); }); - it('hides network indicator', function () { + it('hides network indicator', () => { wrapper.setProps({ hideNetworkIndicator: true }); - const network = wrapper.find({ network: 'test' }); - assert.strictEqual(network.length, 0); + const network = wrapper.find(NetworkDisplay); + expect(network).toHaveLength(0); }); }); - describe('Account Menu', function () { - it('toggles account menu', function () { + describe('Account Menu', () => { + it('toggles account menu', () => { const accountMenu = wrapper.find('.account-menu__icon'); accountMenu.simulate('click'); - assert(props.toggleAccountMenu.calledOnce); + expect(props.toggleAccountMenu.calledOnce).toStrictEqual(true); }); - it('does not toggle account menu when disabled', function () { + it('does not toggle account menu when disabled', () => { wrapper.setProps({ disabled: true }); const accountMenu = wrapper.find('.account-menu__icon'); accountMenu.simulate('click'); - assert(props.toggleAccountMenu.notCalled); + expect(props.toggleAccountMenu.notCalled).toStrictEqual(true); }); }); }); diff --git a/ui/app/components/app/asset-list-item/asset-list-item.js b/ui/app/components/app/asset-list-item/asset-list-item.js index d2c9da08d..7096664d4 100644 --- a/ui/app/components/app/asset-list-item/asset-list-item.js +++ b/ui/app/components/app/asset-list-item/asset-list-item.js @@ -131,27 +131,27 @@ const AssetListItem = ({ }; AssetListItem.propTypes = { - className: PropTypes.string, + 'className': PropTypes.string, 'data-testid': PropTypes.string, - iconClassName: PropTypes.string, - onClick: PropTypes.func.isRequired, - tokenAddress: PropTypes.string, - tokenSymbol: PropTypes.string, - tokenDecimals: PropTypes.number, - tokenImage: PropTypes.string, - warning: PropTypes.node, - primary: PropTypes.string, - secondary: PropTypes.string, - identiconBorder: PropTypes.bool, + 'iconClassName': PropTypes.string, + 'onClick': PropTypes.func.isRequired, + 'tokenAddress': PropTypes.string, + 'tokenSymbol': PropTypes.string, + 'tokenDecimals': PropTypes.number, + 'tokenImage': PropTypes.string, + 'warning': PropTypes.node, + 'primary': PropTypes.string, + 'secondary': PropTypes.string, + 'identiconBorder': PropTypes.bool, }; AssetListItem.defaultProps = { - className: undefined, + 'className': undefined, 'data-testid': undefined, - iconClassName: undefined, - tokenAddress: undefined, - tokenImage: undefined, - warning: undefined, + 'iconClassName': undefined, + 'tokenAddress': undefined, + 'tokenImage': undefined, + 'warning': undefined, }; export default AssetListItem; 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 65% 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..a0f866456 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 @@ -1,18 +1,17 @@ -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(), }; -describe('Confirm Detail Row Component', function () { - describe('render', function () { +describe('Confirm Detail Row Component', () => { + describe('render', () => { let wrapper; - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { + expect(wrapper.find('div.confirm-detail-row')).toHaveLength(1); }); - it('should render the label as a child of the confirm-detail-row__label', function () { - assert.strictEqual( + it('should render the label as a child of the confirm-detail-row__label', () => { + expect( wrapper .find('.confirm-detail-row > .confirm-detail-row__label') .childAt(0) .text(), - 'mockLabel', - ); + ).toStrictEqual('mockLabel'); }); - it('should render the headerText as a child of the confirm-detail-row__header-text', function () { - assert.strictEqual( + it('should render the headerText as a child of the confirm-detail-row__header-text', () => { + expect( wrapper .find( '.confirm-detail-row__details > .confirm-detail-row__header-text', ) .childAt(0) .text(), - 'mockHeaderText', - ); + ).toStrictEqual('mockHeaderText'); }); - it('should render the primaryText as a child of the confirm-detail-row__primary', function () { - assert.strictEqual( + it('should render the primaryText as a child of the confirm-detail-row__primary', () => { + expect( wrapper .find('.confirm-detail-row__details > .confirm-detail-row__primary') .childAt(0) .text(), - 'mockFiatText', - ); + ).toStrictEqual('mockFiatText'); }); - it('should render the ethText as a child of the confirm-detail-row__secondary', function () { - assert.strictEqual( + it('should render the ethText as a child of the confirm-detail-row__secondary', () => { + expect( wrapper .find('.confirm-detail-row__details > .confirm-detail-row__secondary') .childAt(0) .text(), - 'mockEthText', - ); + ).toStrictEqual('mockEthText'); }); - it('should set the fiatTextColor on confirm-detail-row__primary', function () { - assert.strictEqual( + it('should set the fiatTextColor on confirm-detail-row__primary', () => { + expect( wrapper.find('.confirm-detail-row__primary').props().style.color, - 'mockColor', - ); + ).toStrictEqual('mockColor'); }); - it('should assure the confirm-detail-row__header-text classname is correct', function () { - assert.strictEqual( + it('should assure the confirm-detail-row__header-text classname is correct', () => { + expect( wrapper.find('.confirm-detail-row__header-text').props().className, - 'confirm-detail-row__header-text mockHeaderClass', - ); + ).toStrictEqual('confirm-detail-row__header-text mockHeaderClass'); }); - it('should call onHeaderClick when headerText div gets clicked', function () { + it('should call onHeaderClick when headerText div gets clicked', () => { wrapper.find('.confirm-detail-row__header-text').props().onClick(); - assert.ok(propsMethodSpies.onHeaderClick.calledOnce); + expect(propsMethodSpies.onHeaderClick.calledOnce).toStrictEqual(true); }); }); }); 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 63% 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..b94d3d15d 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 @@ -1,17 +1,16 @@ -import assert from 'assert'; 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 () { - it('should render a div with a confirm-page-container-header class', function () { +describe('Confirm Detail Row Component', () => { + describe('render', () => { + it('should render a div with a confirm-page-container-header class', () => { const stub = sinon .stub(util, 'getEnvironmentType') .callsFake(() => 'popup'); @@ -27,14 +26,11 @@ describe('Confirm Detail Row Component', function () { /> , ); - assert.strictEqual( - wrapper.html().includes('confirm-page-container-header'), - true, - ); + expect(wrapper.html()).toContain('confirm-page-container-header'); stub.restore(); }); - it('should only render children when fullscreen and showEdit is false', function () { + it('should only render children when fullscreen and showEdit is false', () => { const stub = sinon .stub(util, 'getEnvironmentType') .callsFake(() => 'fullscreen'); @@ -52,11 +48,8 @@ describe('Confirm Detail Row Component', function () { , ); - assert.strictEqual(wrapper.html().includes('nested-test-class'), true); - assert.strictEqual( - wrapper.html().includes('confirm-page-container-header'), - false, - ); + expect(wrapper.html()).toContain('nested-test-class'); + expect(wrapper.html()).not.toContain('confirm-page-container-header'); stub.restore(); }); }); 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/dropdown.test.js b/ui/app/components/app/dropdowns/dropdown.test.js new file mode 100644 index 000000000..be01c89af --- /dev/null +++ b/ui/app/components/app/dropdowns/dropdown.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; +import { DropdownMenuItem } from './dropdown'; + +describe('Dropdown', () => { + let wrapper; + const onClickSpy = sinon.spy(); + const closeMenuSpy = sinon.spy(); + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('renders li with dropdown-menu-item class', () => { + expect(wrapper.find('li.dropdown-menu-item')).toHaveLength(1); + }); + + it('adds style based on props passed', () => { + expect(wrapper.prop('style').test).toStrictEqual('style'); + }); + + it('simulates click event and calls onClick and closeMenu', () => { + wrapper.prop('onClick')(); + expect(onClickSpy.callCount).toStrictEqual(1); + expect(closeMenuSpy.callCount).toStrictEqual(1); + }); +}); 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/network-dropdown.test.js b/ui/app/components/app/dropdowns/network-dropdown.test.js new file mode 100644 index 000000000..3452e8fef --- /dev/null +++ b/ui/app/components/app/dropdowns/network-dropdown.test.js @@ -0,0 +1,112 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +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', () => { + let wrapper; + const createMockStore = configureMockStore([thunk]); + + describe('NetworkDropdown in appState in false', () => { + const mockState = { + metamask: { + network: '1', + provider: { + type: 'test', + }, + }, + appState: { + networkDropdownOpen: false, + }, + }; + + const store = createMockStore(mockState); + + beforeEach(() => { + wrapper = mountWithRouter(); + }); + + it('checks for network droppo class', () => { + expect(wrapper.find('.network-droppo')).toHaveLength(1); + }); + + it('renders only one child when networkDropdown is false in state', () => { + expect(wrapper.children()).toHaveLength(1); + }); + }); + + describe('NetworkDropdown in appState is true', () => { + const mockState = { + metamask: { + network: '1', + provider: { + type: 'test', + }, + frequentRpcListDetail: [ + { chainId: '0x1a', rpcUrl: 'http://localhost:7545' }, + { rpcUrl: 'http://localhost:7546' }, + ], + }, + appState: { + networkDropdownOpen: true, + }, + }; + const store = createMockStore(mockState); + + beforeEach(() => { + wrapper = mountWithRouter(); + }); + + it('renders 8 DropDownMenuItems', () => { + expect(wrapper.find(DropdownMenuItem)).toHaveLength(8); + }); + + it('checks background color for first ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(0); + expect(colorIndicator.prop('color')).toStrictEqual('mainnet'); + expect(colorIndicator.prop('borderColor')).toStrictEqual('mainnet'); + }); + + it('checks background color for second ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(1); + expect(colorIndicator.prop('color')).toStrictEqual('ropsten'); + expect(colorIndicator.prop('borderColor')).toStrictEqual('ropsten'); + }); + + it('checks background color for third ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(2); + expect(colorIndicator.prop('color')).toStrictEqual('kovan'); + expect(colorIndicator.prop('borderColor')).toStrictEqual('kovan'); + }); + + it('checks background color for fourth ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(3); + expect(colorIndicator.prop('color')).toStrictEqual('rinkeby'); + expect(colorIndicator.prop('borderColor')).toStrictEqual('rinkeby'); + }); + + it('checks background color for fifth ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(4); + expect(colorIndicator.prop('color')).toStrictEqual('goerli'); + expect(colorIndicator.prop('borderColor')).toStrictEqual('goerli'); + }); + + it('checks background color for sixth ColorIndicator', () => { + const colorIndicator = wrapper.find(ColorIndicator).at(5); + const customNetworkGray = 'ui-2'; + expect(colorIndicator.prop('color')).toStrictEqual(customNetworkGray); + expect(colorIndicator.prop('borderColor')).toStrictEqual( + customNetworkGray, + ); + }); + + it('checks dropdown for frequestRPCList from state', () => { + expect(wrapper.find(DropdownMenuItem).at(6).text()).toStrictEqual( + '✓http://localhost:7545', + ); + }); + }); +}); diff --git a/ui/app/components/app/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/tests/dropdown.test.js deleted file mode 100644 index 63faf3341..000000000 --- a/ui/app/components/app/dropdowns/tests/dropdown.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import { DropdownMenuItem } from '../components/dropdown'; - -describe('Dropdown', function () { - let wrapper; - const onClickSpy = sinon.spy(); - const closeMenuSpy = sinon.spy(); - - beforeEach(function () { - wrapper = shallow( - , - ); - }); - - it('renders li with dropdown-menu-item class', function () { - assert.strictEqual(wrapper.find('li.dropdown-menu-item').length, 1); - }); - - it('adds style based on props passed', function () { - assert.strictEqual(wrapper.prop('style').test, 'style'); - }); - - it('simulates click event and calls onClick and closeMenu', function () { - wrapper.prop('onClick')(); - assert.strictEqual(onClickSpy.callCount, 1); - assert.strictEqual(closeMenuSpy.callCount, 1); - }); -}); diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js deleted file mode 100644 index c7f872c6a..000000000 --- a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js +++ /dev/null @@ -1,112 +0,0 @@ -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'; - -describe('Network Dropdown', function () { - let wrapper; - const createMockStore = configureMockStore([thunk]); - - describe('NetworkDropdown in appState in false', function () { - const mockState = { - metamask: { - network: '1', - provider: { - type: 'test', - }, - }, - appState: { - networkDropdownOpen: false, - }, - }; - - const store = createMockStore(mockState); - - beforeEach(function () { - wrapper = mountWithRouter(); - }); - - it('checks for network droppo class', function () { - assert.strictEqual(wrapper.find('.network-droppo').length, 1); - }); - - it('renders only one child when networkDropdown is false in state', function () { - assert.strictEqual(wrapper.children().length, 1); - }); - }); - - describe('NetworkDropdown in appState is true', function () { - const mockState = { - metamask: { - network: '1', - provider: { - type: 'test', - }, - frequentRpcListDetail: [ - { chainId: '0x1a', rpcUrl: 'http://localhost:7545' }, - { rpcUrl: 'http://localhost:7546' }, - ], - }, - appState: { - networkDropdownOpen: true, - }, - }; - const store = createMockStore(mockState); - - beforeEach(function () { - wrapper = mountWithRouter(); - }); - - it('renders 8 DropDownMenuItems ', function () { - assert.strictEqual(wrapper.find(DropdownMenuItem).length, 8); - }); - - it('checks background color for first ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(0); - assert.strictEqual(colorIndicator.prop('color'), 'mainnet'); - assert.strictEqual(colorIndicator.prop('borderColor'), 'mainnet'); - }); - - it('checks background color for second ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(1); - assert.strictEqual(colorIndicator.prop('color'), 'ropsten'); - assert.strictEqual(colorIndicator.prop('borderColor'), 'ropsten'); - }); - - it('checks background color for third ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(2); - assert.strictEqual(colorIndicator.prop('color'), 'kovan'); - assert.strictEqual(colorIndicator.prop('borderColor'), 'kovan'); - }); - - it('checks background color for fourth ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(3); - assert.strictEqual(colorIndicator.prop('color'), 'rinkeby'); - assert.strictEqual(colorIndicator.prop('borderColor'), 'rinkeby'); - }); - - it('checks background color for fifth ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(4); - assert.strictEqual(colorIndicator.prop('color'), 'goerli'); - assert.strictEqual(colorIndicator.prop('borderColor'), 'goerli'); - }); - - it('checks background color for sixth ColorIndicator', function () { - const colorIndicator = wrapper.find(ColorIndicator).at(5); - const customNetworkGray = 'ui-2'; - assert.strictEqual(colorIndicator.prop('color'), customNetworkGray); - assert.strictEqual(colorIndicator.prop('borderColor'), customNetworkGray); - }); - - it('checks dropdown for frequestRPCList from state', function () { - assert.strictEqual( - wrapper.find(DropdownMenuItem).at(6).text(), - '✓http://localhost:7545', - ); - }); - }); -}); 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 53% 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..29b0e6702 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 @@ -1,10 +1,9 @@ -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 () { +describe('Advanced Gas Inputs', () => { let wrapper, clock; const props = { @@ -20,7 +19,7 @@ describe('Advanced Gas Inputs', function () { minimumGasLimit: 21000, }; - beforeEach(function () { + beforeEach(() => { clock = sinon.useFakeTimers(); wrapper = mount(, { @@ -30,93 +29,94 @@ describe('Advanced Gas Inputs', function () { }); }); - afterEach(function () { + afterEach(() => { clock.restore(); }); - it('wont update gasPrice in props before debounce', function () { + it('wont update gasPrice in props before debounce', () => { const event = { target: { value: 1 } }; wrapper.find('input').at(0).simulate('change', event); clock.tick(499); - assert.strictEqual(props.updateCustomGasPrice.callCount, 0); + expect(props.updateCustomGasPrice.callCount).toStrictEqual(0); }); - it('simulates onChange on gas price after debounce', function () { + it('simulates onChange on gas price after debounce', () => { const event = { target: { value: 1 } }; wrapper.find('input').at(0).simulate('change', event); clock.tick(500); - assert.strictEqual(props.updateCustomGasPrice.calledOnce, true); - assert.strictEqual(props.updateCustomGasPrice.calledWith(1), true); + expect(props.updateCustomGasPrice.calledOnce).toStrictEqual(true); + expect(props.updateCustomGasPrice.calledWith(1)).toStrictEqual(true); }); - it('wont update gasLimit in props before debounce', function () { + it('wont update gasLimit in props before debounce', () => { const event = { target: { value: 21000 } }; wrapper.find('input').at(1).simulate('change', event); clock.tick(499); - assert.strictEqual(props.updateCustomGasLimit.callCount, 0); + expect(props.updateCustomGasLimit.callCount).toStrictEqual(0); }); - it('simulates onChange on gas limit after debounce', function () { + it('simulates onChange on gas limit after debounce', () => { const event = { target: { value: 21000 } }; wrapper.find('input').at(1).simulate('change', event); clock.tick(500); - assert.strictEqual(props.updateCustomGasLimit.calledOnce, true); - assert.strictEqual(props.updateCustomGasLimit.calledWith(21000), true); + expect(props.updateCustomGasLimit.calledOnce).toStrictEqual(true); + expect(props.updateCustomGasLimit.calledWith(21000)).toStrictEqual(true); }); - it('errors when insufficientBalance under gas price and gas limit', function () { + it('errors when insufficientBalance under gas price and gas limit', () => { wrapper.setProps({ insufficientBalance: true }); const renderError = wrapper.find( '.advanced-gas-inputs__gas-edit-row__error-text', ); - assert.strictEqual(renderError.length, 2); + expect(renderError).toHaveLength(2); - assert.strictEqual(renderError.at(0).text(), 'insufficientBalance'); - assert.strictEqual(renderError.at(1).text(), 'insufficientBalance'); + expect(renderError.at(0).text()).toStrictEqual('insufficientBalance'); + expect(renderError.at(1).text()).toStrictEqual('insufficientBalance'); }); - it('errors zero gas price / speed up', function () { + it('errors zero gas price / speed up', () => { wrapper.setProps({ isSpeedUp: true }); const renderError = wrapper.find( '.advanced-gas-inputs__gas-edit-row__error-text', ); - assert.strictEqual(renderError.length, 2); + expect(renderError).toHaveLength(2); - assert.strictEqual(renderError.at(0).text(), 'zeroGasPriceOnSpeedUpError'); - assert.strictEqual( - renderError.at(1).text(), + expect(renderError.at(0).text()).toStrictEqual( + 'zeroGasPriceOnSpeedUpError', + ); + expect(renderError.at(1).text()).toStrictEqual( 'gasLimitTooLowWithDynamicFee', ); }); - it('warns when custom gas price is too low', function () { + it('warns when custom gas price is too low', () => { wrapper.setProps({ customPriceIsSafe: false }); const renderWarning = wrapper.find( '.advanced-gas-inputs__gas-edit-row__warning-text', ); - assert.strictEqual(renderWarning.length, 1); + expect(renderWarning).toHaveLength(1); - assert.strictEqual(renderWarning.text(), 'gasPriceExtremelyLow'); + expect(renderWarning.text()).toStrictEqual('gasPriceExtremelyLow'); }); - it('errors when custom gas price is too excessive', function () { + it('errors when custom gas price is too excessive', () => { wrapper.setProps({ customPriceIsExcessive: true }); const renderError = wrapper.find( '.advanced-gas-inputs__gas-edit-row__error-text', ); - assert.strictEqual(renderError.length, 2); - assert.strictEqual(renderError.at(0).text(), 'gasPriceExcessiveInput'); + expect(renderError).toHaveLength(2); + expect(renderError.at(0).text()).toStrictEqual('gasPriceExcessiveInput'); }); }); 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 58% 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..0a91fbc69 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,13 +1,12 @@ -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 () { +describe('AdvancedTabContent Component', () => { let wrapper; - beforeEach(function () { + beforeEach(() => { const propsMethodSpies = { updateCustomGasPrice: sinon.spy(), updateCustomGasLimit: sinon.spy(), @@ -28,64 +27,65 @@ describe('AdvancedTabContent Component', function () { ); }); - afterEach(function () { + afterEach(() => { sinon.restore(); }); - describe('render()', function () { - it('should render the advanced-tab root node', function () { - assert(wrapper.hasClass('advanced-tab')); + describe('render()', () => { + it('should render the advanced-tab root node', () => { + expect(wrapper.hasClass('advanced-tab')).toStrictEqual(true); }); - it('should render the expected child of the advanced-tab div', function () { + it('should render the expected child of the advanced-tab div', () => { const advancedTabChildren = wrapper.children(); - assert.strictEqual(advancedTabChildren.length, 2); + expect(advancedTabChildren).toHaveLength(2); - assert( + expect( advancedTabChildren .at(0) .hasClass('advanced-tab__transaction-data-summary'), - ); + ).toStrictEqual(true); }); - it('should call renderDataSummary with the expected params', function () { + it('should call renderDataSummary with the expected params', () => { const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall( 0, ).args; - assert.deepStrictEqual(renderDataSummaryArgs, ['$0.25']); + expect(renderDataSummaryArgs).toStrictEqual(['$0.25']); }); }); - describe('renderDataSummary()', function () { + describe('renderDataSummary()', () => { let dataSummary; - beforeEach(function () { + beforeEach(() => { dataSummary = shallow( wrapper.instance().renderDataSummary('mockTotalFee'), ); }); - it('should render the transaction-data-summary root node', function () { - assert(dataSummary.hasClass('advanced-tab__transaction-data-summary')); + it('should render the transaction-data-summary root node', () => { + expect( + dataSummary.hasClass('advanced-tab__transaction-data-summary'), + ).toStrictEqual(true); }); - it('should render titles of the data', function () { + it('should render titles of the data', () => { const titlesNode = dataSummary.children().at(0); - assert( + expect( titlesNode.hasClass('advanced-tab__transaction-data-summary__titles'), - ); - assert.strictEqual( - titlesNode.children().at(0).text(), + ).toStrictEqual(true); + expect(titlesNode.children().at(0).text()).toStrictEqual( 'newTransactionFee', ); }); - it('should render the data', function () { + it('should render the data', () => { const dataNode = dataSummary.children().at(1); - assert( + expect( dataNode.hasClass('advanced-tab__transaction-data-summary__container'), - ); - assert.strictEqual(dataNode.children().at(0).text(), 'mockTotalFee'); + ).toStrictEqual(true); + expect(dataNode.children().at(0).text()).toStrictEqual('mockTotalFee'); }); }); }); 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 56% 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..facda50a5 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,9 @@ -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, @@ -38,11 +37,11 @@ const mockGasPriceButtonGroupProps = { showCheck: true, }; -describe('BasicTabContent Component', function () { - describe('render', function () { +describe('BasicTabContent Component', () => { + describe('render', () => { let wrapper; - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { + expect( wrapper .find('.basic-tab-content') .childAt(0) .hasClass('basic-tab-content__title'), - ); + ).toStrictEqual(true); }); - it('should render a GasPriceButtonGroup compenent', function () { - assert.strictEqual(wrapper.find(GasPriceButtonGroup).length, 1); + it('should render a GasPriceButtonGroup compenent', () => { + expect(wrapper.find(GasPriceButtonGroup)).toHaveLength(1); }); - it('should pass correct props to GasPriceButtonGroup', function () { + it('should pass correct props to GasPriceButtonGroup', () => { const { buttonDataLoading, className, @@ -72,28 +71,24 @@ describe('BasicTabContent Component', function () { noButtonActiveByDefault, showCheck, } = wrapper.find(GasPriceButtonGroup).props(); - assert.strictEqual(wrapper.find(GasPriceButtonGroup).length, 1); - assert.strictEqual( - buttonDataLoading, + expect(wrapper.find(GasPriceButtonGroup)).toHaveLength(1); + expect(buttonDataLoading).toStrictEqual( mockGasPriceButtonGroupProps.buttonDataLoading, ); - assert.strictEqual(className, mockGasPriceButtonGroupProps.className); - assert.strictEqual( - noButtonActiveByDefault, + expect(className).toStrictEqual(mockGasPriceButtonGroupProps.className); + expect(noButtonActiveByDefault).toStrictEqual( mockGasPriceButtonGroupProps.noButtonActiveByDefault, ); - assert.strictEqual(showCheck, mockGasPriceButtonGroupProps.showCheck); - assert.deepStrictEqual( - gasButtonInfo, + expect(showCheck).toStrictEqual(mockGasPriceButtonGroupProps.showCheck); + expect(gasButtonInfo).toStrictEqual( mockGasPriceButtonGroupProps.gasButtonInfo, ); - assert.strictEqual( - JSON.stringify(handleGasPriceSelection), - JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection), + expect(handleGasPriceSelection).toStrictEqual( + mockGasPriceButtonGroupProps.handleGasPriceSelection, ); }); - it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', function () { + it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => { wrapper.setProps({ gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, @@ -101,8 +96,8 @@ describe('BasicTabContent Component', function () { }, }); - assert.strictEqual(wrapper.find(GasPriceButtonGroup).length, 0); - assert.strictEqual(wrapper.find(Loading).length, 1); + expect(wrapper.find(GasPriceButtonGroup)).toHaveLength(0); + expect(wrapper.find(Loading)).toHaveLength(1); }); }); }); 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 64% 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..dd5f23e39 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,11 @@ -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', @@ -60,10 +59,10 @@ const mockInfoRowProps = { }; const GP = GasModalPageContainer.prototype; -describe('GasModalPageContainer Component', function () { +describe('GasModalPageContainer Component', () => { let wrapper; - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { propsMethodSpies.cancelAndClose.resetHistory(); }); - describe('componentDidMount', function () { - it('should call props.fetchBasicGasEstimates', function () { + describe('componentDidMount', () => { + it('should call props.fetchBasicGasEstimates', () => { propsMethodSpies.fetchBasicGasEstimates.resetHistory(); - assert.strictEqual(propsMethodSpies.fetchBasicGasEstimates.callCount, 0); + expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual( + 0, + ); wrapper.instance().componentDidMount(); - assert.strictEqual(propsMethodSpies.fetchBasicGasEstimates.callCount, 1); + expect(propsMethodSpies.fetchBasicGasEstimates.callCount).toStrictEqual( + 1, + ); }); }); - describe('render', function () { - it('should render a PageContainer compenent', function () { - assert.strictEqual(wrapper.find(PageContainer).length, 1); + describe('render', () => { + it('should render a PageContainer compenent', () => { + expect(wrapper.find(PageContainer)).toHaveLength(1); }); - it('should pass correct props to PageContainer', function () { + it('should pass correct props to PageContainer', () => { const { title, subtitle, disabled } = wrapper.find(PageContainer).props(); - assert.strictEqual(title, 'customGas'); - assert.strictEqual(subtitle, 'customGasSubTitle'); - assert.strictEqual(disabled, false); + expect(title).toStrictEqual('customGas'); + expect(subtitle).toStrictEqual('customGasSubTitle'); + expect(disabled).toStrictEqual(false); }); - it('should pass the correct onCancel and onClose methods to PageContainer', function () { + it('should pass the correct onCancel and onClose methods to PageContainer', () => { const { onCancel, onClose } = wrapper.find(PageContainer).props(); - assert.strictEqual(propsMethodSpies.cancelAndClose.callCount, 0); + expect(propsMethodSpies.cancelAndClose.callCount).toStrictEqual(0); onCancel(); - assert.strictEqual(propsMethodSpies.cancelAndClose.callCount, 1); + expect(propsMethodSpies.cancelAndClose.callCount).toStrictEqual(1); onClose(); - assert.strictEqual(propsMethodSpies.cancelAndClose.callCount, 2); + expect(propsMethodSpies.cancelAndClose.callCount).toStrictEqual(2); }); - it('should pass the correct renderTabs property to PageContainer', function () { + it('should pass the correct renderTabs property to PageContainer', () => { sinon.stub(GP, 'renderTabs').returns('mockTabs'); const renderTabsWrapperTester = shallow( { + beforeEach(() => { sinon.spy(GP, 'renderBasicTabContent'); sinon.spy(GP, 'renderAdvancedTabContent'); sinon.spy(GP, 'renderInfoRows'); }); - afterEach(function () { + afterEach(() => { GP.renderBasicTabContent.restore(); GP.renderAdvancedTabContent.restore(); GP.renderInfoRows.restore(); }); - it('should render a Tabs component with "Basic" and "Advanced" tabs', function () { + it('should render a Tabs component with "Basic" and "Advanced" tabs', () => { const renderTabsResult = wrapper.instance().renderTabs(); const renderedTabs = shallow(renderTabsResult); - assert.strictEqual(renderedTabs.props().className, 'tabs'); + expect(renderedTabs.props().className).toStrictEqual('tabs'); const tabs = renderedTabs.find(Tab); - assert.strictEqual(tabs.length, 2); + expect(tabs).toHaveLength(2); - assert.strictEqual(tabs.at(0).props().name, 'basic'); - assert.strictEqual(tabs.at(1).props().name, 'advanced'); + expect(tabs.at(0).props().name).toStrictEqual('basic'); + expect(tabs.at(1).props().name).toStrictEqual('advanced'); - assert.strictEqual( - tabs.at(0).childAt(0).props().className, + expect(tabs.at(0).childAt(0).props().className).toStrictEqual( 'gas-modal-content', ); - assert.strictEqual( - tabs.at(1).childAt(0).props().className, + expect(tabs.at(1).childAt(0).props().className).toStrictEqual( 'gas-modal-content', ); }); - it('should call renderInfoRows with the expected props', function () { - assert.strictEqual(GP.renderInfoRows.callCount, 0); + it('should call renderInfoRows with the expected props', () => { + expect(GP.renderInfoRows.callCount).toStrictEqual(0); wrapper.instance().renderTabs(); - assert.strictEqual(GP.renderInfoRows.callCount, 2); + expect(GP.renderInfoRows.callCount).toStrictEqual(2); - assert.deepStrictEqual(GP.renderInfoRows.getCall(0).args, [ + expect(GP.renderInfoRows.getCall(0).args).toStrictEqual([ 'mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee', ]); - assert.deepStrictEqual(GP.renderInfoRows.getCall(1).args, [ + expect(GP.renderInfoRows.getCall(1).args).toStrictEqual([ 'mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', @@ -187,7 +188,7 @@ describe('GasModalPageContainer Component', function () { ]); }); - it('should not render the basic tab if hideBasic is true', function () { + it('should not render the basic tab if hideBasic is true', () => { wrapper = shallow( { + it('should render', () => { const renderBasicTabContentResult = wrapper .instance() .renderBasicTabContent(mockGasPriceButtonGroupProps); - assert.deepStrictEqual( + expect( renderBasicTabContentResult.props.gasPriceButtonGroupProps, - mockGasPriceButtonGroupProps, - ); + ).toStrictEqual(mockGasPriceButtonGroupProps); }); }); - describe('renderInfoRows', function () { - it('should render the info rows with the passed data', function () { + describe('renderInfoRows', () => { + it('should render the info rows with the passed data', () => { const baseClassName = 'gas-modal-content__info-row'; const renderedInfoRowsContainer = shallow( wrapper @@ -240,32 +240,35 @@ describe('GasModalPageContainer Component', function () { ), ); - assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName)); + expect( + renderedInfoRowsContainer.childAt(0).hasClass(baseClassName), + ).toStrictEqual(true); const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children(); - assert.strictEqual(renderedInfoRows.length, 4); - assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`)); - assert( + expect(renderedInfoRows).toHaveLength(4); + expect( + renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`), + ).toStrictEqual(true); + expect( renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`), - ); - assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`)); - assert( + ).toStrictEqual(true); + expect( + renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`), + ).toStrictEqual(true); + expect( renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`), - ); + ).toStrictEqual(true); - assert.strictEqual( - renderedInfoRows.at(0).text(), + expect(renderedInfoRows.at(0).text()).toStrictEqual( 'sendAmount mockSendAmount', ); - assert.strictEqual( - renderedInfoRows.at(1).text(), + expect(renderedInfoRows.at(1).text()).toStrictEqual( 'transactionFee mockTransactionFee', ); - assert.strictEqual( - renderedInfoRows.at(2).text(), + expect(renderedInfoRows.at(2).text()).toStrictEqual( 'newTotal mockNewTotalEth', ); - assert.strictEqual(renderedInfoRows.at(3).text(), 'mockNewTotalFiat'); + expect(renderedInfoRows.at(3).text()).toStrictEqual('mockNewTotalFiat'); }); }); }); diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/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 new file mode 100644 index 000000000..ef5e62895 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js @@ -0,0 +1,284 @@ +import sinon from 'sinon'; + +import { hideModal, setGasLimit, setGasPrice } from '../../../../store/actions'; + +import { + setCustomGasPrice, + setCustomGasLimit, + resetCustomData, +} from '../../../../ducks/gas/gas.duck'; + +import { hideGasButtonGroup } from '../../../../ducks/send/send.duck'; + +let mapDispatchToProps; +let mergeProps; + +jest.mock('react-redux', () => ({ + connect: (_, md, mp) => { + mapDispatchToProps = md; + mergeProps = mp; + return () => ({}); + }, +})); + +jest.mock('../../../../../app/selectors', () => ({ + getBasicGasEstimateLoadingStatus: (s) => + `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, + getRenderableBasicEstimateData: (s) => + `mockRenderableBasicEstimateData:${Object.keys(s).length}`, + getDefaultActiveButtonIndex: (a, b) => a + b, + getCurrentEthBalance: (state) => state.metamask.balance || '0x0', + getSendToken: () => null, + getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0', + getCustomGasPrice: (state) => state.gas.customData.price || '0x0', + getCustomGasLimit: (state) => state.gas.customData.limit || '0x0', + getCurrentCurrency: jest.fn().mockReturnValue('usd'), + conversionRateSelector: jest.fn().mockReturnValue(50), + getSendMaxModeState: jest.fn().mockReturnValue(false), + getPreferences: jest.fn(() => ({ + showFiatInTestnets: false, + })), + getIsMainnet: jest.fn().mockReturnValue(false), + isCustomPriceSafe: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../../../../app/store/actions', () => ({ + hideModal: jest.fn(), + setGasLimit: jest.fn(), + setGasPrice: jest.fn(), + updateTransaction: jest.fn(), +})); + +jest.mock('../../../../../app/ducks/gas/gas.duck', () => ({ + setCustomGasPrice: jest.fn(), + setCustomGasLimit: jest.fn(), + resetCustomData: jest.fn(), +})); + +jest.mock('../../../../../app/ducks/send/send.duck', () => ({ + hideGasButtonGroup: jest.fn(), +})); + +require('./gas-modal-page-container.container'); + +describe('gas-modal-page-container container', () => { + describe('mapDispatchToProps()', () => { + let dispatchSpy; + let mapDispatchToPropsObject; + + beforeEach(() => { + dispatchSpy = sinon.spy(); + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy); + }); + + afterEach(() => { + dispatchSpy.resetHistory(); + }); + + describe('hideGasButtonGroup()', () => { + it('should dispatch a hideGasButtonGroup action', () => { + mapDispatchToPropsObject.hideGasButtonGroup(); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(hideGasButtonGroup).toHaveBeenCalled(); + }); + }); + + describe('cancelAndClose()', () => { + it('should dispatch a hideModal action', () => { + mapDispatchToPropsObject.cancelAndClose(); + expect(dispatchSpy.calledTwice).toStrictEqual(true); + expect(hideModal).toHaveBeenCalled(); + expect(resetCustomData).toHaveBeenCalled(); + }); + }); + + describe('updateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => { + mapDispatchToPropsObject.updateCustomGasPrice('ffff'); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(setCustomGasPrice).toHaveBeenCalled(); + expect(setCustomGasPrice).toHaveBeenCalledWith('0xffff'); + // expect( + // setCustomGasPrice.getCall(0).args[0], + // '0xffff', + // ); + }); + + it('should dispatch a setCustomGasPrice action', () => { + mapDispatchToPropsObject.updateCustomGasPrice('0xffff'); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(setCustomGasPrice).toHaveBeenCalled(); + expect(setCustomGasPrice).toHaveBeenCalledWith('0xffff'); + }); + }); + + describe('updateCustomGasLimit()', () => { + it('should dispatch a setCustomGasLimit action', () => { + mapDispatchToPropsObject.updateCustomGasLimit('0x10'); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(setCustomGasLimit).toHaveBeenCalled(); + expect(setCustomGasLimit).toHaveBeenCalledWith('0x10'); + }); + }); + + describe('setGasData()', () => { + it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => { + mapDispatchToPropsObject.setGasData('ffff', 'aaaa'); + expect(dispatchSpy.calledTwice).toStrictEqual(true); + expect(setGasPrice).toHaveBeenCalled(); + expect(setGasLimit).toHaveBeenCalled(); + expect(setGasLimit).toHaveBeenCalledWith('ffff'); + expect(setGasPrice).toHaveBeenCalledWith('aaaa'); + }); + }); + + describe('updateConfirmTxGasAndCalculate()', () => { + it('should dispatch a updateGasAndCalculate action with the correct props', () => { + mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa'); + expect(dispatchSpy.callCount).toStrictEqual(3); + expect(setCustomGasPrice).toHaveBeenCalled(); + expect(setCustomGasLimit).toHaveBeenCalled(); + expect(setCustomGasLimit).toHaveBeenCalledWith('0xffff'); + expect(setCustomGasPrice).toHaveBeenCalledWith('0xaaaa'); + }); + }); + }); + + describe('mergeProps', () => { + let stateProps; + let dispatchProps; + let ownProps; + + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + isConfirm: true, + someOtherStateProp: 'baz', + transaction: {}, + }; + dispatchProps = { + updateCustomGasPrice: sinon.spy(), + hideGasButtonGroup: sinon.spy(), + setGasData: sinon.spy(), + updateConfirmTxGasAndCalculate: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + createSpeedUpTransaction: sinon.spy(), + hideSidebar: sinon.spy(), + hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), + }; + ownProps = { someOwnProp: 123 }; + }); + + it('should return the expected props when isConfirm is true', () => { + const result = mergeProps(stateProps, dispatchProps, ownProps); + + expect(result.isConfirm).toStrictEqual(true); + expect(result.someOtherStateProp).toStrictEqual('baz'); + expect( + result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, + ).toStrictEqual('foo'); + expect( + result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, + ).toStrictEqual('bar'); + expect(result.someOwnProp).toStrictEqual(123); + + expect( + dispatchProps.updateConfirmTxGasAndCalculate.callCount, + ).toStrictEqual(0); + expect(dispatchProps.setGasData.callCount).toStrictEqual(0); + expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0); + expect(dispatchProps.hideModal.callCount).toStrictEqual(0); + + result.onSubmit(); + + expect( + dispatchProps.updateConfirmTxGasAndCalculate.callCount, + ).toStrictEqual(1); + expect(dispatchProps.setGasData.callCount).toStrictEqual(0); + expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0); + expect(dispatchProps.hideModal.callCount).toStrictEqual(1); + + expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0); + result.gasPriceButtonGroupProps.handleGasPriceSelection({ + gasPrice: '0x0', + }); + expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(1); + + expect(dispatchProps.someOtherDispatchProp.callCount).toStrictEqual(0); + result.someOtherDispatchProp(); + expect(dispatchProps.someOtherDispatchProp.callCount).toStrictEqual(1); + }); + + it('should return the expected props when isConfirm is false', () => { + const result = mergeProps( + { ...stateProps, isConfirm: false }, + dispatchProps, + ownProps, + ); + + expect(result.isConfirm).toStrictEqual(false); + expect(result.someOtherStateProp).toStrictEqual('baz'); + expect( + result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, + ).toStrictEqual('foo'); + expect( + result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, + ).toStrictEqual('bar'); + expect(result.someOwnProp).toStrictEqual(123); + + expect( + dispatchProps.updateConfirmTxGasAndCalculate.callCount, + ).toStrictEqual(0); + expect(dispatchProps.setGasData.callCount).toStrictEqual(0); + expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0); + expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(0); + + result.onSubmit('mockNewLimit', 'mockNewPrice'); + + expect( + dispatchProps.updateConfirmTxGasAndCalculate.callCount, + ).toStrictEqual(0); + expect(dispatchProps.setGasData.callCount).toStrictEqual(1); + expect(dispatchProps.setGasData.getCall(0).args).toStrictEqual([ + 'mockNewLimit', + 'mockNewPrice', + ]); + expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(1); + expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1); + + expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(0); + result.gasPriceButtonGroupProps.handleGasPriceSelection({ + gasPrice: '0x0', + }); + expect(dispatchProps.updateCustomGasPrice.callCount).toStrictEqual(1); + + expect(dispatchProps.someOtherDispatchProp.callCount).toStrictEqual(0); + result.someOtherDispatchProp(); + expect(dispatchProps.someOtherDispatchProp.callCount).toStrictEqual(1); + }); + + it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => { + const result = mergeProps( + { ...stateProps, isSpeedUp: true, isConfirm: false }, + dispatchProps, + ownProps, + ); + + result.onSubmit(); + + expect( + dispatchProps.updateConfirmTxGasAndCalculate.callCount, + ).toStrictEqual(0); + expect(dispatchProps.setGasData.callCount).toStrictEqual(0); + expect(dispatchProps.hideGasButtonGroup.callCount).toStrictEqual(0); + expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1); + + expect(dispatchProps.createSpeedUpTransaction.callCount).toStrictEqual(1); + expect(dispatchProps.hideSidebar.callCount).toStrictEqual(1); + }); + }); +}); 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-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js deleted file mode 100644 index 0ba5aabbc..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ /dev/null @@ -1,515 +0,0 @@ -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; - -const actionSpies = { - hideModal: sinon.spy(), - setGasLimit: sinon.spy(), - setGasPrice: sinon.spy(), -}; - -const gasActionSpies = { - setCustomGasPrice: sinon.spy(), - setCustomGasLimit: sinon.spy(), - resetCustomData: sinon.spy(), -}; - -const sendActionSpies = { - hideGasButtonGroup: sinon.spy(), -}; - -proxyquire('../gas-modal-page-container.container.js', { - 'react-redux': { - connect: (ms, md, mp) => { - mapStateToProps = ms; - mapDispatchToProps = md; - mergeProps = mp; - return () => ({}); - }, - }, - '../../../../selectors': { - getBasicGasEstimateLoadingStatus: (s) => - `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, - getRenderableBasicEstimateData: (s) => - `mockRenderableBasicEstimateData:${Object.keys(s).length}`, - getDefaultActiveButtonIndex: (a, b) => a + b, - getCurrentEthBalance: (state) => state.metamask.balance || '0x0', - getSendToken: () => null, - getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0', - }, - '../../../../store/actions': actionSpies, - '../../../../ducks/gas/gas.duck': gasActionSpies, - '../../../../ducks/send/send.duck': sendActionSpies, -}); - -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; - - beforeEach(function () { - dispatchSpy = sinon.spy(); - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy); - }); - - afterEach(function () { - actionSpies.hideModal.resetHistory(); - gasActionSpies.setCustomGasPrice.resetHistory(); - gasActionSpies.setCustomGasLimit.resetHistory(); - }); - - describe('hideGasButtonGroup()', function () { - it('should dispatch a hideGasButtonGroup action', function () { - mapDispatchToPropsObject.hideGasButtonGroup(); - assert(dispatchSpy.calledOnce); - assert(sendActionSpies.hideGasButtonGroup.calledOnce); - }); - }); - - describe('cancelAndClose()', function () { - it('should dispatch a hideModal action', function () { - mapDispatchToPropsObject.cancelAndClose(); - assert(dispatchSpy.calledTwice); - assert(actionSpies.hideModal.calledOnce); - assert(gasActionSpies.resetCustomData.calledOnce); - }); - }); - - describe('updateCustomGasPrice()', function () { - it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', function () { - mapDispatchToPropsObject.updateCustomGasPrice('ffff'); - assert(dispatchSpy.calledOnce); - assert(gasActionSpies.setCustomGasPrice.calledOnce); - assert.strictEqual( - gasActionSpies.setCustomGasPrice.getCall(0).args[0], - '0xffff', - ); - }); - - it('should dispatch a setCustomGasPrice action', function () { - mapDispatchToPropsObject.updateCustomGasPrice('0xffff'); - assert(dispatchSpy.calledOnce); - assert(gasActionSpies.setCustomGasPrice.calledOnce); - assert.strictEqual( - gasActionSpies.setCustomGasPrice.getCall(0).args[0], - '0xffff', - ); - }); - }); - - describe('updateCustomGasLimit()', function () { - it('should dispatch a setCustomGasLimit action', function () { - mapDispatchToPropsObject.updateCustomGasLimit('0x10'); - assert(dispatchSpy.calledOnce); - assert(gasActionSpies.setCustomGasLimit.calledOnce); - assert.strictEqual( - gasActionSpies.setCustomGasLimit.getCall(0).args[0], - '0x10', - ); - }); - }); - - describe('setGasData()', function () { - it('should dispatch a setGasPrice and setGasLimit action with the correct props', function () { - mapDispatchToPropsObject.setGasData('ffff', 'aaaa'); - assert(dispatchSpy.calledTwice); - assert(actionSpies.setGasPrice.calledOnce); - assert(actionSpies.setGasLimit.calledOnce); - assert.strictEqual(actionSpies.setGasLimit.getCall(0).args[0], 'ffff'); - assert.strictEqual(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa'); - }); - }); - - describe('updateConfirmTxGasAndCalculate()', function () { - it('should dispatch a updateGasAndCalculate action with the correct props', function () { - mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa'); - assert.strictEqual(dispatchSpy.callCount, 3); - assert(actionSpies.setGasPrice.calledOnce); - assert(actionSpies.setGasLimit.calledOnce); - assert.strictEqual(actionSpies.setGasLimit.getCall(0).args[0], 'ffff'); - assert.strictEqual(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa'); - }); - }); - }); - - describe('mergeProps', function () { - let stateProps; - let dispatchProps; - let ownProps; - - beforeEach(function () { - stateProps = { - gasPriceButtonGroupProps: { - someGasPriceButtonGroupProp: 'foo', - anotherGasPriceButtonGroupProp: 'bar', - }, - isConfirm: true, - someOtherStateProp: 'baz', - transaction: {}, - }; - dispatchProps = { - updateCustomGasPrice: sinon.spy(), - hideGasButtonGroup: sinon.spy(), - setGasData: sinon.spy(), - updateConfirmTxGasAndCalculate: sinon.spy(), - someOtherDispatchProp: sinon.spy(), - createSpeedUpTransaction: sinon.spy(), - hideSidebar: sinon.spy(), - hideModal: sinon.spy(), - cancelAndClose: sinon.spy(), - }; - ownProps = { someOwnProp: 123 }; - }); - - afterEach(function () { - dispatchProps.updateCustomGasPrice.resetHistory(); - dispatchProps.hideGasButtonGroup.resetHistory(); - dispatchProps.setGasData.resetHistory(); - dispatchProps.updateConfirmTxGasAndCalculate.resetHistory(); - dispatchProps.someOtherDispatchProp.resetHistory(); - dispatchProps.createSpeedUpTransaction.resetHistory(); - dispatchProps.hideSidebar.resetHistory(); - dispatchProps.hideModal.resetHistory(); - }); - it('should return the expected props when isConfirm is true', function () { - const result = mergeProps(stateProps, dispatchProps, ownProps); - - assert.strictEqual(result.isConfirm, true); - assert.strictEqual(result.someOtherStateProp, 'baz'); - assert.strictEqual( - result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, - 'foo', - ); - assert.strictEqual( - result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, - 'bar', - ); - assert.strictEqual(result.someOwnProp, 123); - - assert.strictEqual( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - 0, - ); - assert.strictEqual(dispatchProps.setGasData.callCount, 0); - assert.strictEqual(dispatchProps.hideGasButtonGroup.callCount, 0); - assert.strictEqual(dispatchProps.hideModal.callCount, 0); - - result.onSubmit(); - - assert.strictEqual( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - 1, - ); - assert.strictEqual(dispatchProps.setGasData.callCount, 0); - assert.strictEqual(dispatchProps.hideGasButtonGroup.callCount, 0); - assert.strictEqual(dispatchProps.hideModal.callCount, 1); - - assert.strictEqual(dispatchProps.updateCustomGasPrice.callCount, 0); - result.gasPriceButtonGroupProps.handleGasPriceSelection({ - gasPrice: '0x0', - }); - assert.strictEqual(dispatchProps.updateCustomGasPrice.callCount, 1); - - assert.strictEqual(dispatchProps.someOtherDispatchProp.callCount, 0); - result.someOtherDispatchProp(); - assert.strictEqual(dispatchProps.someOtherDispatchProp.callCount, 1); - }); - - it('should return the expected props when isConfirm is false', function () { - const result = mergeProps( - { ...stateProps, isConfirm: false }, - dispatchProps, - ownProps, - ); - - assert.strictEqual(result.isConfirm, false); - assert.strictEqual(result.someOtherStateProp, 'baz'); - assert.strictEqual( - result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, - 'foo', - ); - assert.strictEqual( - result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, - 'bar', - ); - assert.strictEqual(result.someOwnProp, 123); - - assert.strictEqual( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - 0, - ); - assert.strictEqual(dispatchProps.setGasData.callCount, 0); - assert.strictEqual(dispatchProps.hideGasButtonGroup.callCount, 0); - assert.strictEqual(dispatchProps.cancelAndClose.callCount, 0); - - result.onSubmit('mockNewLimit', 'mockNewPrice'); - - assert.strictEqual( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - 0, - ); - assert.strictEqual(dispatchProps.setGasData.callCount, 1); - assert.deepStrictEqual(dispatchProps.setGasData.getCall(0).args, [ - 'mockNewLimit', - 'mockNewPrice', - ]); - assert.strictEqual(dispatchProps.hideGasButtonGroup.callCount, 1); - assert.strictEqual(dispatchProps.cancelAndClose.callCount, 1); - - assert.strictEqual(dispatchProps.updateCustomGasPrice.callCount, 0); - result.gasPriceButtonGroupProps.handleGasPriceSelection({ - gasPrice: '0x0', - }); - assert.strictEqual(dispatchProps.updateCustomGasPrice.callCount, 1); - - assert.strictEqual(dispatchProps.someOtherDispatchProp.callCount, 0); - result.someOtherDispatchProp(); - assert.strictEqual(dispatchProps.someOtherDispatchProp.callCount, 1); - }); - - it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', function () { - const result = mergeProps( - { ...stateProps, isSpeedUp: true, isConfirm: false }, - dispatchProps, - ownProps, - ); - - result.onSubmit(); - - assert.strictEqual( - dispatchProps.updateConfirmTxGasAndCalculate.callCount, - 0, - ); - assert.strictEqual(dispatchProps.setGasData.callCount, 0); - assert.strictEqual(dispatchProps.hideGasButtonGroup.callCount, 0); - assert.strictEqual(dispatchProps.cancelAndClose.callCount, 1); - - assert.strictEqual(dispatchProps.createSpeedUpTransaction.callCount, 1); - assert.strictEqual(dispatchProps.hideSidebar.callCount, 1); - }); - }); -}); 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 64% 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..2cb5cb428 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,18 +1,17 @@ -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 () { +describe('GasPriceButtonGroup Component', () => { let mockButtonPropsAndFlags; let mockGasPriceButtonGroupProps; let wrapper; - beforeEach(function () { + beforeEach(() => { mockGasPriceButtonGroupProps = { buttonDataLoading: false, className: 'gas-price-button-group', @@ -60,59 +59,59 @@ describe('GasPriceButtonGroup Component', function () { ); }); - afterEach(function () { + afterEach(() => { sinon.restore(); }); - describe('render', function () { - it('should render a ButtonGroup', function () { - assert(wrapper.is(ButtonGroup)); + describe('render', () => { + it('should render a ButtonGroup', () => { + expect(wrapper.is(ButtonGroup)).toStrictEqual(true); }); - it('should render the correct props on the ButtonGroup', function () { + it('should render the correct props on the ButtonGroup', () => { const { className, defaultActiveButtonIndex, noButtonActiveByDefault, } = wrapper.props(); - assert.strictEqual(className, 'gas-price-button-group'); - assert.strictEqual(defaultActiveButtonIndex, 2); - assert.strictEqual(noButtonActiveByDefault, true); + expect(className).toStrictEqual('gas-price-button-group'); + expect(defaultActiveButtonIndex).toStrictEqual(2); + expect(noButtonActiveByDefault).toStrictEqual(true); }); function renderButtonArgsTest(i, mockPropsAndFlags) { - assert.deepStrictEqual( + expect( GasPriceButtonGroup.prototype.renderButton.getCall(i).args, - [ - { ...mockGasPriceButtonGroupProps.gasButtonInfo[i] }, - mockPropsAndFlags, - i, - ], - ); + ).toStrictEqual([ + { ...mockGasPriceButtonGroupProps.gasButtonInfo[i] }, + mockPropsAndFlags, + i, + ]); } - it('should call this.renderButton 3 times, with the correct args', function () { - assert.strictEqual( + it('should call this.renderButton 3 times, with the correct args', () => { + expect( GasPriceButtonGroup.prototype.renderButton.callCount, - 3, - ); + ).toStrictEqual(3); renderButtonArgsTest(0, mockButtonPropsAndFlags); renderButtonArgsTest(1, mockButtonPropsAndFlags); renderButtonArgsTest(2, mockButtonPropsAndFlags); }); - it('should show loading if buttonDataLoading', function () { + it('should show loading if buttonDataLoading', () => { wrapper.setProps({ buttonDataLoading: true }); - assert(wrapper.is('div')); - assert(wrapper.hasClass('gas-price-button-group__loading-container')); - assert.strictEqual(wrapper.text(), 'loading'); + expect(wrapper.is('div')).toStrictEqual(true); + expect( + wrapper.hasClass('gas-price-button-group__loading-container'), + ).toStrictEqual(true); + expect(wrapper.text()).toStrictEqual('loading'); }); }); - describe('renderButton', function () { + describe('renderButton', () => { let wrappedRenderButtonResult; - beforeEach(function () { + beforeEach(() => { GasPriceButtonGroup.prototype.renderButtonContent.resetHistory(); const renderButtonResult = wrapper .instance() @@ -123,38 +122,33 @@ describe('GasPriceButtonGroup Component', function () { wrappedRenderButtonResult = shallow(renderButtonResult); }); - it('should render a button', function () { - assert.strictEqual(wrappedRenderButtonResult.type(), 'button'); + it('should render a button', () => { + expect(wrappedRenderButtonResult.type()).toStrictEqual('button'); }); - it('should call the correct method when clicked', function () { - assert.strictEqual( + it('should call the correct method when clicked', () => { + expect( mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, - 0, - ); + ).toStrictEqual(0); wrappedRenderButtonResult.props().onClick(); - assert.strictEqual( + expect( mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, - 1, - ); - assert.deepStrictEqual( + ).toStrictEqual(1); + expect( mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args, - [ - { - gasPrice: - mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei, - gasEstimateType: - mockGasPriceButtonGroupProps.gasButtonInfo[0].gasEstimateType, - }, - ], - ); + ).toStrictEqual([ + { + gasPrice: mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei, + gasEstimateType: + mockGasPriceButtonGroupProps.gasButtonInfo[0].gasEstimateType, + }, + ]); }); - it('should call this.renderButtonContent with the correct args', function () { - assert.strictEqual( + it('should call this.renderButtonContent with the correct args', () => { + expect( GasPriceButtonGroup.prototype.renderButtonContent.callCount, - 1, - ); + ).toStrictEqual(1); const { feeInPrimaryCurrency, feeInSecondaryCurrency, @@ -162,26 +156,25 @@ describe('GasPriceButtonGroup Component', function () { gasEstimateType, } = mockGasPriceButtonGroupProps.gasButtonInfo[0]; const { showCheck, className } = mockGasPriceButtonGroupProps; - assert.deepStrictEqual( + expect( GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args, - [ - { - gasEstimateType, - feeInPrimaryCurrency, - feeInSecondaryCurrency, - timeEstimate, - }, - { - showCheck, - className, - }, - ], - ); + ).toStrictEqual([ + { + gasEstimateType, + feeInPrimaryCurrency, + feeInSecondaryCurrency, + timeEstimate, + }, + { + showCheck, + className, + }, + ]); }); }); - describe('renderButtonContent', function () { - it('should render a label if passed a gasEstimateType', function () { + describe('renderButtonContent', () => { + it('should render a label if passed a gasEstimateType', () => { const renderButtonContentResult = wrapper.instance().renderButtonContent( { gasEstimateType: 'SLOW', @@ -193,17 +186,15 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual( - wrappedRenderButtonContentResult.childAt(0).children().length, - 1, - ); - assert.strictEqual( + expect( + wrappedRenderButtonContentResult.childAt(0).children(), + ).toHaveLength(1); + expect( wrappedRenderButtonContentResult.find('.someClass__label').text(), - 'slow', - ); + ).toStrictEqual('slow'); }); - it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', function () { + it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => { const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent( { feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', @@ -215,19 +206,17 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual( - wrappedRenderButtonContentResult.childAt(0).children().length, - 1, - ); - assert.strictEqual( + expect( + wrappedRenderButtonContentResult.childAt(0).children(), + ).toHaveLength(1); + expect( wrappedRenderButtonContentResult .find('.someClass__primary-currency') .text(), - 'mockFeeInPrimaryCurrency', - ); + ).toStrictEqual('mockFeeInPrimaryCurrency'); }); - it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', function () { + it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => { const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent( { feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', @@ -239,19 +228,17 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual( - wrappedRenderButtonContentResult.childAt(0).children().length, - 1, - ); - assert.strictEqual( + expect( + wrappedRenderButtonContentResult.childAt(0).children(), + ).toHaveLength(1); + expect( wrappedRenderButtonContentResult .find('.someClass__secondary-currency') .text(), - 'mockFeeInSecondaryCurrency', - ); + ).toStrictEqual('mockFeeInSecondaryCurrency'); }); - it('should render a timeEstimate if passed a timeEstimate', function () { + it('should render a timeEstimate if passed a timeEstimate', () => { const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent( { timeEstimate: 'mockTimeEstimate', @@ -263,19 +250,17 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual( - wrappedRenderButtonContentResult.childAt(0).children().length, - 1, - ); - assert.strictEqual( + expect( + wrappedRenderButtonContentResult.childAt(0).children(), + ).toHaveLength(1); + expect( wrappedRenderButtonContentResult .find('.someClass__time-estimate') .text(), - 'mockTimeEstimate', - ); + ).toStrictEqual('mockTimeEstimate'); }); - it('should render a check if showCheck is true', function () { + it('should render a check if showCheck is true', () => { const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent( {}, { @@ -286,13 +271,12 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual( - wrappedRenderButtonContentResult.find('.fa-check').length, + expect(wrappedRenderButtonContentResult.find('.fa-check')).toHaveLength( 1, ); }); - it('should render all elements if all args passed', function () { + it('should render all elements if all args passed', () => { const renderButtonContentResult = wrapper.instance().renderButtonContent( { gasEstimateType: 'SLOW', @@ -308,10 +292,10 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual(wrappedRenderButtonContentResult.children().length, 5); + expect(wrappedRenderButtonContentResult.children()).toHaveLength(5); }); - it('should render no elements if all args passed', function () { + it('should render no elements if all args passed', () => { const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent( {}, {}, @@ -319,7 +303,7 @@ describe('GasPriceButtonGroup Component', function () { const wrappedRenderButtonContentResult = shallow( renderButtonContentResult, ); - assert.strictEqual(wrappedRenderButtonContentResult.children().length, 0); + expect(wrappedRenderButtonContentResult.children()).toHaveLength(0); }); }); }); 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 54% 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..0d81f7372 100644 --- a/ui/app/components/app/info-box/tests/info-box.test.js +++ b/ui/app/components/app/info-box/info-box.test.js @@ -1,11 +1,10 @@ -import assert from 'assert'; import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import InfoBox from '..'; +import InfoBox from './info-box.component'; -describe('InfoBox', function () { +describe('InfoBox', () => { let wrapper; const props = { @@ -14,23 +13,23 @@ describe('InfoBox', function () { onClose: sinon.spy(), }; - beforeEach(function () { + beforeEach(() => { wrapper = shallow(); }); - it('renders title from props', function () { + it('renders title from props', () => { const title = wrapper.find('.info-box__title'); - assert.strictEqual(title.text(), props.title); + expect(title.text()).toStrictEqual(props.title); }); - it('renders description from props', function () { + it('renders description from props', () => { const description = wrapper.find('.info-box__description'); - assert.strictEqual(description.text(), props.description); + expect(description.text()).toStrictEqual(props.description); }); - it('closes info box', function () { + it('closes info box', () => { const close = wrapper.find('.info-box__close'); close.simulate('click'); - assert(props.onClose.calledOnce); + expect(props.onClose.calledOnce).toStrictEqual(true); }); }); 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 71% 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..5db320352 100644 --- a/ui/app/components/app/menu-bar/tests/menu-bar.test.js +++ b/ui/app/components/app/menu-bar/menu-bar.test.js @@ -1,10 +1,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: {}, @@ -30,22 +29,22 @@ const initState = { }; const mockStore = configureStore(); -describe('MenuBar', function () { - it('opens account detail menu when account options is clicked', function () { +describe('MenuBar', () => { + it('opens account detail menu when account options is clicked', () => { const store = mockStore(initState); const wrapper = mountWithRouter( , ); - assert.ok(!wrapper.exists('AccountOptionsMenu')); + expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); const accountOptions = wrapper.find('.menu-bar__account-options'); accountOptions.simulate('click'); wrapper.update(); - assert.ok(wrapper.exists('AccountOptionsMenu')); + expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); }); - it('sets accountDetailsMenuOpen to false when closed', function () { + it('sets accountDetailsMenuOpen to false when closed', () => { const store = mockStore(initState); const wrapper = mountWithRouter( @@ -55,10 +54,10 @@ describe('MenuBar', function () { const accountOptions = wrapper.find('.menu-bar__account-options'); accountOptions.simulate('click'); wrapper.update(); - assert.ok(wrapper.exists('AccountOptionsMenu')); + expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); const accountDetailsMenu = wrapper.find('AccountOptionsMenu'); accountDetailsMenu.prop('onClose')(); wrapper.update(); - assert.ok(!wrapper.exists('AccountOptionsMenu')); + expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); }); }); diff --git a/ui/app/components/app/modal/modal-content/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/modal-content.component.test.js new file mode 100644 index 000000000..ec7bc725d --- /dev/null +++ b/ui/app/components/app/modal/modal-content/modal-content.component.test.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ModalContent from './modal-content.component'; + +describe('ModalContent Component', () => { + it('should render a title', () => { + const wrapper = shallow(); + + expect(wrapper.find('.modal-content__title')).toHaveLength(1); + expect(wrapper.find('.modal-content__title').text()).toStrictEqual( + 'Modal Title', + ); + expect(wrapper.find('.modal-content__description')).toHaveLength(0); + }); + + it('should render a description', () => { + const wrapper = shallow(); + + expect(wrapper.find('.modal-content__title')).toHaveLength(0); + expect(wrapper.find('.modal-content__description')).toHaveLength(1); + expect(wrapper.find('.modal-content__description').text()).toStrictEqual( + 'Modal Description', + ); + }); + + it('should render both a title and a description', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find('.modal-content__title')).toHaveLength(1); + expect(wrapper.find('.modal-content__title').text()).toStrictEqual( + 'Modal Title', + ); + expect(wrapper.find('.modal-content__description')).toHaveLength(1); + expect(wrapper.find('.modal-content__description').text()).toStrictEqual( + 'Modal Description', + ); + }); +}); diff --git a/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js deleted file mode 100644 index 485b2f951..000000000 --- a/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import ModalContent from '../modal-content.component'; - -describe('ModalContent Component', function () { - it('should render a title', function () { - const wrapper = shallow(); - - assert.strictEqual(wrapper.find('.modal-content__title').length, 1); - assert.strictEqual( - wrapper.find('.modal-content__title').text(), - 'Modal Title', - ); - assert.strictEqual(wrapper.find('.modal-content__description').length, 0); - }); - - it('should render a description', function () { - const wrapper = shallow(); - - assert.strictEqual(wrapper.find('.modal-content__title').length, 0); - assert.strictEqual(wrapper.find('.modal-content__description').length, 1); - assert.strictEqual( - wrapper.find('.modal-content__description').text(), - 'Modal Description', - ); - }); - - it('should render both a title and a description', function () { - const wrapper = shallow( - , - ); - - assert.strictEqual(wrapper.find('.modal-content__title').length, 1); - assert.strictEqual( - wrapper.find('.modal-content__title').text(), - 'Modal Title', - ); - assert.strictEqual(wrapper.find('.modal-content__description').length, 1); - assert.strictEqual( - wrapper.find('.modal-content__description').text(), - 'Modal Description', - ); - }); -}); diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 89b814610..3ad24196b 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -21,11 +21,13 @@ export default class Modal extends PureComponent { onCancel: PropTypes.func, cancelType: PropTypes.string, cancelText: PropTypes.string, + rounded: PropTypes.bool, }; static defaultProps = { submitType: 'secondary', cancelType: 'default', + rounded: false, }; render() { @@ -43,6 +45,7 @@ export default class Modal extends PureComponent { contentClass, containerClass, hideFooter, + rounded, } = this.props; return ( @@ -61,6 +64,7 @@ export default class Modal extends PureComponent { {onCancel && (
    + + + {t('editNonceMessage')} + + + + + + + {t('editNonceField')} + + + + + +
    + { + setCustomNonce(e.target.value); + }} + fullWidth + margin="dense" + value={customNonce} + id="custom-nonce-id" + /> +
    +
    +
    + + ); +}; + +CustomizeNonce.propTypes = { + hideModal: PropTypes.func.isRequired, + customNonceValue: PropTypes.string, + nextNonce: PropTypes.number, + updateCustomNonce: PropTypes.func, + getNextNonce: PropTypes.func, +}; +export default withModalProps(CustomizeNonce); diff --git a/ui/app/components/app/modals/customize-nonce/index.js b/ui/app/components/app/modals/customize-nonce/index.js new file mode 100644 index 000000000..27fa704ae --- /dev/null +++ b/ui/app/components/app/modals/customize-nonce/index.js @@ -0,0 +1 @@ +export { default } from './customize-nonce.component'; diff --git a/ui/app/components/app/modals/customize-nonce/index.scss b/ui/app/components/app/modals/customize-nonce/index.scss new file mode 100644 index 000000000..533071977 --- /dev/null +++ b/ui/app/components/app/modals/customize-nonce/index.scss @@ -0,0 +1,53 @@ +.customize-nonce-modal { + padding-left: 24px; + padding-right: 18px; + display: flex; + flex-flow: column nowrap; + + &__main-header { + display: flex; + align-items: center; + padding-top: 24px; + } + + &__main-title { + flex: 1; + } + + &__close { + @include H4; + + color: $ui-black; + background: none; + flex: 0; + align-self: flex-start; + } + + & &__link { + @include H6; + + display: inline; + padding-left: 5px; + } + + & &__reset { + @include H7; + } + + &__input { + input { + @include Paragraph; + + width: 100%; + } + } +} + +.customize-nonce-modal-content { + padding: 0; +} + +.customize-nonce-modal-container { + height: 324px; + width: 100%; +} 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/index.scss b/ui/app/components/app/modals/index.scss index 0a48e41c4..742190ab9 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -11,6 +11,7 @@ @import 'new-account-modal/index'; @import 'qr-scanner/index'; @import 'transaction-confirmed/index'; +@import 'customize-nonce/index'; .modal { z-index: 1050; 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 53% 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..fc45751b5 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 @@ -1,11 +1,10 @@ -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 () { +describe('MetaMetrics Opt In', () => { let wrapper; const props = { @@ -14,7 +13,7 @@ describe('MetaMetrics Opt In', function () { participateInMetaMetrics: null, }; - beforeEach(function () { + beforeEach(() => { wrapper = mount(, { context: { metricsEvent: () => undefined, @@ -23,40 +22,36 @@ describe('MetaMetrics Opt In', function () { }); }); - afterEach(function () { + afterEach(() => { props.setParticipateInMetaMetrics.resetHistory(); props.hideModal.resetHistory(); }); - it('passes false to setParticipateInMetaMetrics and hides modal', function (done) { + it('passes false to setParticipateInMetaMetrics and hides modal', async () => { const noThanks = wrapper.find('.btn-default.page-container__footer-button'); noThanks.simulate('click'); - setImmediate(() => { - assert(props.setParticipateInMetaMetrics.calledOnce); - assert.strictEqual( - props.setParticipateInMetaMetrics.getCall(0).args[0], - false, - ); - assert(props.hideModal.calledOnce); - done(); - }); + expect(await props.setParticipateInMetaMetrics.calledOnce).toStrictEqual( + true, + ); + expect(props.setParticipateInMetaMetrics.getCall(0).args[0]).toStrictEqual( + false, + ); + expect(props.hideModal.calledOnce).toStrictEqual(true); }); - it('passes true to setParticipateInMetaMetrics and hides modal', function (done) { + it('passes true to setParticipateInMetaMetrics and hides modal', async () => { const affirmAgree = wrapper.find( '.btn-primary.page-container__footer-button', ); affirmAgree.simulate('click'); - setImmediate(() => { - assert(props.setParticipateInMetaMetrics.calledOnce); - assert.strictEqual( - props.setParticipateInMetaMetrics.getCall(0).args[0], - true, - ); - assert(props.hideModal.calledOnce); - done(); - }); + expect(await props.setParticipateInMetaMetrics.calledOnce).toStrictEqual( + true, + ); + expect(props.setParticipateInMetaMetrics.getCall(0).args[0]).toStrictEqual( + true, + ); + expect(props.hideModal.calledOnce).toStrictEqual(true); }); }); diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index 9aa659d32..74373354c 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -29,6 +29,7 @@ import ConfirmDeleteNetwork from './confirm-delete-network'; import AddToAddressBookModal from './add-to-addressbook-modal'; import EditApprovalPermission from './edit-approval-permission'; import NewAccountModal from './new-account-modal'; +import CustomizeNonceModal from './customize-nonce'; const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -376,6 +377,19 @@ const MODALS = { }, }, + CUSTOMIZE_NONCE: { + contents: , + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + DEFAULT: { contents: [], mobileModalStyle: {}, 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 63% 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..043f9bab7 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 @@ -1,10 +1,9 @@ -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 () { +describe('Reject Transactions Model', () => { let wrapper; const props = { @@ -13,7 +12,7 @@ describe('Reject Transactions Model', function () { unapprovedTxCount: 2, }; - beforeEach(function () { + beforeEach(() => { wrapper = mount(, { context: { t: (str) => str, @@ -21,29 +20,26 @@ describe('Reject Transactions Model', function () { }); }); - afterEach(function () { + afterEach(() => { props.hideModal.resetHistory(); }); - it('hides modal when cancel button is clicked', function () { + it('hides modal when cancel button is clicked', () => { const cancelButton = wrapper.find( '.btn-default.modal-container__footer-button', ); cancelButton.simulate('click'); - assert(props.hideModal.calledOnce); + expect(props.hideModal.calledOnce).toStrictEqual(true); }); - it('onSubmit is called and hides modal when reject all clicked', function (done) { + it('onSubmit is called and hides modal when reject all clicked', async () => { const rejectAllButton = wrapper.find( '.btn-secondary.modal-container__footer-button', ); rejectAllButton.simulate('click'); - setImmediate(() => { - assert(props.onSubmit.calledOnce); - assert(props.hideModal.calledOnce); - done(); - }); + expect(await props.onSubmit.calledOnce).toStrictEqual(true); + expect(props.hideModal.calledOnce).toStrictEqual(true); }); }); 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 62% 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..84111a9ee 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 @@ -1,11 +1,10 @@ -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 () { +describe('Transaction Confirmed', () => { + it('clicks ok to submit and hide modal', () => { const props = { onSubmit: sinon.spy(), hideModal: sinon.spy(), @@ -23,7 +22,7 @@ describe('Transaction Confirmed', function () { ); submit.simulate('click'); - assert(props.onSubmit.calledOnce); - assert(props.hideModal.calledOnce); + expect(props.onSubmit.calledOnce).toStrictEqual(true); + expect(props.hideModal.calledOnce).toStrictEqual(true); }); }); 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({ { + it('should render checksummed address', () => { const wrapper = render( undefined } }, ); // Checksummed version of address is displayed - assert.strictEqual( - wrapper.find('.selected-account__address').text(), + expect(wrapper.find('.selected-account__address').text()).toStrictEqual( '0x1B82...5C9D', ); - assert.strictEqual( - wrapper.find('.selected-account__name').text(), + expect(wrapper.find('.selected-account__name').text()).toStrictEqual( 'testName', ); }); diff --git a/ui/app/components/app/sidebars/sidebar.component.js b/ui/app/components/app/sidebars/sidebar.component.js index 7a31f2be4..7b385db22 100644 --- a/ui/app/components/app/sidebars/sidebar.component.js +++ b/ui/app/components/app/sidebars/sidebar.component.js @@ -30,12 +30,12 @@ export default class Sidebar extends Component { renderSidebarContent() { const { type, sidebarProps = {} } = this.props; - const { transaction = {} } = sidebarProps; + const { transaction = {}, onSubmit } = sidebarProps; switch (type) { case 'customize-gas': return (
    - +
    ); 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 50% 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..90ecb5ab5 100644 --- a/ui/app/components/app/sidebars/tests/sidebars-component.test.js +++ b/ui/app/components/app/sidebars/sidebar.component.test.js @@ -1,20 +1,18 @@ -import assert from 'assert'; 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(), }; -describe('Sidebar Component', function () { +describe('Sidebar Component', () => { let wrapper; - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { propsMethodSpies.hideSidebar.resetHistory(); }); - describe('renderOverlay', function () { + describe('renderOverlay', () => { let renderOverlay; - beforeEach(function () { + beforeEach(() => { renderOverlay = shallow(wrapper.instance().renderOverlay()); }); - it('should render a overlay element', function () { - assert(renderOverlay.hasClass('sidebar-overlay')); + it('should render a overlay element', () => { + expect(renderOverlay.hasClass('sidebar-overlay')).toStrictEqual(true); }); - it('should pass the correct onClick function to the element', function () { - assert.strictEqual(propsMethodSpies.hideSidebar.callCount, 0); + it('should pass the correct onClick function to the element', () => { + expect(propsMethodSpies.hideSidebar.callCount).toStrictEqual(0); renderOverlay.props().onClick(); - assert.strictEqual(propsMethodSpies.hideSidebar.callCount, 1); + expect(propsMethodSpies.hideSidebar.callCount).toStrictEqual(1); }); }); - describe('renderSidebarContent', function () { + describe('renderSidebarContent', () => { let renderSidebarContent; - beforeEach(function () { + beforeEach(() => { renderSidebarContent = wrapper.instance().renderSidebarContent(); }); - it('should render sidebar content with the type customize-gas', function () { + it('should render sidebar content with the type customize-gas', () => { renderSidebarContent = wrapper.instance().renderSidebarContent(); const renderedSidebarContent = shallow(renderSidebarContent); - assert(renderedSidebarContent.hasClass('sidebar-left')); - assert(renderedSidebarContent.childAt(0).is(CustomizeGas)); + expect(renderedSidebarContent.hasClass('sidebar-left')).toStrictEqual( + true, + ); + expect(renderedSidebarContent.childAt(0).is(CustomizeGas)).toStrictEqual( + true, + ); }); - it('should not render with an unrecognized type', function () { + it('should not render with an unrecognized type', () => { wrapper.setProps({ type: 'foobar' }); renderSidebarContent = wrapper.instance().renderSidebarContent(); - assert.strictEqual(renderSidebarContent, null); + expect(renderSidebarContent).toBeNull(); }); }); - describe('render', function () { - it('should render a div with one child', function () { - assert(wrapper.is('div')); - assert.strictEqual(wrapper.children().length, 1); + describe('render', () => { + it('should render a div with one child', () => { + expect(wrapper.is('div')).toStrictEqual(true); + expect(wrapper.children()).toHaveLength(1); }); - it('should render the ReactCSSTransitionGroup without any children', function () { - assert(wrapper.children().at(0).is(ReactCSSTransitionGroup)); - assert.strictEqual(wrapper.children().at(0).children().length, 0); + it('should render the ReactCSSTransitionGroup without any children', () => { + expect( + wrapper.children().at(0).is(ReactCSSTransitionGroup), + ).toStrictEqual(true); + expect(wrapper.children().at(0).children()).toHaveLength(0); }); - it('should render sidebar content and the overlay if sidebarOpen is true', function () { + it('should render sidebar content and the overlay if sidebarOpen is true', () => { wrapper.setProps({ sidebarOpen: true }); - assert.strictEqual(wrapper.children().length, 2); - assert(wrapper.children().at(1).hasClass('sidebar-overlay')); - assert.strictEqual(wrapper.children().at(0).children().length, 1); - assert( + expect(wrapper.children()).toHaveLength(2); + expect( + wrapper.children().at(1).hasClass('sidebar-overlay'), + ).toStrictEqual(true); + expect(wrapper.children().at(0).children()).toHaveLength(1); + expect( wrapper.children().at(0).children().at(0).hasClass('sidebar-left'), - ); - assert( + ).toStrictEqual(true); + expect( wrapper .children() .at(0) @@ -96,7 +102,7 @@ describe('Sidebar Component', function () { .children() .at(0) .is(CustomizeGas), - ); + ).toBe(true); }); }); }); diff --git a/ui/app/components/app/signature-request-original/signature-request-original.component.js b/ui/app/components/app/signature-request-original/signature-request-original.component.js index 5dbcf9eb7..ae62fcad9 100644 --- a/ui/app/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/app/components/app/signature-request-original/signature-request-original.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import ethUtil from 'ethereumjs-util'; +import { stripHexPrefix } from 'ethereumjs-util'; import classnames from 'classnames'; import { ObjectInspector } from 'react-inspector'; @@ -179,7 +179,7 @@ export default class SignatureRequestOriginal extends Component { msgHexToText = (hex) => { try { - const stripped = ethUtil.stripHexPrefix(hex); + const stripped = stripHexPrefix(hex); const buff = Buffer.from(stripped, 'hex'); return buff.length === 32 ? hex : buff.toString('utf8'); } catch (e) { 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 53% 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..1afa1b98c 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,12 +1,11 @@ -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 () { +describe('Signature Request Component', () => { + describe('render', () => { const fromAddress = '0x123456789abcdef'; - it('should render a div with one child', function () { + it('should render a div with one child', () => { const wrapper = shallow( undefined} @@ -22,9 +21,9 @@ describe('Signature Request Component', function () { />, ); - assert(wrapper.is('div')); - assert.strictEqual(wrapper.length, 1); - assert(wrapper.hasClass('signature-request')); + expect(wrapper.is('div')).toStrictEqual(true); + expect(wrapper).toHaveLength(1); + expect(wrapper.hasClass('signature-request')).toStrictEqual(true); }); }); }); 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 87% 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..c4de25b1a 100644 --- a/ui/app/components/app/tests/signature-request.test.js +++ b/ui/app/components/app/signature-request/signature-request.container.test.js @@ -1,18 +1,17 @@ -import assert from 'assert'; 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 SignatureRequest from '../signature-request'; +import SignatureRequest from './signature-request.container'; -describe('Signature Request', function () { +describe('Signature Request', () => { let wrapper; const mockStore = { metamask: { provider: { - type: 'test', + type: 'transparent', }, accounts: { '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { @@ -51,7 +50,7 @@ describe('Signature Request', function () { }, }; - beforeEach(function () { + beforeEach(() => { wrapper = mountWithRouter( @@ -60,21 +59,21 @@ describe('Signature Request', function () { ); }); - afterEach(function () { + afterEach(() => { props.clearConfirmTransaction.resetHistory(); }); - it('cancel', function () { + it('cancel', () => { const cancelButton = wrapper.find('button.btn-default'); cancelButton.simulate('click'); - assert(props.cancel.calledOnce); + expect(props.cancel.calledOnce).toStrictEqual(true); }); - it('sign', function () { + it('sign', () => { const signButton = wrapper.find('button.btn-primary'); signButton.simulate('click'); - assert(props.sign.calledOnce); + expect(props.sign.calledOnce).toStrictEqual(true); }); }); diff --git a/ui/app/components/app/token-cell/token-cell.test.js b/ui/app/components/app/token-cell/token-cell.test.js index 9694e188c..12b3ae973 100644 --- a/ui/app/components/app/token-cell/token-cell.test.js +++ b/ui/app/components/app/token-cell/token-cell.test.js @@ -1,4 +1,3 @@ -import assert from 'assert'; import React from 'react'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; @@ -10,7 +9,7 @@ import { MemoryRouter } from 'react-router-dom'; import Identicon from '../../ui/identicon'; import TokenCell from '.'; -describe('Token Cell', function () { +describe('Token Cell', () => { let wrapper; const state = { @@ -41,7 +40,7 @@ describe('Token Cell', function () { let onClick; - beforeEach(function () { + beforeEach(() => { onClick = sinon.stub(); wrapper = mount( @@ -59,42 +58,38 @@ describe('Token Cell', function () { ); }); - afterEach(function () { + afterEach(() => { sinon.restore(); }); - it('renders Identicon with props from token cell', function () { - assert.strictEqual( - wrapper.find(Identicon).prop('address'), + it('renders Identicon with props from token cell', () => { + expect(wrapper.find(Identicon).prop('address')).toStrictEqual( '0xAnotherToken', ); - assert.strictEqual(wrapper.find(Identicon).prop('image'), './test-image'); + expect(wrapper.find(Identicon).prop('image')).toStrictEqual('./test-image'); }); - it('renders token balance', function () { - assert.strictEqual( - wrapper.find('.asset-list-item__token-value').text(), + it('renders token balance', () => { + expect(wrapper.find('.asset-list-item__token-value').text()).toStrictEqual( '5.000', ); }); - it('renders token symbol', function () { - assert.strictEqual( - wrapper.find('.asset-list-item__token-symbol').text(), + it('renders token symbol', () => { + expect(wrapper.find('.asset-list-item__token-symbol').text()).toStrictEqual( 'TEST', ); }); - it('renders converted fiat amount', function () { - assert.strictEqual( - wrapper.find('.list-item__subheading').text(), + it('renders converted fiat amount', () => { + expect(wrapper.find('.list-item__subheading').text()).toStrictEqual( '$0.52 USD', ); }); - it('calls onClick when clicked', function () { - assert.ok(!onClick.called); + it('calls onClick when clicked', () => { + expect(!onClick.called).toStrictEqual(true); wrapper.simulate('click'); - assert.ok(onClick.called); + expect(onClick.called).toStrictEqual(true); }); }); 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 83% 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..a4331aac5 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,10 +1,9 @@ -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 () { +describe('TransactionActivityLog Component', () => { + it('should render properly', () => { const activities = [ { eventKey: 'transactionCreated', @@ -54,11 +53,11 @@ describe('TransactionActivityLog Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-activity-log')); - assert.ok(wrapper.hasClass('test-class')); + expect(wrapper.hasClass('transaction-activity-log')).toStrictEqual(true); + expect(wrapper.hasClass('test-class')).toStrictEqual(true); }); - it('should render inline retry and cancel buttons for earliest pending transaction', function () { + it('should render inline retry and cancel buttons for earliest pending transaction', () => { const activities = [ { eventKey: 'transactionCreated', @@ -107,15 +106,14 @@ describe('TransactionActivityLog Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-activity-log')); - assert.ok(wrapper.hasClass('test-class')); - assert.strictEqual( - wrapper.find('.transaction-activity-log__action-link').length, + expect(wrapper.hasClass('transaction-activity-log')).toStrictEqual(true); + expect(wrapper.hasClass('test-class')).toStrictEqual(true); + expect(wrapper.find('.transaction-activity-log__action-link')).toHaveLength( 2, ); }); - it('should not render inline retry and cancel buttons for newer pending transactions', function () { + it('should not render inline retry and cancel buttons for newer pending transactions', () => { const activities = [ { eventKey: 'transactionCreated', @@ -164,10 +162,9 @@ describe('TransactionActivityLog Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-activity-log')); - assert.ok(wrapper.hasClass('test-class')); - assert.strictEqual( - wrapper.find('.transaction-activity-log__action-link').length, + expect(wrapper.hasClass('transaction-activity-log')).toStrictEqual(true); + expect(wrapper.hasClass('test-class')).toStrictEqual(true); + expect(wrapper.find('.transaction-activity-log__action-link')).toHaveLength( 0, ); }); 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 65% 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..17302ce9d 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 @@ -1,20 +1,18 @@ -import assert from 'assert'; -import proxyquire from 'proxyquire'; - +/* eslint-disable import/unambiguous */ let mapStateToProps; -proxyquire('../transaction-activity-log.container.js', { - 'react-redux': { - connect: (ms) => { - mapStateToProps = ms; - return () => ({}); - }, +jest.mock('react-redux', () => ({ + connect: (ms) => { + mapStateToProps = ms; + return () => ({}); }, -}); +})); + +require('./transaction-activity-log.container.js'); -describe('TransactionActivityLog container', function () { - describe('mapStateToProps()', function () { - it('should return the correct props', function () { +describe('TransactionActivityLog container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { const mockState = { metamask: { conversionRate: 280.45, @@ -23,14 +21,14 @@ describe('TransactionActivityLog container', function () { }, }; - assert.deepStrictEqual(mapStateToProps(mockState), { + expect(mapStateToProps(mockState)).toStrictEqual({ conversionRate: 280.45, nativeCurrency: 'ETH', rpcPrefs: {}, }); }); - it('should return the correct props when on a custom network', function () { + it('should return the correct props when on a custom network', () => { const mockState = { metamask: { conversionRate: 280.45, @@ -49,7 +47,7 @@ describe('TransactionActivityLog container', function () { }, }; - assert.deepStrictEqual(mapStateToProps(mockState), { + expect(mapStateToProps(mockState)).toStrictEqual({ conversionRate: 280.45, nativeCurrency: 'ETH', rpcPrefs: { 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 93% 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..16211afd4 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 @@ -1,24 +1,23 @@ -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 () { - it('should return no activities for an empty list of transactions', function () { - assert.deepStrictEqual(combineTransactionHistories([]), []); +describe('TransactionActivityLog utils', () => { + describe('combineTransactionHistories', () => { + it('should return no activities for an empty list of transactions', () => { + expect(combineTransactionHistories([])).toStrictEqual([]); }); - it('should return activities for an array of transactions', function () { + it('should return activities for an array of transactions', () => { const transactions = [ { hash: @@ -233,15 +232,12 @@ describe('TransactionActivityLog utils', function () { }, ]; - assert.deepStrictEqual( - combineTransactionHistories(transactions), - expected, - ); + expect(combineTransactionHistories(transactions)).toStrictEqual(expected); }); }); - describe('getActivities', function () { - it('should return no activities for an empty history', function () { + describe('getActivities', () => { + it('should return no activities for an empty history', () => { const transaction = { history: [], id: 1, @@ -256,10 +252,10 @@ describe('TransactionActivityLog utils', function () { }, }; - assert.deepStrictEqual(getActivities(transaction), []); + expect(getActivities(transaction)).toStrictEqual([]); }); - it("should return activities for a transaction's history", function () { + it("should return activities for a transaction's history", () => { const transaction = { history: [ { @@ -440,7 +436,7 @@ describe('TransactionActivityLog utils', function () { }, ]; - assert.deepStrictEqual(getActivities(transaction, true), expectedResult); + expect(getActivities(transaction, true)).toStrictEqual(expectedResult); }); }); }); 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 53% 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..45117b571 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,11 +1,10 @@ -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 () { +describe('TransactionBreakdownRow Component', () => { + it('should render text properly', () => { const wrapper = shallow( Test @@ -13,18 +12,16 @@ describe('TransactionBreakdownRow Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-breakdown-row')); - assert.strictEqual( + expect(wrapper.hasClass('transaction-breakdown-row')).toStrictEqual(true); + expect( wrapper.find('.transaction-breakdown-row__title').text(), - 'test', - ); - assert.strictEqual( + ).toStrictEqual('test'); + expect( wrapper.find('.transaction-breakdown-row__value').text(), - 'Test', - ); + ).toStrictEqual('Test'); }); - it('should render components properly', function () { + it('should render components properly', () => { const wrapper = shallow( @@ -32,11 +29,12 @@ describe('TransactionBreakdownRow Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-breakdown-row')); - assert.strictEqual( + expect(wrapper.hasClass('transaction-breakdown-row')).toStrictEqual(true); + expect( wrapper.find('.transaction-breakdown-row__title').text(), - 'test', - ); - assert.ok(wrapper.find('.transaction-breakdown-row__value').find(Button)); + ).toStrictEqual('test'); + expect( + wrapper.find('.transaction-breakdown-row__value').find(Button), + ).toHaveLength(1); }); }); 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 58% 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..9b34276db 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,11 +1,10 @@ -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 () { +describe('TransactionBreakdown Component', () => { + it('should render properly', () => { const transaction = { history: [], id: 1, @@ -25,7 +24,7 @@ describe('TransactionBreakdown Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); - assert.ok(wrapper.hasClass('transaction-breakdown')); - assert.ok(wrapper.hasClass('test-class')); + expect(wrapper.hasClass('transaction-breakdown')).toStrictEqual(true); + expect(wrapper.hasClass('test-class')).toStrictEqual(true); }); }); 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 {
    { + it('should render properly', () => { const transaction = { history: [], id: 1, @@ -32,6 +31,7 @@ describe('TransactionListItemDetails Component', function () { const wrapper = shallow( undefined} title="Test Transaction Details" recipientAddress="0x1" senderAddress="0x2" @@ -43,14 +43,14 @@ describe('TransactionListItemDetails Component', function () { { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); const child = wrapper.childAt(0); - assert.ok(child.hasClass('transaction-list-item-details')); - assert.strictEqual(child.find(Button).length, 2); - assert.strictEqual(child.find(SenderToRecipient).length, 1); - assert.strictEqual(child.find(TransactionBreakdown).length, 1); - assert.strictEqual(child.find(TransactionActivityLog).length, 1); + expect(child.hasClass('transaction-list-item-details')).toStrictEqual(true); + expect(child.find(Button)).toHaveLength(2); + expect(child.find(SenderToRecipient)).toHaveLength(1); + expect(child.find(TransactionBreakdown)).toHaveLength(1); + expect(child.find(TransactionActivityLog)).toHaveLength(1); }); - it('should render a retry button', function () { + it('should render a retry button', () => { const transaction = { history: [], id: 1, @@ -76,6 +76,8 @@ describe('TransactionListItemDetails Component', function () { const wrapper = shallow( undefined} + title="Test Transaction Details" recipientAddress="0x1" senderAddress="0x2" tryReverseResolveAddress={() => undefined} @@ -89,15 +91,15 @@ describe('TransactionListItemDetails Component', function () { const child = wrapper.childAt(0); - assert.ok(child.hasClass('transaction-list-item-details')); - assert.strictEqual(child.find(Button).length, 3); + expect(child.hasClass('transaction-list-item-details')).toStrictEqual(true); + expect(child.find(Button)).toHaveLength(3); }); - it('should disable the Copy Tx ID and View In Etherscan buttons when tx hash is missing', function () { + it('should disable the Copy Tx ID and View In Etherscan buttons when tx hash is missing', () => { const transaction = { history: [], id: 1, - status: TRANSACTION_STATUSES.CONFIRMED, + status: 'confirmed', txParams: { from: '0x1', gas: '0x5208', @@ -116,6 +118,8 @@ describe('TransactionListItemDetails Component', function () { const wrapper = shallow( undefined} + title="Test Transaction Details" recipientAddress="0x1" senderAddress="0x2" tryReverseResolveAddress={() => undefined} @@ -128,17 +132,17 @@ describe('TransactionListItemDetails Component', function () { const child = wrapper.childAt(0); - assert.ok(child.hasClass('transaction-list-item-details')); + expect(child.hasClass('transaction-list-item-details')).toStrictEqual(true); const buttons = child.find(Button); - assert.strictEqual(buttons.at(0).prop('disabled'), true); - assert.strictEqual(buttons.at(1).prop('disabled'), true); + expect(buttons.at(0).prop('disabled')).toStrictEqual(true); + expect(buttons.at(1).prop('disabled')).toStrictEqual(true); }); - it('should render functional Copy Tx ID and View In Etherscan buttons when tx hash exists', function () { + it('should render functional Copy Tx ID and View In Etherscan buttons when tx hash exists', () => { const transaction = { history: [], id: 1, - status: TRANSACTION_STATUSES.CONFIRMED, + status: 'confirmed', hash: '0xaa', txParams: { from: '0x1', @@ -158,6 +162,8 @@ describe('TransactionListItemDetails Component', function () { const wrapper = shallow( undefined} + title="Test Transaction Details" recipientAddress="0x1" senderAddress="0x2" tryReverseResolveAddress={() => undefined} @@ -170,9 +176,9 @@ describe('TransactionListItemDetails Component', function () { const child = wrapper.childAt(0); - assert.ok(child.hasClass('transaction-list-item-details')); + expect(child.hasClass('transaction-list-item-details')).toStrictEqual(true); const buttons = child.find(Button); - assert.strictEqual(buttons.at(0).prop('disabled'), false); - assert.strictEqual(buttons.at(1).prop('disabled'), false); + expect(buttons.at(0).prop('disabled')).toStrictEqual(false); + expect(buttons.at(1).prop('disabled')).toStrictEqual(false); }); }); diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index ec29e04d6..d3f957904 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -118,13 +118,21 @@ export default function TransactionListItem({ ); - }, [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/tests/transaction-status.component.test.js deleted file mode 100644 index 2300081de..000000000 --- a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js +++ /dev/null @@ -1,71 +0,0 @@ -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'; - -describe('TransactionStatus Component', function () { - before(function () { - sinon.stub(i18nHook, 'useI18nContext').returns((str) => str.toUpperCase()); - }); - - it('should render CONFIRMED properly', function () { - const wrapper = mount( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.text(), 'June 1'); - }); - - it('should render PENDING properly when status is APPROVED', function () { - const wrapper = mount( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.text(), 'PENDING'); - assert.strictEqual(wrapper.find(Tooltip).props().title, 'test-title'); - }); - - it('should render PENDING properly', function () { - const wrapper = mount( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.text(), 'PENDING'); - }); - - it('should render QUEUED properly', function () { - const wrapper = mount(); - - assert.ok(wrapper); - assert.ok( - wrapper.find('.transaction-status--queued').length, - 'queued className not found', - ); - assert.strictEqual(wrapper.text(), 'QUEUED'); - }); - - it('should render UNAPPROVED properly', function () { - const wrapper = mount(); - - assert.ok(wrapper); - assert.ok( - wrapper.find('.transaction-status--unapproved').length, - 'unapproved className not found', - ); - assert.strictEqual(wrapper.text(), 'UNAPPROVED'); - }); - - after(function () { - sinon.restore(); - }); -}); diff --git a/ui/app/components/app/transaction-status/transaction-status.component.test.js b/ui/app/components/app/transaction-status/transaction-status.component.test.js new file mode 100644 index 000000000..220d898ff --- /dev/null +++ b/ui/app/components/app/transaction-status/transaction-status.component.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import sinon from 'sinon'; +import * as i18nHook from '../../../hooks/useI18nContext'; +import Tooltip from '../../ui/tooltip'; +import TransactionStatus from './transaction-status.component'; + +describe('TransactionStatus Component', () => { + beforeAll(() => { + sinon.stub(i18nHook, 'useI18nContext').returns((str) => str.toUpperCase()); + }); + + afterAll(() => { + sinon.restore(); + }); + + it('should render CONFIRMED properly', () => { + const wrapper = mount( + , + ); + + expect(wrapper.find(TransactionStatus)).toHaveLength(1); + expect(wrapper.text()).toStrictEqual('June 1'); + }); + + it('should render PENDING properly when status is APPROVED', () => { + const wrapper = mount( + , + ); + + expect(wrapper.text()).toStrictEqual('PENDING'); + expect(wrapper.find(Tooltip).props().title).toStrictEqual('test-title'); + }); + + it('should render PENDING properly', () => { + const wrapper = mount( + , + ); + + expect(wrapper.find(TransactionStatus)).toHaveLength(1); + expect(wrapper.text()).toStrictEqual('PENDING'); + }); + + it('should render QUEUED properly', () => { + const wrapper = mount(); + + expect(wrapper.find(TransactionStatus)).toHaveLength(1); + expect(wrapper.find('.transaction-status--queued')).toHaveLength(1); + expect(wrapper.text()).toStrictEqual('QUEUED'); + }); + + it('should render UNAPPROVED properly', () => { + const wrapper = mount(); + + expect(wrapper.find(TransactionStatus)).toHaveLength(1); + expect(wrapper.find('.transaction-status--unapproved')).toHaveLength(1); + expect(wrapper.text()).toStrictEqual('UNAPPROVED'); + }); +}); 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/tests/user-preferenced-currency-display.component.test.js deleted file mode 100644 index 7b57d9265..000000000 --- a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js +++ /dev/null @@ -1,40 +0,0 @@ -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'; - -describe('UserPreferencedCurrencyDisplay Component', function () { - describe('rendering', function () { - beforeEach(function () { - sinon.stub(currencyHook, 'useCurrencyDisplay').returns(['1', {}]); - sinon - .stub(currencyPrefHook, 'useUserPreferencedCurrency') - .returns({ currency: 'ETH', decimals: 6 }); - }); - it('should render properly', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(CurrencyDisplay).length, 1); - }); - - it('should pass all props to the CurrencyDisplay child component', function () { - const wrapper = shallow( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(CurrencyDisplay).length, 1); - assert.strictEqual(wrapper.find(CurrencyDisplay).props().prop1, true); - assert.strictEqual(wrapper.find(CurrencyDisplay).props().prop2, 'test'); - assert.strictEqual(wrapper.find(CurrencyDisplay).props().prop3, 1); - }); - afterEach(function () { - sinon.restore(); - }); - }); -}); diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index dfcc235e2..aa1aba7e7 100644 --- a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -9,7 +9,7 @@ export default function UserPreferencedCurrencyDisplay({ ethLogoHeight = 12, ethNumberOfDecimals, fiatNumberOfDecimals, - numberOfDecimals: propsNumberOfDecimals, + 'numberOfDecimals': propsNumberOfDecimals, showEthLogo, type, ...restProps @@ -38,22 +38,22 @@ export default function UserPreferencedCurrencyDisplay({ } UserPreferencedCurrencyDisplay.propTypes = { - className: PropTypes.string, + 'className': PropTypes.string, 'data-testid': PropTypes.string, - prefix: PropTypes.string, - value: PropTypes.string, - numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - hideLabel: PropTypes.bool, - hideTitle: PropTypes.bool, - style: PropTypes.object, - showEthLogo: PropTypes.bool, - ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - type: PropTypes.oneOf([PRIMARY, SECONDARY]), - ethNumberOfDecimals: PropTypes.oneOfType([ + 'prefix': PropTypes.string, + 'value': PropTypes.string, + 'numberOfDecimals': PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + 'hideLabel': PropTypes.bool, + 'hideTitle': PropTypes.bool, + 'style': PropTypes.object, + 'showEthLogo': PropTypes.bool, + 'ethLogoHeight': PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + 'type': PropTypes.oneOf([PRIMARY, SECONDARY]), + 'ethNumberOfDecimals': PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), - fiatNumberOfDecimals: PropTypes.oneOfType([ + 'fiatNumberOfDecimals': PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js new file mode 100644 index 000000000..ed2a56fab --- /dev/null +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.test.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +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', () => { + describe('rendering', () => { + beforeEach(() => { + sinon.stub(currencyHook, 'useCurrencyDisplay').returns(['1', {}]); + sinon + .stub(currencyPrefHook, 'useUserPreferencedCurrency') + .returns({ currency: 'ETH', decimals: 6 }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should render properly', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(CurrencyDisplay)).toHaveLength(1); + }); + + it('should pass all props to the CurrencyDisplay child component', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(CurrencyDisplay)).toHaveLength(1); + expect(wrapper.find(CurrencyDisplay).props().prop1).toStrictEqual(true); + expect(wrapper.find(CurrencyDisplay).props().prop2).toStrictEqual('test'); + expect(wrapper.find(CurrencyDisplay).props().prop3).toStrictEqual(1); + }); + }); +}); 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/tests/user-preferenced-currency-input.component.test.js deleted file mode 100644 index f0c704459..000000000 --- a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js +++ /dev/null @@ -1,28 +0,0 @@ -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'; - -describe('UserPreferencedCurrencyInput Component', function () { - describe('rendering', function () { - it('should render properly', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(CurrencyInput).length, 1); - }); - - it('should render useFiat for CurrencyInput based on preferences.useNativeCurrencyAsPrimaryCurrency', function () { - const wrapper = shallow( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(CurrencyInput).length, 1); - assert.strictEqual(wrapper.find(CurrencyInput).props().useFiat, false); - wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }); - assert.strictEqual(wrapper.find(CurrencyInput).props().useFiat, true); - }); - }); -}); 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/tests/user-preferenced-currency-input.container.test.js deleted file mode 100644 index 40234c9c5..000000000 --- a/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import assert from 'assert'; -import proxyquire from 'proxyquire'; - -let mapStateToProps; - -proxyquire('../user-preferenced-currency-input.container.js', { - 'react-redux': { - connect: (ms) => { - mapStateToProps = ms; - return () => ({}); - }, - }, -}); - -describe('UserPreferencedCurrencyInput container', function () { - describe('mapStateToProps()', function () { - it('should return the correct props', function () { - const mockState = { - metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, - }, - }; - - assert.deepStrictEqual(mapStateToProps(mockState), { - useNativeCurrencyAsPrimaryCurrency: true, - }); - }); - }); -}); diff --git a/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js new file mode 100644 index 000000000..f7600595e --- /dev/null +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CurrencyInput from '../../ui/currency-input'; +import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component'; + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(CurrencyInput)).toHaveLength(1); + }); + + it('should render useFiat for CurrencyInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(CurrencyInput)).toHaveLength(1); + expect(wrapper.find(CurrencyInput).props().useFiat).toStrictEqual(false); + wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }); + expect(wrapper.find(CurrencyInput).props().useFiat).toStrictEqual(true); + }); + }); +}); diff --git a/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js new file mode 100644 index 000000000..dc73275a9 --- /dev/null +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.test.js @@ -0,0 +1,29 @@ +/* eslint-disable import/unambiguous */ +let mapStateToProps; + +jest.mock('react-redux', () => ({ + connect: (ms) => { + mapStateToProps = ms; + return () => ({}); + }, +})); + +require('./user-preferenced-currency-input.container.js'); + +describe('UserPreferencedCurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + + expect(mapStateToProps(mockState)).toStrictEqual({ + useNativeCurrencyAsPrimaryCurrency: true, + }); + }); + }); +}); 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/tests/user-preferenced-token-input.component.test.js deleted file mode 100644 index 3fc20f657..000000000 --- a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js +++ /dev/null @@ -1,33 +0,0 @@ -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'; - -describe('UserPreferencedCurrencyInput Component', function () { - describe('rendering', function () { - it('should render properly', function () { - const wrapper = shallow( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(TokenInput).length, 1); - }); - - it('should render showFiat for TokenInput based on preferences.useNativeCurrencyAsPrimaryCurrency', function () { - const wrapper = shallow( - , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find(TokenInput).length, 1); - assert.strictEqual(wrapper.find(TokenInput).props().showFiat, false); - wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }); - assert.strictEqual(wrapper.find(TokenInput).props().showFiat, true); - }); - }); -}); 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/tests/user-preferenced-token-input.container.test.js deleted file mode 100644 index 9ff33f0f5..000000000 --- a/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import assert from 'assert'; -import proxyquire from 'proxyquire'; - -let mapStateToProps; - -proxyquire('../user-preferenced-token-input.container.js', { - 'react-redux': { - connect: (ms) => { - mapStateToProps = ms; - return () => ({}); - }, - }, -}); - -describe('UserPreferencedTokenInput container', function () { - describe('mapStateToProps()', function () { - it('should return the correct props', function () { - const mockState = { - metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, - }, - }; - - assert.deepStrictEqual(mapStateToProps(mockState), { - useNativeCurrencyAsPrimaryCurrency: true, - }); - }); - }); -}); diff --git a/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js new file mode 100644 index 000000000..70e4ca0eb --- /dev/null +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.test.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TokenInput from '../../ui/token-input'; +import UserPreferencedTokenInput from './user-preferenced-token-input.component'; + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(TokenInput)).toHaveLength(1); + }); + + it('should render showFiat for TokenInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find(TokenInput)).toHaveLength(1); + expect(wrapper.find(TokenInput).props().showFiat).toStrictEqual(false); + wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }); + expect(wrapper.find(TokenInput).props().showFiat).toStrictEqual(true); + }); + }); +}); diff --git a/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js new file mode 100644 index 000000000..64c0e702e --- /dev/null +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.test.js @@ -0,0 +1,29 @@ +// eslint-disable-next-line import/unambiguous +let mapStateToProps; + +jest.mock('react-redux', () => ({ + connect: (ms) => { + mapStateToProps = ms; + return () => ({}); + }, +})); + +require('./user-preferenced-token-input.container.js'); + +describe('UserPreferencedTokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + + expect(mapStateToProps(mockState)).toStrictEqual({ + useNativeCurrencyAsPrimaryCurrency: true, + }); + }); + }); +}); diff --git a/ui/app/components/app/wallet-overview/eth-overview.js b/ui/app/components/app/wallet-overview/eth-overview.js index df79de100..812b3bc90 100644 --- a/ui/app/components/app/wallet-overview/eth-overview.js +++ b/ui/app/components/app/wallet-overview/eth-overview.js @@ -32,10 +32,7 @@ import { import SwapIcon from '../../ui/icon/swap-icon.component'; import BuyIcon from '../../ui/icon/overview-buy-icon.component'; import SendIcon from '../../ui/icon/overview-send-icon.component'; -import { - getSwapsFeatureLiveness, - setSwapsFromToken, -} from '../../../ducks/swaps/swaps'; +import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import IconButton from '../../ui/icon-button'; import WalletOverview from './wallet-overview'; @@ -73,7 +70,6 @@ const EthOverview = ({ className }) => { properties: { source: 'Main View', active_currency: 'ETH' }, category: 'swaps', }); - const swapsEnabled = useSelector(getSwapsFeatureLiveness); const defaultSwapsToken = useSelector(getSwapsDefaultToken); return ( @@ -138,34 +134,32 @@ const EthOverview = ({ className }) => { history.push(SEND_ROUTE); }} /> - {swapsEnabled ? ( - { - if (isSwapsChain) { - enteredSwapsEvent(); - dispatch(setSwapsFromToken(defaultSwapsToken)); - if (usingHardwareWallet) { - global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); - } else { - history.push(BUILD_QUOTE_ROUTE); - } + { + if (isSwapsChain) { + enteredSwapsEvent(); + dispatch(setSwapsFromToken(defaultSwapsToken)); + if (usingHardwareWallet) { + global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); + } else { + history.push(BUILD_QUOTE_ROUTE); } - }} - label={t('swap')} - tooltipRender={(contents) => ( - - {contents} - - )} - /> - ) : null} + } + }} + label={t('swap')} + tooltipRender={(contents) => ( + + {contents} + + )} + /> } className={className} diff --git a/ui/app/components/app/wallet-overview/token-overview.js b/ui/app/components/app/wallet-overview/token-overview.js index 953de1fd8..4b71e3dba 100644 --- a/ui/app/components/app/wallet-overview/token-overview.js +++ b/ui/app/components/app/wallet-overview/token-overview.js @@ -18,10 +18,7 @@ import { import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { updateSendToken } from '../../../store/actions'; -import { - getSwapsFeatureLiveness, - setSwapsFromToken, -} from '../../../ducks/swaps/swaps'; +import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { getAssetImages, getCurrentKeyring, @@ -63,7 +60,6 @@ const TokenOverview = ({ className, token }) => { properties: { source: 'Token View', active_currency: token.symbol }, category: 'swaps', }); - const swapsEnabled = useSelector(getSwapsFeatureLiveness); return ( { label={t('send')} data-testid="eth-overview-send" /> - {swapsEnabled ? ( - { - if (isSwapsChain) { - enteredSwapsEvent(); - dispatch( - setSwapsFromToken({ - ...token, - iconUrl: assetImages[token.address], - balance, - string: balanceToRender, - }), - ); - if (usingHardwareWallet) { - global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); - } else { - history.push(BUILD_QUOTE_ROUTE); - } + { + if (isSwapsChain) { + enteredSwapsEvent(); + dispatch( + setSwapsFromToken({ + ...token, + iconUrl: assetImages[token.address], + balance, + string: balanceToRender, + }), + ); + if (usingHardwareWallet) { + global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); + } else { + history.push(BUILD_QUOTE_ROUTE); } - }} - label={t('swap')} - tooltipRender={(contents) => ( - - {contents} - - )} - /> - ) : null} + } + }} + label={t('swap')} + tooltipRender={(contents) => ( + + {contents} + + )} + /> } className={className} diff --git a/ui/app/components/app/whats-new-popup/index.js b/ui/app/components/app/whats-new-popup/index.js new file mode 100644 index 000000000..2f20e4f4f --- /dev/null +++ b/ui/app/components/app/whats-new-popup/index.js @@ -0,0 +1 @@ +export { default } from './whats-new-popup'; diff --git a/ui/app/components/app/whats-new-popup/index.scss b/ui/app/components/app/whats-new-popup/index.scss new file mode 100644 index 000000000..8f2aca460 --- /dev/null +++ b/ui/app/components/app/whats-new-popup/index.scss @@ -0,0 +1,61 @@ +.whats-new-popup { + &__notifications { + display: flex; + flex-direction: column; + } + + &__notification, + &__first-notification { + display: flex; + flex-direction: column; + margin: 0 24px 24px 24px; + border-bottom: 1px solid $Grey-100; + } + + &__notification-image { + align-self: center; + margin-bottom: 16px; + } + + &__description-and-date { + margin-bottom: 16px; + } + + &__notification-date { + color: $Grey-500; + } + + &__button { + margin-right: auto; + } + + &__button, + &__link { + margin-bottom: 24px; + } + + &__link { + @include H6; + + color: $Blue-500; + cursor: pointer; + } + + &__notification-title { + @include H4; + + font-weight: bold; + margin-bottom: 8px; + } +} + +.popover-wrap.whats-new-popup__popover { + @media screen and (min-width: 576px) { + max-height: 600px; + width: 500px; + } + + @media screen and (max-width: 575px) { + max-height: 568px; + } +} diff --git a/ui/app/components/app/whats-new-popup/whats-new-popup.js b/ui/app/components/app/whats-new-popup/whats-new-popup.js new file mode 100644 index 000000000..3eaad0af3 --- /dev/null +++ b/ui/app/components/app/whats-new-popup/whats-new-popup.js @@ -0,0 +1,181 @@ +import React, { useContext, useMemo, useRef, useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { getCurrentLocale } from '../../../ducks/metamask/metamask'; +import { I18nContext } from '../../../contexts/i18n'; +import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; +import Button from '../../ui/button'; +import Popover from '../../ui/popover'; +import { updateViewedNotifications } from '../../../store/actions'; +import { getTranslatedUINoficiations } from '../../../../../shared/notifications'; +import { getSortedNotificationsToShow } from '../../../selectors'; + +function getActionFunctionById(id) { + const actionFunctions = { + 2: () => { + global.platform.openTab({ + url: + 'https://survey.alchemer.com/s3/6173069/MetaMask-Extension-NPS-January-2021', + }); + }, + 3: () => { + global.platform.openTab({ + url: 'https://community.metamask.io/t/about-the-security-category/72', + }); + }, + }; + + return actionFunctions[id]; +} + +const renderFirstNotification = (notification, idRefMap) => { + const { id, date, title, description, image, actionText } = notification; + const actionFunction = getActionFunctionById(id); + const imageComponent = image && ( + + ); + const placeImageBelowDescription = image?.placeImageBelowDescription; + return ( +
    + {!placeImageBelowDescription && imageComponent} +
    {title}
    +
    +
    + {description} +
    +
    {date}
    +
    + {placeImageBelowDescription && imageComponent} + {actionText && ( + + )} +
    + ); +}; + +const renderSubsequentNotification = (notification, idRefMap) => { + const { id, date, title, description, actionText } = notification; + + const actionFunction = getActionFunctionById(id); + return ( +
    +
    {title}
    +
    +
    + {description} +
    +
    {date}
    +
    + {actionText && ( +
    + {`${actionText} >`} +
    + )} +
    + ); +}; + +export default function WhatsNewPopup({ onClose }) { + const t = useContext(I18nContext); + + const notifications = useSelector(getSortedNotificationsToShow); + const locale = useSelector(getCurrentLocale); + + const [seenNotifications, setSeenNotifications] = useState({}); + + const popoverRef = useRef(); + + const memoizedNotifications = useEqualityCheck(notifications); + const idRefMap = useMemo( + () => + memoizedNotifications.reduce( + (_idRefMap, notification) => ({ + ..._idRefMap, + [notification.id]: React.createRef(), + }), + {}, + ), + [memoizedNotifications], + ); + + useEffect(() => { + const observer = new window.IntersectionObserver( + (entries, _observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const [id, ref] = Object.entries(idRefMap).find(([_, _ref]) => + _ref.current.isSameNode(entry.target), + ); + + setSeenNotifications((_seenNotifications) => ({ + ..._seenNotifications, + [id]: true, + })); + + _observer.unobserve(ref.current); + } + }); + }, + { + root: popoverRef.current, + threshold: 1.0, + }, + ); + + Object.values(idRefMap).forEach((ref) => { + observer.observe(ref.current); + }); + + return () => { + observer.disconnect(); + }; + }, [idRefMap, setSeenNotifications]); + + return ( + { + updateViewedNotifications(seenNotifications); + onClose(); + }} + popoverRef={popoverRef} + > +
    + {notifications.map(({ id }, index) => { + const notification = getTranslatedUINoficiations(t, locale)[id]; + return index === 0 + ? renderFirstNotification(notification, idRefMap) + : renderSubsequentNotification(notification, idRefMap); + })} +
    +
    + ); +} + +WhatsNewPopup.propTypes = { + onClose: PropTypes.func.isRequired, +}; 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 58% 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..e06f8f875 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 @@ -1,14 +1,13 @@ -import assert from 'assert'; 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 () { +describe('AccountMismatchWarning', () => { + beforeAll(() => { sinon.stub(reactRedux, 'useSelector').callsFake((selector) => { if (selector === getSelectedAccount) { return { address: 'mockedAddress' }; @@ -18,17 +17,19 @@ describe('AccountMismatchWarning', function () { ); }); }); - it('renders nothing when the addresses match', function () { + + afterAll(() => { + sinon.restore(); + }); + + it('renders nothing when the addresses match', () => { const wrapper = shallow(); - assert.strictEqual(wrapper.find(InfoIcon).length, 0); + expect(wrapper.find(InfoIcon)).toHaveLength(0); }); - it('renders a warning info icon when addresses do not match', function () { + it('renders a warning info icon when addresses do not match', () => { const wrapper = shallow( , ); - assert.strictEqual(wrapper.find(InfoIcon).length, 1); - }); - after(function () { - sinon.restore(); + expect(wrapper.find(InfoIcon)).toHaveLength(1); }); }); diff --git a/ui/app/components/ui/alert/tests/alert.test.js b/ui/app/components/ui/alert/index.test.js similarity index 56% rename from ui/app/components/ui/alert/tests/alert.test.js rename to ui/app/components/ui/alert/index.test.js index 64c519e0e..aae068be3 100644 --- a/ui/app/components/ui/alert/tests/alert.test.js +++ b/ui/app/components/ui/alert/index.test.js @@ -1,41 +1,40 @@ -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 () { +describe('Alert', () => { let wrapper; - beforeEach(function () { - wrapper = shallow(); + beforeEach(() => { + wrapper = shallow(); }); - it('renders nothing with no visible boolean in state', function () { + it('renders nothing with no visible boolean in state', () => { const alert = wrapper.find('.global-alert'); - assert.strictEqual(alert.length, 0); + expect(alert).toHaveLength(0); }); - it('renders when visible in state is true, and message', function () { + it('renders when visible in state is true, and message', () => { const errorMessage = 'Error Message'; wrapper.setState({ visible: true, msg: errorMessage }); const alert = wrapper.find('.global-alert'); - assert.strictEqual(alert.length, 1); + expect(alert).toHaveLength(1); const errorText = wrapper.find('.msg'); - assert.strictEqual(errorText.text(), errorMessage); + expect(errorText.text()).toStrictEqual(errorMessage); }); - it('calls component method when componentWillReceiveProps is called', function () { + it('calls component method when componentWillReceiveProps is called', () => { const animateInSpy = sinon.stub(wrapper.instance(), 'animateIn'); const animateOutSpy = sinon.stub(wrapper.instance(), 'animateOut'); wrapper.setProps({ visible: true }); - assert(animateInSpy.calledOnce); + expect(animateInSpy.calledOnce).toStrictEqual(true); wrapper.setProps({ visible: false }); - assert(animateOutSpy.calledOnce); + expect(animateOutSpy.calledOnce).toStrictEqual(true); }); }); diff --git a/ui/app/components/ui/box/box.js b/ui/app/components/ui/box/box.js index 006d566ce..bf4756084 100644 --- a/ui/app/components/ui/box/box.js +++ b/ui/app/components/ui/box/box.js @@ -74,8 +74,9 @@ export default function Box({ width, height, children, + className, }) { - const boxClassName = classnames('box', { + const boxClassName = classnames('box', className, { // ---Borders--- // if borderWidth or borderColor is supplied w/o style, default to solid 'box--border-style-solid': @@ -146,4 +147,5 @@ Box.propTypes = { display: PropTypes.oneOf(Object.values(DISPLAY)), width: PropTypes.oneOf(Object.values(BLOCK_SIZES)), height: PropTypes.oneOf(Object.values(BLOCK_SIZES)), + className: PropTypes.string, }; diff --git a/ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js new file mode 100644 index 000000000..ec18a434a --- /dev/null +++ b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Breadcrumbs from './breadcrumbs.component'; + +describe('Breadcrumbs Component', () => { + it('should render with the correct colors', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.breadcrumbs')).toHaveLength(1); + expect(wrapper.find('.breadcrumb')).toHaveLength(3); + expect( + wrapper.find('.breadcrumb').at(0).props().style.backgroundColor, + ).toStrictEqual('#FFFFFF'); + expect( + wrapper.find('.breadcrumb').at(1).props().style.backgroundColor, + ).toStrictEqual('#D8D8D8'); + expect( + wrapper.find('.breadcrumb').at(2).props().style.backgroundColor, + ).toStrictEqual('#FFFFFF'); + }); +}); diff --git a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js deleted file mode 100644 index c2f7af83b..000000000 --- a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import Breadcrumbs from '../breadcrumbs.component'; - -describe('Breadcrumbs Component', function () { - it('should render with the correct colors', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.breadcrumbs').length, 1); - assert.strictEqual(wrapper.find('.breadcrumb').length, 3); - assert.strictEqual( - wrapper.find('.breadcrumb').at(0).props().style.backgroundColor, - '#FFFFFF', - ); - assert.strictEqual( - wrapper.find('.breadcrumb').at(1).props().style.backgroundColor, - '#D8D8D8', - ); - assert.strictEqual( - wrapper.find('.breadcrumb').at(2).props().style.backgroundColor, - '#FFFFFF', - ); - }); -}); 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 53% 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..64aef67b4 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 @@ -1,10 +1,9 @@ -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 () { +describe('ButtonGroup Component', () => { let wrapper; const childButtonSpies = { @@ -19,12 +18,12 @@ describe('ButtonGroup Component', function () { , ]; - before(function () { + beforeAll(() => { sinon.spy(ButtonGroup.prototype, 'handleButtonClick'); sinon.spy(ButtonGroup.prototype, 'renderButtons'); }); - beforeEach(function () { + beforeEach(() => { wrapper = shallow( { childButtonSpies.onClick.resetHistory(); ButtonGroup.prototype.handleButtonClick.resetHistory(); ButtonGroup.prototype.renderButtons.resetHistory(); }); - after(function () { + afterAll(() => { sinon.restore(); }); - describe('componentDidUpdate', function () { - it('should set the activeButtonIndex to the updated newActiveButtonIndex', function () { - assert.strictEqual(wrapper.state('activeButtonIndex'), 1); + describe('componentDidUpdate', () => { + it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => { + expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); wrapper.setProps({ newActiveButtonIndex: 2 }); - assert.strictEqual(wrapper.state('activeButtonIndex'), 2); + expect(wrapper.state('activeButtonIndex')).toStrictEqual(2); }); - it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', function () { - assert.strictEqual(wrapper.state('activeButtonIndex'), 1); + it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => { + expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); wrapper.setProps({ newActiveButtonIndex: null }); - assert.strictEqual(wrapper.state('activeButtonIndex'), 1); + expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); }); }); - describe('handleButtonClick', function () { - it('should set the activeButtonIndex', function () { - assert.strictEqual(wrapper.state('activeButtonIndex'), 1); + describe('handleButtonClick', () => { + it('should set the activeButtonIndex', () => { + expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); wrapper.instance().handleButtonClick(2); - assert.strictEqual(wrapper.state('activeButtonIndex'), 2); + expect(wrapper.state('activeButtonIndex')).toStrictEqual(2); }); }); - describe('renderButtons', function () { - it('should render a button for each child', function () { + describe('renderButtons', () => { + it('should render a button for each child', () => { const childButtons = wrapper.find('.button-group__button'); - assert.strictEqual(childButtons.length, 3); + expect(childButtons).toHaveLength(3); }); - it('should render the correct button with an active state', function () { + it('should render the correct button with an active state', () => { const childButtons = wrapper.find('.button-group__button'); const activeChildButton = wrapper.find('.button-group__button--active'); - assert.deepStrictEqual(childButtons.get(1), activeChildButton.get(0)); + expect(childButtons.get(1)).toStrictEqual(activeChildButton.get(0)); }); - it("should call handleButtonClick and the respective button's onClick method when a button is clicked", function () { - assert.strictEqual(ButtonGroup.prototype.handleButtonClick.callCount, 0); - assert.strictEqual(childButtonSpies.onClick.callCount, 0); + it("should call handleButtonClick and the respective button's onClick method when a button is clicked", () => { + expect(ButtonGroup.prototype.handleButtonClick.callCount).toStrictEqual( + 0, + ); + expect(childButtonSpies.onClick.callCount).toStrictEqual(0); const childButtons = wrapper.find('.button-group__button'); childButtons.at(0).props().onClick(); childButtons.at(1).props().onClick(); childButtons.at(2).props().onClick(); - assert.strictEqual(ButtonGroup.prototype.handleButtonClick.callCount, 3); - assert.strictEqual(childButtonSpies.onClick.callCount, 3); + expect(ButtonGroup.prototype.handleButtonClick.callCount).toStrictEqual( + 3, + ); + expect(childButtonSpies.onClick.callCount).toStrictEqual(3); }); - it('should render all child buttons as disabled if props.disabled is true', function () { + it('should render all child buttons as disabled if props.disabled is true', () => { const childButtons = wrapper.find('.button-group__button'); childButtons.forEach((button) => { - assert.strictEqual(button.props().disabled, undefined); + expect(button.props().disabled).toBeUndefined(); }); wrapper.setProps({ disabled: true }); const disabledChildButtons = wrapper.find('[disabled=true]'); - assert.strictEqual(disabledChildButtons.length, 3); + expect(disabledChildButtons).toHaveLength(3); }); - it('should render the children of the button', function () { + it('should render the children of the button', () => { const mockClass = wrapper.find('.mockClass'); - assert.strictEqual(mockClass.length, 1); + expect(mockClass).toHaveLength(1); }); }); - describe('render', function () { - it('should render a div with the expected class and style', function () { - assert.strictEqual( - wrapper.find('div').at(0).props().className, + describe('render', () => { + it('should render a div with the expected class and style', () => { + expect(wrapper.find('div').at(0).props().className).toStrictEqual( 'someClassName', ); - assert.deepStrictEqual(wrapper.find('div').at(0).props().style, { + expect(wrapper.find('div').at(0).props().style).toStrictEqual({ color: 'red', }); }); - it('should call renderButtons when rendering', function () { - assert.strictEqual(ButtonGroup.prototype.renderButtons.callCount, 1); + it('should call renderButtons when rendering', () => { + expect(ButtonGroup.prototype.renderButtons.callCount).toStrictEqual(1); wrapper.instance().render(); - assert.strictEqual(ButtonGroup.prototype.renderButtons.callCount, 2); + expect(ButtonGroup.prototype.renderButtons.callCount).toStrictEqual(2); }); }); }); diff --git a/ui/app/components/ui/button/button.component.js b/ui/app/components/ui/button/button.component.js index 4313a301a..c994bb311 100644 --- a/ui/app/components/ui/button/button.component.js +++ b/ui/app/components/ui/button/button.component.js @@ -12,16 +12,16 @@ const CLASSNAME_ROUNDED = 'btn--rounded'; const CLASSNAME_FIRST_TIME = 'btn--first-time'; const typeHash = { - default: CLASSNAME_DEFAULT, - primary: CLASSNAME_PRIMARY, - secondary: CLASSNAME_SECONDARY, - warning: 'btn-warning', - danger: 'btn-danger', + 'default': CLASSNAME_DEFAULT, + 'primary': CLASSNAME_PRIMARY, + 'secondary': CLASSNAME_SECONDARY, + 'warning': 'btn-warning', + 'danger': 'btn-danger', 'danger-primary': 'btn-danger-primary', - link: 'btn-link', + 'link': 'btn-link', // TODO: Legacy button type to be deprecated - confirm: CLASSNAME_CONFIRM, - raised: CLASSNAME_RAISED, + 'confirm': CLASSNAME_CONFIRM, + 'raised': CLASSNAME_RAISED, 'first-time': CLASSNAME_FIRST_TIME, }; diff --git a/ui/app/components/ui/card/card.component.test.js b/ui/app/components/ui/card/card.component.test.js new file mode 100644 index 000000000..83205ec9c --- /dev/null +++ b/ui/app/components/ui/card/card.component.test.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Card from './card.component'; + +describe('Card Component', () => { + it('should render a card with a title and child element', () => { + const wrapper = shallow( + +
    Child
    +
    , + ); + + expect(wrapper.hasClass('card-test-class')).toStrictEqual(true); + const title = wrapper.find('.card__title'); + expect(title).toHaveLength(1); + expect(title.text()).toStrictEqual('Test'); + const child = wrapper.find('.child-test-class'); + expect(child).toHaveLength(1); + expect(child.text()).toStrictEqual('Child'); + }); +}); diff --git a/ui/app/components/ui/card/tests/card.component.test.js b/ui/app/components/ui/card/tests/card.component.test.js deleted file mode 100644 index 2f1d4b43c..000000000 --- a/ui/app/components/ui/card/tests/card.component.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import Card from '../card.component'; - -describe('Card Component', function () { - it('should render a card with a title and child element', function () { - const wrapper = shallow( - -
    Child
    -
    , - ); - - assert.ok(wrapper.hasClass('card-test-class')); - const title = wrapper.find('.card__title'); - assert.ok(title); - assert.strictEqual(title.text(), 'Test'); - const child = wrapper.find('.child-test-class'); - assert.ok(child); - assert.strictEqual(child.text(), 'Child'); - }); -}); diff --git a/ui/app/components/ui/confusable/confusable.component.test.js b/ui/app/components/ui/confusable/confusable.component.test.js new file mode 100644 index 000000000..cb38cae8e --- /dev/null +++ b/ui/app/components/ui/confusable/confusable.component.test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Confusable from './confusable.component'; + +describe('Confusable component', () => { + it('should detect zero-width unicode', () => { + const wrapper = shallow(); + expect(wrapper.find('.confusable__point')).toHaveLength(1); + }); + + it('should detect homoglyphic unicode points', () => { + const wrapper = shallow(); + expect(wrapper.find('.confusable__point')).toHaveLength(1); + }); + + it('should detect multiple homoglyphic unicode points', () => { + const wrapper = shallow(); + expect(wrapper.find('.confusable__point')).toHaveLength(5); + }); + + it('should not detect emoji', () => { + const wrapper = shallow(); + expect(wrapper.find('.confusable__point')).toHaveLength(0); + }); +}); diff --git a/ui/app/components/ui/confusable/test/confusable.component.test.js b/ui/app/components/ui/confusable/test/confusable.component.test.js deleted file mode 100644 index d3166ccd7..000000000 --- a/ui/app/components/ui/confusable/test/confusable.component.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import Confusable from '../confusable.component'; - -describe('Confusable component', function () { - it('should detect zero-width unicode', function () { - const wrapper = shallow(); - assert.ok(wrapper.find('.confusable__point').length === 1); - }); - - it('should detect homoglyphic unicode points', function () { - const wrapper = shallow(); - assert.ok(wrapper.find('.confusable__point').length === 1); - }); - - it('should detect multiple homoglyphic unicode points', function () { - const wrapper = shallow(); - assert.ok(wrapper.find('.confusable__point').length === 5); - }); - - it('should not detect emoji', function () { - const wrapper = shallow(); - assert.ok(wrapper.find('.confusable__point').length === 0); - }); -}); diff --git a/ui/app/components/ui/currency-display/currency-display.component.js b/ui/app/components/ui/currency-display/currency-display.component.js index 6310960af..69f884c1c 100644 --- a/ui/app/components/ui/currency-display/currency-display.component.js +++ b/ui/app/components/ui/currency-display/currency-display.component.js @@ -50,17 +50,17 @@ export default function CurrencyDisplay({ } CurrencyDisplay.propTypes = { - className: PropTypes.string, - currency: PropTypes.string, + 'className': PropTypes.string, + 'currency': PropTypes.string, 'data-testid': PropTypes.string, - denomination: PropTypes.oneOf([GWEI]), - displayValue: PropTypes.string, - hideLabel: PropTypes.bool, - hideTitle: PropTypes.bool, - numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - prefix: PropTypes.string, - prefixComponent: PropTypes.node, - style: PropTypes.object, - suffix: PropTypes.string, - value: PropTypes.string, + 'denomination': PropTypes.oneOf([GWEI]), + 'displayValue': PropTypes.string, + 'hideLabel': PropTypes.bool, + 'hideTitle': PropTypes.bool, + 'numberOfDecimals': PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + 'prefix': PropTypes.string, + 'prefixComponent': PropTypes.node, + 'style': PropTypes.object, + 'suffix': PropTypes.string, + 'value': PropTypes.string, }; 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 57% 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..367cf5496 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 @@ -1,12 +1,11 @@ -import assert from 'assert'; 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 () { +describe('CurrencyDisplay Component', () => { + beforeEach(() => { const stub = sinon.stub(reactRedux, 'useSelector'); stub.callsFake(() => ({ currentCurrency: 'usd', @@ -14,7 +13,10 @@ describe('CurrencyDisplay Component', function () { conversionRate: 280.45, })); }); - it('should render text with a className', function () { + afterEach(() => { + sinon.restore(); + }); + it('should render text with a className', () => { const wrapper = shallow( , ); - assert.ok(wrapper.hasClass('currency-display')); - assert.strictEqual(wrapper.text(), '$123.45'); + expect(wrapper.hasClass('currency-display')).toStrictEqual(true); + expect(wrapper.text()).toStrictEqual('$123.45'); }); - it('should render text with a prefix', function () { + it('should render text with a prefix', () => { const wrapper = shallow( , ); - assert.ok(wrapper.hasClass('currency-display')); - assert.strictEqual(wrapper.text(), '-$123.45'); - }); - afterEach(function () { - sinon.restore(); + expect(wrapper.hasClass('currency-display')).toStrictEqual(true); + expect(wrapper.text()).toStrictEqual('-$123.45'); }); }); 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 56% 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..7092b73dd 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 @@ -1,24 +1,23 @@ -import assert from 'assert'; import React from 'react'; import PropTypes from 'prop-types'; 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 () { - it('should render properly without a suffix', function () { +describe('CurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { const wrapper = shallow(); - assert.ok(wrapper); - assert.strictEqual(wrapper.find(UnitInput).length, 1); + expect(wrapper).toHaveLength(1); + expect(wrapper.find(UnitInput)).toHaveLength(1); }); - it('should render properly with a suffix', function () { + it('should render properly with a suffix', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -38,13 +37,13 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ETH'); - assert.strictEqual(wrapper.find(CurrencyDisplay).length, 1); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ETH'); + expect(wrapper.find(CurrencyDisplay)).toHaveLength(1); }); - it('should render properly with an ETH value', function () { + it('should render properly with an ETH value', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -67,26 +66,24 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 1); - assert.strictEqual( - currencyInputInstance.state.hexValue, + expect(currencyInputInstance.state.decimalValue).toStrictEqual(1); + expect(currencyInputInstance.state.hexValue).toStrictEqual( 'de0b6b3a7640000', ); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ETH'); - assert.strictEqual(wrapper.find('.unit-input__input').props().value, 1); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ETH'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual(1); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$231.06USD', ); }); - it('should render properly with a fiat value', function () { + it('should render properly with a fiat value', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -110,23 +107,24 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 1); - assert.strictEqual(currencyInputInstance.state.hexValue, 'f602f2234d0ea'); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'USD'); - assert.strictEqual(wrapper.find('.unit-input__input').props().value, 1); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(currencyInputInstance.state.decimalValue).toStrictEqual(1); + expect(currencyInputInstance.state.hexValue).toStrictEqual( + 'f602f2234d0ea', + ); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('USD'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual(1); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '0.004328ETH', ); }); - it('should render properly with a native value when hideFiat is true', function () { + it('should render properly with a native value when hideFiat is true', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -134,6 +132,7 @@ describe('CurrencyInput Component', function () { conversionRate: 231.06, }, }; + const store = configureMockStore()(mockStore); const wrapper = mount( @@ -155,36 +154,36 @@ describe('CurrencyInput Component', function () { }, ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 0.004328); - assert.strictEqual(currencyInputInstance.state.hexValue, 'f602f2234d0ea'); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ETH'); - assert.strictEqual( - wrapper.find('.unit-input__input').props().value, + expect(currencyInputInstance.state.decimalValue).toStrictEqual(0.004328); + expect(currencyInputInstance.state.hexValue).toStrictEqual( + 'f602f2234d0ea', + ); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ETH'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual( 0.004328, ); - assert.strictEqual( + expect( wrapper.find('.currency-input__conversion-component').text(), - 'noConversionRateAvailable_t', - ); + ).toStrictEqual('noConversionRateAvailable_t'); }); }); - describe('handling actions', function () { + describe('handling actions', () => { const handleChangeSpy = sinon.spy(); const handleBlurSpy = sinon.spy(); - afterEach(function () { + afterEach(() => { handleChangeSpy.resetHistory(); handleBlurSpy.resetHistory(); }); - it('should call onChange on input changes with the hex value for ETH', function () { + it('should call onChange on input changes with the hex value for ETH', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -205,38 +204,35 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - assert.strictEqual(handleBlurSpy.callCount, 0); + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + expect(handleBlurSpy.callCount).toStrictEqual(0); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 0); - assert.strictEqual(currencyInputInstance.state.hexValue, undefined); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(currencyInputInstance.state.decimalValue).toStrictEqual(0); + expect(currencyInputInstance.state.hexValue).toBeUndefined(); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$0.00USD', ); const input = wrapper.find('input'); - assert.strictEqual(input.props().value, 0); + expect(input.props().value).toStrictEqual(0); input.simulate('change', { target: { value: 1 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith('de0b6b3a7640000')).toStrictEqual(true); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$231.06USD', ); - assert.strictEqual(currencyInputInstance.state.decimalValue, 1); - assert.strictEqual( - currencyInputInstance.state.hexValue, + expect(currencyInputInstance.state.decimalValue).toStrictEqual(1); + expect(currencyInputInstance.state.hexValue).toStrictEqual( 'de0b6b3a7640000', ); }); - it('should call onChange on input changes with the hex value for fiat', function () { + it('should call onChange on input changes with the hex value for fiat', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -258,35 +254,35 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - assert.strictEqual(handleBlurSpy.callCount, 0); + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + expect(handleBlurSpy.callCount).toStrictEqual(0); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 0); - assert.strictEqual(currencyInputInstance.state.hexValue, undefined); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(currencyInputInstance.state.decimalValue).toStrictEqual(0); + expect(currencyInputInstance.state.hexValue).toBeUndefined(); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '0ETH', ); const input = wrapper.find('input'); - assert.strictEqual(input.props().value, 0); + expect(input.props().value).toStrictEqual(0); input.simulate('change', { target: { value: 1 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith('f602f2234d0ea')); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith('f602f2234d0ea')).toStrictEqual(true); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '0.004328ETH', ); - assert.strictEqual(currencyInputInstance.state.decimalValue, 1); - assert.strictEqual(currencyInputInstance.state.hexValue, 'f602f2234d0ea'); + expect(currencyInputInstance.state.decimalValue).toStrictEqual(1); + expect(currencyInputInstance.state.hexValue).toStrictEqual( + 'f602f2234d0ea', + ); }); - it('should change the state and pass in a new decimalValue when props.value changes', function () { + it('should change the state and pass in a new decimalValue when props.value changes', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -308,29 +304,26 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const currencyInputInstance = wrapper.find(CurrencyInput).dive(); - assert.strictEqual(currencyInputInstance.state('decimalValue'), 0); - assert.strictEqual(currencyInputInstance.state('hexValue'), undefined); - assert.strictEqual( - currencyInputInstance.find(UnitInput).props().value, + expect(currencyInputInstance.state('decimalValue')).toStrictEqual(0); + expect(currencyInputInstance.state('hexValue')).toBeUndefined(); + expect(currencyInputInstance.find(UnitInput).props().value).toStrictEqual( 0, ); currencyInputInstance.setProps({ value: '1ec05e43e72400' }); currencyInputInstance.update(); - assert.strictEqual(currencyInputInstance.state('decimalValue'), 2); - assert.strictEqual( - currencyInputInstance.state('hexValue'), + expect(currencyInputInstance.state('decimalValue')).toStrictEqual(2); + expect(currencyInputInstance.state('hexValue')).toStrictEqual( '1ec05e43e72400', ); - assert.strictEqual( - currencyInputInstance.find(UnitInput).props().value, + expect(currencyInputInstance.find(UnitInput).props().value).toStrictEqual( 2, ); }); - it('should swap selected currency when swap icon is clicked', function () { + it('should swap selected currency when swap icon is clicked', () => { const mockStore = { metamask: { nativeCurrency: 'ETH', @@ -352,40 +345,36 @@ describe('CurrencyInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - assert.strictEqual(handleBlurSpy.callCount, 0); + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + expect(handleBlurSpy.callCount).toStrictEqual(0); const currencyInputInstance = wrapper .find(CurrencyInput) .at(0) .instance(); - assert.strictEqual(currencyInputInstance.state.decimalValue, 0); - assert.strictEqual(currencyInputInstance.state.hexValue, undefined); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(currencyInputInstance.state.decimalValue).toStrictEqual(0); + expect(currencyInputInstance.state.hexValue).toBeUndefined(); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$0.00USD', ); const input = wrapper.find('input'); - assert.strictEqual(input.props().value, 0); + expect(input.props().value).toStrictEqual(0); input.simulate('change', { target: { value: 1 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith('de0b6b3a7640000')).toStrictEqual(true); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$231.06USD', ); - assert.strictEqual(currencyInputInstance.state.decimalValue, 1); - assert.strictEqual( - currencyInputInstance.state.hexValue, + expect(currencyInputInstance.state.decimalValue).toStrictEqual(1); + expect(currencyInputInstance.state.hexValue).toStrictEqual( 'de0b6b3a7640000', ); const swap = wrapper.find('.currency-input__swap-component'); swap.simulate('click'); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '0.004328ETH', ); }); 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 86% 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..e4a153bb1 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 @@ -1,20 +1,18 @@ -import assert from 'assert'; -import proxyquire from 'proxyquire'; - +// eslint-disable-next-line import/unambiguous let mapStateToProps, mergeProps; -proxyquire('../currency-input.container.js', { - 'react-redux': { - connect: (ms, _, mp) => { - mapStateToProps = ms; - mergeProps = mp; - return () => ({}); - }, +jest.mock('react-redux', () => ({ + connect: (ms, _, mp) => { + mapStateToProps = ms; + mergeProps = mp; + return () => ({}); }, -}); +})); + +require('./currency-input.container.js'); -describe('CurrencyInput container', function () { - describe('mapStateToProps()', function () { +describe('CurrencyInput container', () => { + describe('mapStateToProps()', () => { const tests = [ // Test # 1 { @@ -115,13 +113,13 @@ describe('CurrencyInput container', function () { ]; tests.forEach(({ mockState, expected, comment }) => { - it(comment, function () { - return assert.deepStrictEqual(mapStateToProps(mockState), expected); + it(`${comment}`, () => { + expect(mapStateToProps(mockState)).toStrictEqual(expected); }); }); }); - describe('mergeProps()', function () { + describe('mergeProps()', () => { const tests = [ // Test # 1 { @@ -173,9 +171,8 @@ describe('CurrencyInput container', function () { expected, comment, }) => { - it(comment, function () { - assert.deepStrictEqual( - mergeProps(stateProps, dispatchProps, ownProps), + it(`${comment}`, () => { + expect(mergeProps(stateProps, dispatchProps, ownProps)).toStrictEqual( expected, ); }); diff --git a/ui/app/components/ui/definition-list/definition-list.stories.js b/ui/app/components/ui/definition-list/definition-list.stories.js index 5a5c3042c..9760d159e 100644 --- a/ui/app/components/ui/definition-list/definition-list.stories.js +++ b/ui/app/components/ui/definition-list/definition-list.stories.js @@ -24,13 +24,13 @@ const basic = { const advanced = { 'Network Name': 'Ethereum Mainnet', 'Chain ID': '1', - Ticker: 'ETH', + 'Ticker': 'ETH', }; const tooltips = { 'Network Name': 'The name that is associated with this network', 'Chain ID': 'The numeric value representing the ID of this network', - Ticker: 'The currency symbol of the primary currency for this network', + 'Ticker': 'The currency symbol of the primary currency for this network', }; export const definitionList = () => ( diff --git a/ui/app/components/ui/error-message/error-message.component.test.js b/ui/app/components/ui/error-message/error-message.component.test.js new file mode 100644 index 000000000..5e1f0669a --- /dev/null +++ b/ui/app/components/ui/error-message/error-message.component.test.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ErrorMessage from './error-message.component'; + +describe('ErrorMessage Component', () => { + const t = (key) => `translate ${key}`; + + it('should render a message from props.errorMessage', () => { + const wrapper = shallow(, { + context: { t }, + }); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.error-message')).toHaveLength(1); + expect(wrapper.find('.error-message__icon')).toHaveLength(1); + expect(wrapper.find('.error-message__text').text()).toStrictEqual( + 'ALERT: This is an error.', + ); + }); + + it('should render a message translated from props.errorKey', () => { + const wrapper = shallow(, { + context: { t }, + }); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.error-message')).toHaveLength(1); + expect(wrapper.find('.error-message__icon')).toHaveLength(1); + expect(wrapper.find('.error-message__text').text()).toStrictEqual( + 'ALERT: translate testKey', + ); + }); +}); diff --git a/ui/app/components/ui/error-message/tests/error-message.component.test.js b/ui/app/components/ui/error-message/tests/error-message.component.test.js deleted file mode 100644 index a6c2d4ac3..000000000 --- a/ui/app/components/ui/error-message/tests/error-message.component.test.js +++ /dev/null @@ -1,36 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import ErrorMessage from '../error-message.component'; - -describe('ErrorMessage Component', function () { - const t = (key) => `translate ${key}`; - - it('should render a message from props.errorMessage', function () { - const wrapper = shallow(, { - context: { t }, - }); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.error-message').length, 1); - assert.strictEqual(wrapper.find('.error-message__icon').length, 1); - assert.strictEqual( - wrapper.find('.error-message__text').text(), - 'ALERT: This is an error.', - ); - }); - - it('should render a message translated from props.errorKey', function () { - const wrapper = shallow(, { - context: { t }, - }); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.error-message').length, 1); - assert.strictEqual(wrapper.find('.error-message__icon').length, 1); - assert.strictEqual( - wrapper.find('.error-message__text').text(), - 'ALERT: translate testKey', - ); - }); -}); 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 52% 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..0ec752b16 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,24 +1,23 @@ -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 () { +describe('HexToDecimal Component', () => { + it('should render a prefixed hex as a decimal with a className', () => { const wrapper = shallow( , ); - assert.ok(wrapper.hasClass('hex-to-decimal')); - assert.strictEqual(wrapper.text(), '12345'); + expect(wrapper.hasClass('hex-to-decimal')).toStrictEqual(true); + expect(wrapper.text()).toStrictEqual('12345'); }); - it('should render an unprefixed hex as a decimal with a className', function () { + it('should render an unprefixed hex as a decimal with a className', () => { const wrapper = shallow( , ); - assert.ok(wrapper.hasClass('hex-to-decimal')); - assert.strictEqual(wrapper.text(), '6789'); + expect(wrapper.hasClass('hex-to-decimal')).toStrictEqual(true); + expect(wrapper.text()).toStrictEqual('6789'); }); }); diff --git a/ui/app/components/ui/icon-button/icon-button.js b/ui/app/components/ui/icon-button/icon-button.js index dacd1ef25..2f43f8334 100644 --- a/ui/app/components/ui/icon-button/icon-button.js +++ b/ui/app/components/ui/icon-button/icon-button.js @@ -36,11 +36,11 @@ export default function IconButton({ } IconButton.propTypes = { - onClick: PropTypes.func.isRequired, - Icon: PropTypes.func.isRequired, - disabled: PropTypes.bool, - label: PropTypes.string.isRequired, - tooltipRender: PropTypes.func, - className: PropTypes.string, + 'onClick': PropTypes.func.isRequired, + 'Icon': PropTypes.func.isRequired, + 'disabled': PropTypes.bool, + 'label': PropTypes.string.isRequired, + 'tooltipRender': PropTypes.func, + 'className': PropTypes.string, 'data-testid': PropTypes.string, }; 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 54% 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..f78a47b6d 100644 --- a/ui/app/components/ui/identicon/tests/identicon.component.test.js +++ b/ui/app/components/ui/identicon/identicon.component.test.js @@ -1,11 +1,10 @@ -import assert from 'assert'; 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 () { +describe('Identicon', () => { const state = { metamask: { useBlockie: false, @@ -16,34 +15,33 @@ describe('Identicon', function () { const mockStore = configureMockStore(middlewares); const store = mockStore(state); - it('renders empty identicon with no props', function () { + it('renders empty identicon with no props', () => { const wrapper = mount(); - assert.ok(wrapper.find('div'), 'Empty identicon found'); + expect(wrapper.find('div').prop('className')).toStrictEqual( + 'identicon__image-border', + ); }); - it('renders custom image and add className props', function () { + it('renders custom image and add className props', () => { const wrapper = mount( , ); - assert.strictEqual( - wrapper.find('img.test-image').prop('className'), + expect(wrapper.find('img.test-image').prop('className')).toStrictEqual( 'identicon test-image', ); - assert.strictEqual( - wrapper.find('img.test-image').prop('src'), + expect(wrapper.find('img.test-image').prop('src')).toStrictEqual( 'test-image', ); }); - it('renders div with address prop', function () { + it('renders div with address prop', () => { const wrapper = mount( - , + , ); - assert.strictEqual( - wrapper.find('div.test-address').prop('className'), + expect(wrapper.find('div.test-address').prop('className')).toStrictEqual( 'identicon test-address', ); }); diff --git a/ui/app/components/ui/list-item/list-item.component.js b/ui/app/components/ui/list-item/list-item.component.js index 6cb919aec..e6b3da462 100644 --- a/ui/app/components/ui/list-item/list-item.component.js +++ b/ui/app/components/ui/list-item/list-item.component.js @@ -51,14 +51,14 @@ export default function ListItem({ } ListItem.propTypes = { - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - titleIcon: PropTypes.node, - subtitle: PropTypes.node, - children: PropTypes.node, - icon: PropTypes.node, - rightContent: PropTypes.node, - midContent: PropTypes.node, - className: PropTypes.string, - onClick: PropTypes.func, + 'title': PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + 'titleIcon': PropTypes.node, + 'subtitle': PropTypes.node, + 'children': PropTypes.node, + 'icon': PropTypes.node, + 'rightContent': PropTypes.node, + 'midContent': PropTypes.node, + 'className': PropTypes.string, + 'onClick': PropTypes.func, 'data-testid': PropTypes.string, }; diff --git a/ui/app/components/ui/list-item/list-item.component.test.js b/ui/app/components/ui/list-item/list-item.component.test.js new file mode 100644 index 000000000..9aee68af1 --- /dev/null +++ b/ui/app/components/ui/list-item/list-item.component.test.js @@ -0,0 +1,80 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; +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

    ; +const CLASSNAME = 'list-item-test'; +const RIGHT_CONTENT =

    Content rendered to the right

    ; +const CHILDREN = ; +const MID_CONTENT =

    Content rendered in the middle

    ; + +describe('ListItem', () => { + let wrapper; + let clickHandler; + beforeAll(() => { + clickHandler = sinon.fake(); + wrapper = shallow( + } + titleIcon={} + onClick={clickHandler} + > + {CHILDREN} + , + ); + }); + + afterAll(() => { + sinon.restore(); + }); + + it('includes the data-testid', () => { + expect(wrapper.props()['data-testid']).toStrictEqual('test-id'); + }); + it(`renders "${TITLE}" title`, () => { + expect(wrapper.find('.list-item__heading h2').text()).toStrictEqual(TITLE); + }); + it(`renders "I am a list item" subtitle`, () => { + expect(wrapper.find('.list-item__subheading').text()).toStrictEqual( + 'I am a list item', + ); + }); + it('attaches external className', () => { + expect(wrapper.props().className).toContain(CLASSNAME); + }); + it('renders content on the right side of the list item', () => { + expect(wrapper.find('.list-item__right-content p').text()).toStrictEqual( + 'Content rendered to the right', + ); + }); + it('renders content in the middle of the list item', () => { + expect(wrapper.find('.list-item__mid-content p').text()).toStrictEqual( + 'Content rendered in the middle', + ); + }); + it('renders list item actions', () => { + expect(wrapper.find('.list-item__actions button').text()).toStrictEqual( + 'I am a button', + ); + }); + it('renders the title icon', () => { + expect(wrapper.find(Preloader)).toHaveLength(1); + }); + it('renders the list item icon', () => { + expect(wrapper.find(Send)).toHaveLength(1); + }); + it('handles click action and fires onClick', () => { + wrapper.simulate('click'); + expect(clickHandler.callCount).toStrictEqual(1); + }); +}); diff --git a/ui/app/components/ui/list-item/tests/list-item.test.js b/ui/app/components/ui/list-item/tests/list-item.test.js deleted file mode 100644 index 488ee9039..000000000 --- a/ui/app/components/ui/list-item/tests/list-item.test.js +++ /dev/null @@ -1,84 +0,0 @@ -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'; - -const TITLE = 'Hello World'; -const SUBTITLE =

    I am a list item

    ; -const CLASSNAME = 'list-item-test'; -const RIGHT_CONTENT =

    Content rendered to the right

    ; -const CHILDREN = ; -const MID_CONTENT =

    Content rendered in the middle

    ; - -describe('ListItem', function () { - let wrapper; - let clickHandler; - before(function () { - clickHandler = Sinon.fake(); - wrapper = shallow( - } - titleIcon={} - onClick={clickHandler} - > - {CHILDREN} - , - ); - }); - it('includes the data-testid', function () { - assert.strictEqual(wrapper.props()['data-testid'], 'test-id'); - }); - it(`renders "${TITLE}" title`, function () { - assert.strictEqual(wrapper.find('.list-item__heading h2').text(), TITLE); - }); - it(`renders "I am a list item" subtitle`, function () { - assert.strictEqual( - wrapper.find('.list-item__subheading').text(), - 'I am a list item', - ); - }); - it('attaches external className', function () { - assert(wrapper.props().className.includes(CLASSNAME)); - }); - it('renders content on the right side of the list item', function () { - assert.strictEqual( - wrapper.find('.list-item__right-content p').text(), - 'Content rendered to the right', - ); - }); - it('renders content in the middle of the list item', function () { - assert.strictEqual( - wrapper.find('.list-item__mid-content p').text(), - 'Content rendered in the middle', - ); - }); - it('renders list item actions', function () { - assert.strictEqual( - wrapper.find('.list-item__actions button').text(), - 'I am a button', - ); - }); - it('renders the title icon', function () { - assert(wrapper.find(Preloader)); - }); - it('renders the list item icon', function () { - assert(wrapper.find(Send)); - }); - it('handles click action and fires onClick', function () { - wrapper.simulate('click'); - assert.strictEqual(clickHandler.callCount, 1); - }); - - after(function () { - Sinon.restore(); - }); -}); diff --git a/ui/app/components/ui/menu/menu-item.js b/ui/app/components/ui/menu/menu-item.js index f62c76ba9..f35449b84 100644 --- a/ui/app/components/ui/menu/menu-item.js +++ b/ui/app/components/ui/menu/menu-item.js @@ -24,20 +24,20 @@ const MenuItem = ({ ); MenuItem.propTypes = { - children: PropTypes.node.isRequired, - className: PropTypes.string, + 'children': PropTypes.node.isRequired, + 'className': PropTypes.string, 'data-testid': PropTypes.string, - iconClassName: PropTypes.string, - onClick: PropTypes.func, - subtitle: PropTypes.node, + 'iconClassName': PropTypes.string, + 'onClick': PropTypes.func, + 'subtitle': PropTypes.node, }; MenuItem.defaultProps = { - className: undefined, + 'className': undefined, 'data-testid': undefined, - iconClassName: undefined, - onClick: undefined, - subtitle: undefined, + 'iconClassName': undefined, + 'onClick': undefined, + 'subtitle': undefined, }; export default MenuItem; 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 59% 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..819281291 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,32 +1,27 @@ -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 () { +describe('MetaFoxLogo', () => { + it('sets icon height and width to 42 by default', () => { const wrapper = mount(); - assert.strictEqual( + expect( wrapper.find('img.app-header__metafox-logo--icon').prop('width'), - 42, - ); - assert.strictEqual( + ).toStrictEqual(42); + expect( wrapper.find('img.app-header__metafox-logo--icon').prop('height'), - 42, - ); + ).toStrictEqual(42); }); - it('does not set icon height and width when unsetIconHeight is true', function () { + it('does not set icon height and width when unsetIconHeight is true', () => { const wrapper = mount(); - assert.strictEqual( + expect( wrapper.find('img.app-header__metafox-logo--icon').prop('width'), - undefined, - ); - assert.strictEqual( + ).toBeUndefined(); + expect( wrapper.find('img.app-header__metafox-logo--icon').prop('height'), - undefined, - ); + ).toBeUndefined(); }); }); diff --git a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js new file mode 100644 index 000000000..c24fcd372 --- /dev/null +++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.test.js @@ -0,0 +1,87 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Button from '../../button'; +import PageFooter from './page-container-footer.component'; + +describe('Page Footer', () => { + let wrapper; + const onCancel = sinon.spy(); + const onSubmit = sinon.spy(); + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('renders page container footer', () => { + expect(wrapper.find('.page-container__footer')).toHaveLength(1); + }); + + it('should render a secondary footer inside page-container__footer when given children', () => { + wrapper = shallow( + +
    Works
    +
    , + { context: { t: sinon.spy((k) => `[${k}]`) } }, + ); + + expect(wrapper.find('.page-container__footer-secondary')).toHaveLength(1); + }); + + it('renders two button components', () => { + expect(wrapper.find(Button)).toHaveLength(2); + }); + + describe('Cancel Button', () => { + it('has button type of default', () => { + expect( + wrapper.find('.page-container__footer-button').first().prop('type'), + ).toStrictEqual('default'); + }); + + it('has children text of Cancel', () => { + expect( + wrapper.find('.page-container__footer-button').first().prop('children'), + ).toStrictEqual('Cancel'); + }); + + it('should call cancel when click is simulated', () => { + wrapper.find('.page-container__footer-button').first().prop('onClick')(); + expect(onCancel.callCount).toStrictEqual(1); + }); + }); + + describe('Submit Button', () => { + it('assigns button type based on props', () => { + expect( + wrapper.find('.page-container__footer-button').last().prop('type'), + ).toStrictEqual('Test Type'); + }); + + it('has disabled prop', () => { + expect( + wrapper.find('.page-container__footer-button').last().prop('disabled'), + ).toStrictEqual(false); + }); + + it('has children text when submitText prop exists', () => { + expect( + wrapper.find('.page-container__footer-button').last().prop('children'), + ).toStrictEqual('Submit'); + }); + + it('should call submit when click is simulated', () => { + wrapper.find('.page-container__footer-button').last().prop('onClick')(); + expect(onSubmit.callCount).toStrictEqual(1); + }); + }); +}); 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/tests/page-container-footer.component.test.js deleted file mode 100644 index 56c798f65..000000000 --- a/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js +++ /dev/null @@ -1,96 +0,0 @@ -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'; - -describe('Page Footer', function () { - let wrapper; - const onCancel = sinon.spy(); - const onSubmit = sinon.spy(); - - beforeEach(function () { - wrapper = shallow( - , - ); - }); - - it('renders page container footer', function () { - assert.strictEqual(wrapper.find('.page-container__footer').length, 1); - }); - - it('should render a secondary footer inside page-container__footer when given children', function () { - wrapper = shallow( - -
    Works
    -
    , - { context: { t: sinon.spy((k) => `[${k}]`) } }, - ); - - assert.strictEqual( - wrapper.find('.page-container__footer-secondary').length, - 1, - ); - }); - - it('renders two button components', function () { - assert.strictEqual(wrapper.find(Button).length, 2); - }); - - describe('Cancel Button', function () { - it('has button type of default', function () { - assert.strictEqual( - wrapper.find('.page-container__footer-button').first().prop('type'), - 'default', - ); - }); - - it('has children text of Cancel', function () { - assert.strictEqual( - wrapper.find('.page-container__footer-button').first().prop('children'), - 'Cancel', - ); - }); - - it('should call cancel when click is simulated', function () { - wrapper.find('.page-container__footer-button').first().prop('onClick')(); - assert.strictEqual(onCancel.callCount, 1); - }); - }); - - describe('Submit Button', function () { - it('assigns button type based on props', function () { - assert.strictEqual( - wrapper.find('.page-container__footer-button').last().prop('type'), - 'Test Type', - ); - }); - - it('has disabled prop', function () { - assert.strictEqual( - wrapper.find('.page-container__footer-button').last().prop('disabled'), - false, - ); - }); - - it('has children text when submitText prop exists', function () { - assert.strictEqual( - wrapper.find('.page-container__footer-button').last().prop('children'), - 'Submit', - ); - }); - - it('should call submit when click is simulated', function () { - wrapper.find('.page-container__footer-button').last().prop('onClick')(); - assert.strictEqual(onSubmit.callCount, 1); - }); - }); -}); diff --git a/ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js new file mode 100644 index 000000000..00d7f8983 --- /dev/null +++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.test.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import PageContainerHeader from './page-container-header.component'; + +describe('Page Container Header', () => { + let wrapper, style, onBackButtonClick, onClose; + + beforeEach(() => { + style = { test: 'style' }; + onBackButtonClick = sinon.spy(); + onClose = sinon.spy(); + + wrapper = shallow( + , + ); + }); + + describe('Render Header Row', () => { + it('renders back button', () => { + expect(wrapper.find('.page-container__back-button')).toHaveLength(1); + expect(wrapper.find('.page-container__back-button').text()).toStrictEqual( + 'Back', + ); + }); + + it('ensures style prop', () => { + expect( + wrapper.find('.page-container__back-button').props().style, + ).toStrictEqual(style); + }); + + it('should call back button when click is simulated', () => { + wrapper.find('.page-container__back-button').prop('onClick')(); + expect(onBackButtonClick.callCount).toStrictEqual(1); + }); + }); + + describe('Render', () => { + let header, headerRow, pageTitle, pageSubtitle, pageClose, pageTab; + + beforeEach(() => { + header = wrapper.find('.page-container__header--no-padding-bottom'); + headerRow = wrapper.find('.page-container__header-row'); + pageTitle = wrapper.find('.page-container__title'); + pageSubtitle = wrapper.find('.page-container__subtitle'); + pageClose = wrapper.find('.page-container__header-close'); + pageTab = wrapper.find('.page-container__tabs'); + }); + + it('renders page container', () => { + expect(header).toHaveLength(1); + expect(headerRow).toHaveLength(1); + expect(pageTitle).toHaveLength(1); + expect(pageSubtitle).toHaveLength(1); + expect(pageClose).toHaveLength(1); + expect(pageTab).toHaveLength(1); + }); + + it('renders title', () => { + expect(pageTitle.text()).toStrictEqual('Test Title'); + }); + + it('renders subtitle', () => { + expect(pageSubtitle.text()).toStrictEqual('Test Subtitle'); + }); + + it('renders tabs', () => { + expect(pageTab.text()).toStrictEqual('Test Tab'); + }); + + it('should call close when click is simulated', () => { + pageClose.prop('onClick')(); + expect(onClose.callCount).toStrictEqual(1); + }); + }); +}); 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/tests/page-container-header.component.test.js deleted file mode 100644 index 1c96ee0e5..000000000 --- a/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; -import PageContainerHeader from '../page-container-header.component'; - -describe('Page Container Header', function () { - let wrapper, style, onBackButtonClick, onClose; - - beforeEach(function () { - style = { test: 'style' }; - onBackButtonClick = sinon.spy(); - onClose = sinon.spy(); - - wrapper = shallow( - , - ); - }); - - describe('Render Header Row', function () { - it('renders back button', function () { - assert.strictEqual( - wrapper.find('.page-container__back-button').length, - 1, - ); - assert.strictEqual( - wrapper.find('.page-container__back-button').text(), - 'Back', - ); - }); - - it('ensures style prop', function () { - assert.strictEqual( - wrapper.find('.page-container__back-button').props().style, - style, - ); - }); - - it('should call back button when click is simulated', function () { - wrapper.find('.page-container__back-button').prop('onClick')(); - assert.strictEqual(onBackButtonClick.callCount, 1); - }); - }); - - describe('Render', function () { - let header, headerRow, pageTitle, pageSubtitle, pageClose, pageTab; - - beforeEach(function () { - header = wrapper.find('.page-container__header--no-padding-bottom'); - headerRow = wrapper.find('.page-container__header-row'); - pageTitle = wrapper.find('.page-container__title'); - pageSubtitle = wrapper.find('.page-container__subtitle'); - pageClose = wrapper.find('.page-container__header-close'); - pageTab = wrapper.find('.page-container__tabs'); - }); - - it('renders page container', function () { - assert.strictEqual(header.length, 1); - assert.strictEqual(headerRow.length, 1); - assert.strictEqual(pageTitle.length, 1); - assert.strictEqual(pageSubtitle.length, 1); - assert.strictEqual(pageClose.length, 1); - assert.strictEqual(pageTab.length, 1); - }); - - it('renders title', function () { - assert.strictEqual(pageTitle.text(), 'Test Title'); - }); - - it('renders subtitle', function () { - assert.strictEqual(pageSubtitle.text(), 'Test Subtitle'); - }); - - it('renders tabs', function () { - assert.strictEqual(pageTab.text(), 'Test Tab'); - }); - - it('should call close when click is simulated', function () { - pageClose.prop('onClick')(); - assert.strictEqual(onClose.callCount, 1); - }); - }); -}); diff --git a/ui/app/components/ui/popover/popover.component.js b/ui/app/components/ui/popover/popover.component.js index 0f93c7183..6053c9d05 100644 --- a/ui/app/components/ui/popover/popover.component.js +++ b/ui/app/components/ui/popover/popover.component.js @@ -16,6 +16,7 @@ const Popover = ({ contentClassName, showArrow, CustomBackground, + popoverRef, }) => { const t = useI18nContext(); return ( @@ -25,7 +26,10 @@ const Popover = ({ ) : (
    )} -
    +
    {showArrow ?
    : null}
    @@ -42,6 +46,7 @@ const Popover = ({
    @@ -76,6 +81,9 @@ Popover.propTypes = { contentClassName: PropTypes.string, className: PropTypes.string, showArrow: PropTypes.bool, + popoverRef: PropTypes.shape({ + current: PropTypes.instanceOf(window.Element), + }), }; export default class PopoverPortal extends PureComponent { diff --git a/ui/app/components/ui/tabs/index.scss b/ui/app/components/ui/tabs/index.scss index ea22a7d52..4be72a141 100644 --- a/ui/app/components/ui/tabs/index.scss +++ b/ui/app/components/ui/tabs/index.scss @@ -1,6 +1,7 @@ @import 'tab/index'; .tabs { + flex-grow: 1; // Just for Firefox — https://github.com/MetaMask/metamask-extension/issues/8700 -moz-transform: translateZ(0); diff --git a/ui/app/components/ui/tabs/tab/tab.component.js b/ui/app/components/ui/tabs/tab/tab.component.js index 63325c025..1335511b2 100644 --- a/ui/app/components/ui/tabs/tab/tab.component.js +++ b/ui/app/components/ui/tabs/tab/tab.component.js @@ -31,13 +31,13 @@ const Tab = (props) => { }; Tab.propTypes = { - activeClassName: PropTypes.string, - className: PropTypes.string, + 'activeClassName': PropTypes.string, + 'className': PropTypes.string, 'data-testid': PropTypes.string, - isActive: PropTypes.bool, // required, but added using React.cloneElement - name: PropTypes.string.isRequired, - onClick: PropTypes.func, - tabIndex: PropTypes.number, // required, but added using React.cloneElement + 'isActive': PropTypes.bool, // required, but added using React.cloneElement + 'name': PropTypes.string.isRequired, + 'onClick': PropTypes.func, + 'tabIndex': PropTypes.number, // required, but added using React.cloneElement }; Tab.defaultProps = { 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..a9a212d16 100644 --- a/ui/app/components/ui/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js @@ -18,13 +18,13 @@ const styles = { '&$materialError': { color: '#aeaeae', }, - fontWeight: '400', - color: '#aeaeae', + 'fontWeight': '400', + 'color': '#aeaeae', }, materialFocused: {}, materialUnderline: { '&:after': { - borderBottom: '2px solid #f7861c', + borderBottom: `2px solid rgb(3, 125, 214)`, }, }, materialError: {}, @@ -32,7 +32,7 @@ const styles = { color: '#aeaeae', }, materialWhitePaddedInput: { - padding: '8px', + 'padding': '8px', '&::placeholder': { color: '#aeaeae', @@ -61,12 +61,12 @@ const styles = { 'label + &': { marginTop: '9px', }, - border: '1px solid #BBC0C5', - height: '48px', - borderRadius: '6px', - padding: '0 16px', - display: 'flex', - alignItems: 'center', + 'border': '1px solid #BBC0C5', + 'height': '48px', + 'borderRadius': '6px', + 'padding': '0 16px', + 'display': 'flex', + 'alignItems': 'center', '&$inputFocused': { border: '1px solid #2f9ae0', }, @@ -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,13 +180,16 @@ const getBorderedThemeInputProps = ({ }, inputProps: { dir, + min, + max, + autoComplete, }, }, }); const themeToInputProps = { - material: getMaterialThemeInputProps, - bordered: getBorderedThemeInputProps, + 'material': getMaterialThemeInputProps, + 'bordered': getBorderedThemeInputProps, 'material-white-padded': getMaterialWhitePaddedThemeInputProps, }; @@ -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 57% 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..33712be1d 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 @@ -1,19 +1,18 @@ -import assert from 'assert'; import React from 'react'; import PropTypes from 'prop-types'; 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 () { +describe('TokenInput Component', () => { const t = (key) => `translate ${key}`; - describe('rendering', function () { - it('should render properly', function () { + describe('rendering', () => { + it('should render properly', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -40,20 +39,18 @@ describe('TokenInput Component', function () { }, ); - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ABC'); - assert.strictEqual( - wrapper.find('.currency-input__conversion-component').length, - 1, - ); - assert.strictEqual( + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ABC'); + expect( + wrapper.find('.currency-input__conversion-component'), + ).toHaveLength(1); + expect( wrapper.find('.currency-input__conversion-component').text(), - 'translate noConversionRateAvailable', - ); + ).toStrictEqual('translate noConversionRateAvailable'); }); - it('should render properly with tokenExchangeRates', function () { + it('should render properly with tokenExchangeRates', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -81,13 +78,13 @@ describe('TokenInput Component', function () { }, ); - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ABC'); - assert.strictEqual(wrapper.find(CurrencyDisplay).length, 1); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ABC'); + expect(wrapper.find(CurrencyDisplay)).toHaveLength(1); }); - it('should render properly with a token value for ETH', function () { + it('should render properly with a token value for ETH', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -110,20 +107,21 @@ describe('TokenInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const tokenInputInstance = wrapper.find(TokenInput).at(0).instance(); - assert.strictEqual(tokenInputInstance.state.decimalValue, '1'); - assert.strictEqual(tokenInputInstance.state.hexValue, '2710'); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ABC'); - assert.strictEqual(wrapper.find('.unit-input__input').props().value, '1'); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(tokenInputInstance.state.decimalValue).toStrictEqual('1'); + expect(tokenInputInstance.state.hexValue).toStrictEqual('2710'); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ABC'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual( + '1', + ); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '2ETH', ); }); - it('should render properly with a token value for fiat', function () { + it('should render properly with a token value for fiat', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -147,20 +145,21 @@ describe('TokenInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const tokenInputInstance = wrapper.find(TokenInput).at(0).instance(); - assert.strictEqual(tokenInputInstance.state.decimalValue, '1'); - assert.strictEqual(tokenInputInstance.state.hexValue, '2710'); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ABC'); - assert.strictEqual(wrapper.find('.unit-input__input').props().value, '1'); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(tokenInputInstance.state.decimalValue).toStrictEqual('1'); + expect(tokenInputInstance.state.hexValue).toStrictEqual('2710'); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ABC'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual( + '1', + ); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$462.12USD', ); }); - it('should render properly with a token value for fiat, but hideConversion is true', function () { + it('should render properly with a token value for fiat, but hideConversion is true', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -191,30 +190,31 @@ describe('TokenInput Component', function () { }, ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const tokenInputInstance = wrapper.find(TokenInput).at(0).instance(); - assert.strictEqual(tokenInputInstance.state.decimalValue, '1'); - assert.strictEqual(tokenInputInstance.state.hexValue, '2710'); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ABC'); - assert.strictEqual(wrapper.find('.unit-input__input').props().value, '1'); - assert.strictEqual( - wrapper.find('.currency-input__conversion-component').text(), - 'translate noConversionRateAvailable', + expect(tokenInputInstance.state.decimalValue).toStrictEqual('1'); + expect(tokenInputInstance.state.hexValue).toStrictEqual('2710'); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ABC'); + expect(wrapper.find('.unit-input__input').props().value).toStrictEqual( + '1', ); + expect( + wrapper.find('.currency-input__conversion-component').text(), + ).toStrictEqual('translate noConversionRateAvailable'); }); }); - describe('handling actions', function () { + describe('handling actions', () => { const handleChangeSpy = sinon.spy(); const handleBlurSpy = sinon.spy(); - afterEach(function () { + afterEach(() => { handleChangeSpy.resetHistory(); handleBlurSpy.resetHistory(); }); - it('should call onChange on input changes with the hex value for ETH', function () { + it('should call onChange on input changes with the hex value for ETH', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -236,32 +236,30 @@ describe('TokenInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - assert.strictEqual(handleBlurSpy.callCount, 0); + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + expect(handleBlurSpy.callCount).toStrictEqual(0); const tokenInputInstance = wrapper.find(TokenInput).at(0).instance(); - assert.strictEqual(tokenInputInstance.state.decimalValue, 0); - assert.strictEqual(tokenInputInstance.state.hexValue, undefined); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(tokenInputInstance.state.decimalValue).toStrictEqual(0); + expect(tokenInputInstance.state.hexValue).toBeUndefined(); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '0ETH', ); const input = wrapper.find('input'); - assert.strictEqual(input.props().value, 0); + expect(input.props().value).toStrictEqual(0); input.simulate('change', { target: { value: 1 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith('2710')); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith('2710')).toStrictEqual(true); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '2ETH', ); - assert.strictEqual(tokenInputInstance.state.decimalValue, 1); - assert.strictEqual(tokenInputInstance.state.hexValue, '2710'); + expect(tokenInputInstance.state.decimalValue).toStrictEqual(1); + expect(tokenInputInstance.state.hexValue).toStrictEqual('2710'); }); - it('should call onChange on input changes with the hex value for fiat', function () { + it('should call onChange on input changes with the hex value for fiat', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -284,32 +282,30 @@ describe('TokenInput Component', function () { , ); - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - assert.strictEqual(handleBlurSpy.callCount, 0); + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + expect(handleBlurSpy.callCount).toStrictEqual(0); const tokenInputInstance = wrapper.find(TokenInput).at(0).instance(); - assert.strictEqual(tokenInputInstance.state.decimalValue, 0); - assert.strictEqual(tokenInputInstance.state.hexValue, undefined); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(tokenInputInstance.state.decimalValue).toStrictEqual(0); + expect(tokenInputInstance.state.hexValue).toBeUndefined(); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$0.00USD', ); const input = wrapper.find('input'); - assert.strictEqual(input.props().value, 0); + expect(input.props().value).toStrictEqual(0); input.simulate('change', { target: { value: 1 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith('2710')); - assert.strictEqual( - wrapper.find('.currency-display-component').text(), + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith('2710')).toStrictEqual(true); + expect(wrapper.find('.currency-display-component').text()).toStrictEqual( '$462.12USD', ); - assert.strictEqual(tokenInputInstance.state.decimalValue, 1); - assert.strictEqual(tokenInputInstance.state.hexValue, '2710'); + expect(tokenInputInstance.state.decimalValue).toStrictEqual(1); + expect(tokenInputInstance.state.hexValue).toStrictEqual('2710'); }); - it('should change the state and pass in a new decimalValue when props.value changes', function () { + it('should change the state and pass in a new decimalValue when props.value changes', () => { const mockStore = { metamask: { currentCurrency: 'usd', @@ -332,17 +328,19 @@ describe('TokenInput Component', function () { , ); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const tokenInputInstance = wrapper.find(TokenInput).dive(); - assert.strictEqual(tokenInputInstance.state('decimalValue'), 0); - assert.strictEqual(tokenInputInstance.state('hexValue'), undefined); - assert.strictEqual(tokenInputInstance.find(UnitInput).props().value, 0); + expect(tokenInputInstance.state('decimalValue')).toStrictEqual(0); + expect(tokenInputInstance.state('hexValue')).toBeUndefined(); + expect(tokenInputInstance.find(UnitInput).props().value).toStrictEqual(0); tokenInputInstance.setProps({ value: '2710' }); tokenInputInstance.update(); - assert.strictEqual(tokenInputInstance.state('decimalValue'), '1'); - assert.strictEqual(tokenInputInstance.state('hexValue'), '2710'); - assert.strictEqual(tokenInputInstance.find(UnitInput).props().value, '1'); + expect(tokenInputInstance.state('decimalValue')).toStrictEqual('1'); + expect(tokenInputInstance.state('hexValue')).toStrictEqual('2710'); + expect(tokenInputInstance.find(UnitInput).props().value).toStrictEqual( + '1', + ); }); }); }); diff --git a/ui/app/components/ui/truncated-definition-list/truncated-definition-list.stories.js b/ui/app/components/ui/truncated-definition-list/truncated-definition-list.stories.js index 01c9977e1..12a82f843 100644 --- a/ui/app/components/ui/truncated-definition-list/truncated-definition-list.stories.js +++ b/ui/app/components/ui/truncated-definition-list/truncated-definition-list.stories.js @@ -20,13 +20,13 @@ const basic = { const advanced = { 'Network Name': 'Ethereum Mainnet', 'Chain ID': '1', - Ticker: 'ETH', + 'Ticker': 'ETH', }; const tooltips = { 'Network Name': 'The name that is associated with this network', 'Chain ID': 'The numeric value representing the ID of this network', - Ticker: 'The currency symbol of the primary currency for this network', + 'Ticker': 'The currency symbol of the primary currency for this network', }; export const truncatedDefinitionList = () => ( diff --git a/ui/app/components/ui/unit-input/tests/unit-input.component.test.js b/ui/app/components/ui/unit-input/tests/unit-input.component.test.js deleted file mode 100644 index 032b9b76e..000000000 --- a/ui/app/components/ui/unit-input/tests/unit-input.component.test.js +++ /dev/null @@ -1,99 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import sinon from 'sinon'; -import UnitInput from '../unit-input.component'; - -describe('UnitInput Component', function () { - describe('rendering', function () { - it('should render properly without a suffix', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 0); - }); - - it('should render properly with a suffix', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input__suffix').length, 1); - assert.strictEqual(wrapper.find('.unit-input__suffix').text(), 'ETH'); - }); - - it('should render properly with a child component', function () { - const wrapper = shallow( - -
    TESTCOMPONENT
    -
    , - ); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.testing').length, 1); - assert.strictEqual(wrapper.find('.testing').text(), 'TESTCOMPONENT'); - }); - - it('should render with an error class when props.error === true', function () { - const wrapper = shallow(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.find('.unit-input--error').length, 1); - }); - }); - - describe('handling actions', function () { - const handleChangeSpy = sinon.spy(); - const handleBlurSpy = sinon.spy(); - - afterEach(function () { - handleChangeSpy.resetHistory(); - handleBlurSpy.resetHistory(); - }); - - it('should focus the input on component click', function () { - const wrapper = mount(); - - assert.ok(wrapper); - const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus'); - wrapper.instance().forceUpdate(); - wrapper.update(); - assert.strictEqual(handleFocusSpy.callCount, 0); - wrapper.find('.unit-input').simulate('click'); - assert.strictEqual(handleFocusSpy.callCount, 1); - }); - - it('should call onChange on input changes with the value', function () { - const wrapper = mount(); - - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - const input = wrapper.find('input'); - input.simulate('change', { target: { value: 123 } }); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith(123)); - assert.strictEqual(wrapper.state('value'), 123); - }); - - it('should set the component state value with props.value', function () { - const wrapper = mount(); - - assert.ok(wrapper); - assert.strictEqual(wrapper.state('value'), 123); - }); - - it('should update the component state value with props.value', function () { - const wrapper = mount(); - - assert.ok(wrapper); - assert.strictEqual(handleChangeSpy.callCount, 0); - const input = wrapper.find('input'); - input.simulate('change', { target: { value: 123 } }); - assert.strictEqual(wrapper.state('value'), 123); - assert.strictEqual(handleChangeSpy.callCount, 1); - assert.ok(handleChangeSpy.calledWith(123)); - wrapper.setProps({ value: 456 }); - assert.strictEqual(wrapper.state('value'), 456); - assert.strictEqual(handleChangeSpy.callCount, 1); - }); - }); -}); diff --git a/ui/app/components/ui/unit-input/unit-input.component.test.js b/ui/app/components/ui/unit-input/unit-input.component.test.js new file mode 100644 index 000000000..546c978ed --- /dev/null +++ b/ui/app/components/ui/unit-input/unit-input.component.test.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; +import UnitInput from './unit-input.component'; + +describe('UnitInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(0); + }); + + it('should render properly with a suffix', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix')).toHaveLength(1); + expect(wrapper.find('.unit-input__suffix').text()).toStrictEqual('ETH'); + }); + + it('should render properly with a child component', () => { + const wrapper = shallow( + +
    TESTCOMPONENT
    +
    , + ); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.testing')).toHaveLength(1); + expect(wrapper.find('.testing').text()).toStrictEqual('TESTCOMPONENT'); + }); + + it('should render with an error class when props.error === true', () => { + const wrapper = shallow(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.unit-input--error')).toHaveLength(1); + }); + }); + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy(); + const handleBlurSpy = sinon.spy(); + + afterEach(() => { + handleChangeSpy.resetHistory(); + handleBlurSpy.resetHistory(); + }); + + it('should focus the input on component click', () => { + const wrapper = mount(); + + expect(wrapper).toHaveLength(1); + const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus'); + wrapper.instance().forceUpdate(); + wrapper.update(); + expect(handleFocusSpy.callCount).toStrictEqual(0); + wrapper.find('.unit-input').simulate('click'); + expect(handleFocusSpy.callCount).toStrictEqual(1); + }); + + it('should call onChange on input changes with the value', () => { + const wrapper = mount(); + + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + const input = wrapper.find('input'); + input.simulate('change', { target: { value: 123 } }); + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith(123)).toStrictEqual(true); + expect(wrapper.state('value')).toStrictEqual(123); + }); + + it('should set the component state value with props.value', () => { + const wrapper = mount(); + + expect(wrapper).toHaveLength(1); + expect(wrapper.state('value')).toStrictEqual(123); + }); + + it('should update the component state value with props.value', () => { + const wrapper = mount(); + + expect(wrapper).toHaveLength(1); + expect(handleChangeSpy.callCount).toStrictEqual(0); + const input = wrapper.find('input'); + input.simulate('change', { target: { value: 123 } }); + expect(wrapper.state('value')).toStrictEqual(123); + expect(handleChangeSpy.callCount).toStrictEqual(1); + expect(handleChangeSpy.calledWith(123)).toStrictEqual(true); + wrapper.setProps({ value: 456 }); + expect(wrapper.state('value')).toStrictEqual(456); + expect(handleChangeSpy.callCount).toStrictEqual(1); + }); + }); +}); diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index c927e70c9..20bab88f7 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -50,6 +50,7 @@ export default function reduceApp(state = {}, action) { requestAccountTabs: {}, openMetaMaskTabs: {}, currentWindowTab: {}, + showWhatsNewPopup: true, ...state, }; @@ -352,6 +353,12 @@ export default function reduceApp(state = {}, action) { currentWindowTab: action.value, }; + case actionConstants.HIDE_WHATS_NEW_POPUP: + return { + ...appState, + showWhatsNewPopup: false, + }; + default: return appState; } @@ -364,3 +371,9 @@ export function setThreeBoxLastUpdated(lastUpdated) { value: lastUpdated, }; } + +export function hideWhatsNewPopup() { + return { + type: actionConstants.HIDE_WHATS_NEW_POPUP, + }; +} diff --git a/test/unit/ui/app/reducers/app.test.js b/ui/app/ducks/app/app.test.js similarity index 58% rename from test/unit/ui/app/reducers/app.test.js rename to ui/app/ducks/app/app.test.js index f95060e32..7a25bfe2b 100644 --- a/test/unit/ui/app/reducers/app.test.js +++ b/ui/app/ducks/app/app.test.js @@ -1,10 +1,9 @@ -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; -describe('App State', function () { +describe('App State', () => { const metamaskState = { selectedAddress: '0xAddress', identities: { @@ -15,31 +14,31 @@ describe('App State', function () { }, }; - it('App init state', function () { + it('app init state', () => { const initState = reduceApp(metamaskState, {}); - assert(initState); + expect.anything(initState); }); - it('sets networkDropdownOpen dropdown to true', function () { + it('sets networkDropdownOpen dropdown to true', () => { const state = reduceApp(metamaskState, { type: actions.NETWORK_DROPDOWN_OPEN, }); - assert.equal(state.networkDropdownOpen, true); + expect(state.networkDropdownOpen).toStrictEqual(true); }); - it('sets networkDropdownOpen dropdown to false', function () { + it('sets networkDropdownOpen dropdown to false', () => { const dropdown = { networkDropdowopen: true }; const state = { ...metamaskState, ...dropdown }; const newState = reduceApp(state, { type: actions.NETWORK_DROPDOWN_CLOSE, }); - assert.equal(newState.networkDropdownOpen, false); + expect(newState.networkDropdownOpen).toStrictEqual(false); }); - it('opens sidebar', function () { + it('opens sidebar', () => { const value = { transitionName: 'sidebar-right', type: 'wallet-view', @@ -50,10 +49,10 @@ describe('App State', function () { value, }); - assert.deepEqual(state.sidebar, value); + expect(state.sidebar).toStrictEqual(value); }); - it('closes sidebar', function () { + it('closes sidebar', () => { const openSidebar = { sidebar: { isOpen: true } }; const state = { ...metamaskState, ...openSidebar }; @@ -61,40 +60,40 @@ describe('App State', function () { type: actions.SIDEBAR_CLOSE, }); - assert.equal(newState.sidebar.isOpen, false); + expect(newState.sidebar.isOpen).toStrictEqual(false); }); - it('opens alert', function () { + it('opens alert', () => { const state = reduceApp(metamaskState, { type: actions.ALERT_OPEN, value: 'test message', }); - assert.equal(state.alertOpen, true); - assert.equal(state.alertMessage, 'test message'); + expect(state.alertOpen).toStrictEqual(true); + expect(state.alertMessage).toStrictEqual('test message'); }); - it('closes alert', function () { + it('closes alert', () => { const alert = { alertOpen: true, alertMessage: 'test message' }; const state = { ...metamaskState, ...alert }; const newState = reduceApp(state, { type: actions.ALERT_CLOSE, }); - assert.equal(newState.alertOpen, false); - assert.equal(newState.alertMessage, null); + expect(newState.alertOpen).toStrictEqual(false); + expect(newState.alertMessage).toBeNull(); }); - it('detects qr code data', function () { + it('detects qr code data', () => { const state = reduceApp(metamaskState, { type: actions.QR_CODE_DETECTED, value: 'qr data', }); - assert.equal(state.qrCodeData, 'qr data'); + expect(state.qrCodeData).toStrictEqual('qr data'); }); - it('opens modal', function () { + it('opens modal', () => { const state = reduceApp(metamaskState, { type: actions.MODAL_OPEN, payload: { @@ -102,11 +101,11 @@ describe('App State', function () { }, }); - assert.equal(state.modal.open, true); - assert.equal(state.modal.modalState.name, 'test'); + expect(state.modal.open).toStrictEqual(true); + expect(state.modal.modalState.name).toStrictEqual('test'); }); - it('closes modal, but moves open modal state to previous modal state', function () { + it('closes modal, but moves open modal state to previous modal state', () => { const opensModal = { modal: { open: true, @@ -121,49 +120,49 @@ describe('App State', function () { type: actions.MODAL_CLOSE, }); - assert.equal(newState.modal.open, false); - assert.equal(newState.modal.modalState.name, null); + expect(newState.modal.open).toStrictEqual(false); + expect(newState.modal.modalState.name).toBeNull(); }); - it('shows send token page', function () { + it('shows send token page', () => { const state = reduceApp(metamaskState, { type: actions.SHOW_SEND_TOKEN_PAGE, }); - assert.equal(state.warning, null); + expect(state.warning).toBeNull(); }); - it('locks Metamask', function () { + it('locks Metamask', () => { const state = reduceApp(metamaskState, { type: actions.LOCK_METAMASK, }); - assert.equal(state.warning, null); + expect(state.warning).toBeNull(); }); - it('goes home', function () { + it('goes home', () => { const state = reduceApp(metamaskState, { type: actions.GO_HOME, }); - assert.equal(state.accountDetail.subview, 'transactions'); - assert.equal(state.accountDetail.accountExport, 'none'); - assert.equal(state.accountDetail.privateKey, ''); - assert.equal(state.warning, null); + expect(state.accountDetail.subview).toStrictEqual('transactions'); + expect(state.accountDetail.accountExport).toStrictEqual('none'); + expect(state.accountDetail.privateKey).toStrictEqual(''); + expect(state.warning).toBeNull(); }); - it('shows account detail', function () { + it('shows account detail', () => { const state = reduceApp(metamaskState, { type: actions.SHOW_ACCOUNT_DETAIL, value: 'context address', }); - assert.equal(state.forgottenPassword, null); // default - assert.equal(state.accountDetail.subview, 'transactions'); // default - assert.equal(state.accountDetail.accountExport, 'none'); // default - assert.equal(state.accountDetail.privateKey, ''); // default + expect(state.forgottenPassword).toBeNull(); // default + expect(state.accountDetail.subview).toStrictEqual('transactions'); // default + expect(state.accountDetail.accountExport).toStrictEqual('none'); // default + expect(state.accountDetail.privateKey).toStrictEqual(''); // default }); - it('clears account details', function () { + it('clears account details', () => { const exportPrivKeyModal = { accountDetail: { subview: 'export', @@ -177,21 +176,21 @@ describe('App State', function () { type: actions.CLEAR_ACCOUNT_DETAILS, }); - assert.deepStrictEqual(newState.accountDetail, {}); + expect(newState.accountDetail).toStrictEqual({}); }); - it('shoes account page', function () { + it('shoes account page', () => { const state = reduceApp(metamaskState, { type: actions.SHOW_ACCOUNTS_PAGE, }); - assert.equal(state.isLoading, false); - assert.equal(state.warning, null); - assert.equal(state.scrollToBottom, false); - assert.equal(state.forgottenPassword, false); + expect(state.isLoading).toStrictEqual(false); + expect(state.warning).toBeNull(); + expect(state.scrollToBottom).toStrictEqual(false); + expect(state.forgottenPassword).toStrictEqual(false); }); - it('shows confirm tx page', function () { + it('shows confirm tx page', () => { const txs = { unapprovedTxs: { 1: { @@ -208,12 +207,12 @@ describe('App State', function () { id: 2, }); - assert.equal(state.txId, 2); - assert.equal(state.warning, null); - assert.equal(state.isLoading, false); + expect(state.txId).toStrictEqual(2); + expect(state.warning).toBeNull(); + expect(state.isLoading).toStrictEqual(false); }); - it('completes tx continues to show pending txs current view context', function () { + it('completes tx continues to show pending txs current view context', () => { const txs = { unapprovedTxs: { 1: { @@ -234,11 +233,11 @@ describe('App State', function () { }, }); - assert.equal(state.txId, null); - assert.equal(state.warning, null); + expect(state.txId).toBeNull(); + expect(state.warning).toBeNull(); }); - it('returns to account detail page when no unconf actions completed tx', function () { + it('returns to account detail page when no unconf actions completed tx', () => { const state = reduceApp(metamaskState, { type: actions.COMPLETED_TX, value: { @@ -246,38 +245,38 @@ describe('App State', function () { }, }); - assert.equal(state.warning, null); - assert.equal(state.accountDetail.subview, 'transactions'); + expect(state.warning).toBeNull(); + expect(state.accountDetail.subview).toStrictEqual('transactions'); }); - it('sets default warning when unlock fails', function () { + it('sets default warning when unlock fails', () => { const state = reduceApp(metamaskState, { type: actions.UNLOCK_FAILED, }); - assert.equal(state.warning, 'Incorrect password. Try again.'); + expect(state.warning).toStrictEqual('Incorrect password. Try again.'); }); - it('sets errors when unlock fails', function () { + it('sets errors when unlock fails', () => { const state = reduceApp(metamaskState, { type: actions.UNLOCK_FAILED, value: 'errors', }); - assert.equal(state.warning, 'errors'); + expect(state.warning).toStrictEqual('errors'); }); - it('sets warning to empty string when unlock succeeds', function () { + it('sets warning to empty string when unlock succeeds', () => { const errorState = { warning: 'errors' }; const oldState = { ...metamaskState, ...errorState }; const state = reduceApp(oldState, { type: actions.UNLOCK_SUCCEEDED, }); - assert.equal(state.warning, ''); + expect(state.warning).toStrictEqual(''); }); - it('sets hardware wallet default hd path', function () { + it('sets hardware wallet default hd path', () => { const hdPaths = { trezor: "m/44'/60'/0'/0", ledger: "m/44'/60'/0'", @@ -290,20 +289,20 @@ describe('App State', function () { }, }); - assert.deepEqual(state.defaultHdPaths, hdPaths); + expect(state.defaultHdPaths).toStrictEqual(hdPaths); }); - it('shows loading message', function () { + it('shows loading message', () => { const state = reduceApp(metamaskState, { type: actions.SHOW_LOADING, value: 'loading', }); - assert.equal(state.isLoading, true); - assert.equal(state.loadingMessage, 'loading'); + expect(state.isLoading).toStrictEqual(true); + expect(state.loadingMessage).toStrictEqual('loading'); }); - it('hides loading message', function () { + it('hides loading message', () => { const loadingState = { isLoading: true }; const oldState = { ...metamaskState, ...loadingState }; @@ -311,64 +310,64 @@ describe('App State', function () { type: actions.HIDE_LOADING, }); - assert.equal(state.isLoading, false); + expect(state.isLoading).toStrictEqual(false); }); - it('displays warning', function () { + it('displays warning', () => { const state = reduceApp(metamaskState, { type: actions.DISPLAY_WARNING, value: 'warning', }); - assert.equal(state.isLoading, false); - assert.equal(state.warning, 'warning'); + expect(state.isLoading).toStrictEqual(false); + expect(state.warning).toStrictEqual('warning'); }); - it('hides warning', function () { + it('hides warning', () => { const displayWarningState = { warning: 'warning' }; const oldState = { ...metamaskState, ...displayWarningState }; const state = reduceApp(oldState, { type: actions.HIDE_WARNING, }); - assert.equal(state.warning, undefined); + expect(state.warning).toBeUndefined(); }); - it('shows private key', function () { + it('shows private key', () => { const state = reduceApp(metamaskState, { type: actions.SHOW_PRIVATE_KEY, value: 'private key', }); - assert.equal(state.accountDetail.subview, 'export'); - assert.equal(state.accountDetail.accountExport, 'completed'); - assert.equal(state.accountDetail.privateKey, 'private key'); + expect(state.accountDetail.subview).toStrictEqual('export'); + expect(state.accountDetail.accountExport).toStrictEqual('completed'); + expect(state.accountDetail.privateKey).toStrictEqual('private key'); }); - it('set mouse user state', function () { + it('set mouse user state', () => { const state = reduceApp(metamaskState, { type: actions.SET_MOUSE_USER_STATE, value: true, }); - assert.equal(state.isMouseUser, true); + expect(state.isMouseUser).toStrictEqual(true); }); - it('sets gas loading', function () { + it('sets gas loading', () => { const state = reduceApp(metamaskState, { type: actions.GAS_LOADING_STARTED, }); - assert.equal(state.gasIsLoading, true); + expect(state.gasIsLoading).toStrictEqual(true); }); - it('unsets gas loading', function () { + it('unsets gas loading', () => { const gasLoadingState = { gasIsLoading: true }; const oldState = { ...metamaskState, ...gasLoadingState }; const state = reduceApp(oldState, { type: actions.GAS_LOADING_FINISHED, }); - assert.equal(state.gasIsLoading, false); + expect(state.gasIsLoading).toStrictEqual(false); }); }); diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js index 1457d766f..75e580a60 100644 --- a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js +++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js @@ -1,4 +1,3 @@ -import assert from 'assert'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import sinon from 'sinon'; @@ -37,8 +36,8 @@ const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'; const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION'; -describe('Confirm Transaction Duck', function () { - describe('State changes', function () { +describe('Confirm Transaction Duck', () => { + describe('State changes', () => { const mockState = { txData: { id: 1, @@ -58,61 +57,57 @@ describe('Confirm Transaction Duck', function () { nonce: '0x0', }; - it('should initialize state', function () { - assert.deepStrictEqual( - ConfirmTransactionReducer(undefined, {}), + it('should initialize state', () => { + expect(ConfirmTransactionReducer(undefined, {})).toStrictEqual( initialState, ); }); - it('should return state unchanged if it does not match a dispatched actions type', function () { - assert.deepStrictEqual( + it('should return state unchanged if it does not match a dispatched actions type', () => { + expect( ConfirmTransactionReducer(mockState, { type: 'someOtherAction', value: 'someValue', }), - { ...mockState }, - ); + ).toStrictEqual({ ...mockState }); }); - it('should set txData when receiving a UPDATE_TX_DATA action', function () { - assert.deepStrictEqual( + it('should set txData when receiving a UPDATE_TX_DATA action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_TX_DATA, payload: { id: 2, }, }), - { - ...mockState, - txData: { - ...mockState.txData, - id: 2, - }, + ).toStrictEqual({ + ...mockState, + txData: { + ...mockState.txData, + id: 2, }, - ); + }); }); - it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', function () { - assert.deepStrictEqual( + it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_TOKEN_DATA, payload: { name: 'defToken', }, }), - { - ...mockState, - tokenData: { - ...mockState.tokenData, - name: 'defToken', - }, + ).toStrictEqual({ + ...mockState, + tokenData: { + ...mockState.tokenData, + name: 'defToken', }, - ); + }); }); - it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', function () { - assert.deepStrictEqual( + it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_TRANSACTION_AMOUNTS, payload: { @@ -121,17 +116,16 @@ describe('Confirm Transaction Duck', function () { hexTransactionAmount: '0x1', }, }), - { - ...mockState, - fiatTransactionAmount: '123.45', - ethTransactionAmount: '.5', - hexTransactionAmount: '0x1', - }, - ); + ).toStrictEqual({ + ...mockState, + fiatTransactionAmount: '123.45', + ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', + }); }); - it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', function () { - assert.deepStrictEqual( + it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_TRANSACTION_FEES, payload: { @@ -140,17 +134,16 @@ describe('Confirm Transaction Duck', function () { hexTransactionFee: '0x1', }, }), - { - ...mockState, - fiatTransactionFee: '123.45', - ethTransactionFee: '.5', - hexTransactionFee: '0x1', - }, - ); + ).toStrictEqual({ + ...mockState, + fiatTransactionFee: '123.45', + ethTransactionFee: '.5', + hexTransactionFee: '0x1', + }); }); - it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', function () { - assert.deepStrictEqual( + it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_TRANSACTION_TOTALS, payload: { @@ -159,35 +152,32 @@ describe('Confirm Transaction Duck', function () { hexTransactionTotal: '0x1', }, }), - { - ...mockState, - fiatTransactionTotal: '123.45', - ethTransactionTotal: '.5', - hexTransactionTotal: '0x1', - }, - ); + ).toStrictEqual({ + ...mockState, + fiatTransactionTotal: '123.45', + ethTransactionTotal: '.5', + hexTransactionTotal: '0x1', + }); }); - it('should update nonce when receiving an UPDATE_NONCE action', function () { - assert.deepStrictEqual( + it('should update nonce when receiving an UPDATE_NONCE action', () => { + expect( ConfirmTransactionReducer(mockState, { type: UPDATE_NONCE, payload: '0x1', }), - { - ...mockState, - nonce: '0x1', - }, - ); + ).toStrictEqual({ + ...mockState, + nonce: '0x1', + }); }); - it('should clear confirmTransaction when receiving a FETCH_DATA_END action', function () { - assert.deepStrictEqual( + it('should clear confirmTransaction when receiving a FETCH_DATA_END action', () => { + expect( ConfirmTransactionReducer(mockState, { type: CLEAR_CONFIRM_TRANSACTION, }), - initialState, - ); + ).toStrictEqual(initialState); }); }); @@ -199,7 +189,7 @@ describe('Confirm Transaction Duck', function () { payload: txData, }; - assert.deepStrictEqual(actions.updateTxData(txData), expectedAction); + expect(actions.updateTxData(txData)).toStrictEqual(expectedAction); }); it('should create an action to update tokenData', function () { @@ -209,10 +199,7 @@ describe('Confirm Transaction Duck', function () { payload: tokenData, }; - assert.deepStrictEqual( - actions.updateTokenData(tokenData), - expectedAction, - ); + expect(actions.updateTokenData(tokenData)).toStrictEqual(expectedAction); }); it('should create an action to update transaction amounts', function () { @@ -222,10 +209,9 @@ describe('Confirm Transaction Duck', function () { payload: transactionAmounts, }; - assert.deepStrictEqual( + expect( actions.updateTransactionAmounts(transactionAmounts), - expectedAction, - ); + ).toStrictEqual(expectedAction); }); it('should create an action to update transaction fees', function () { @@ -235,8 +221,7 @@ describe('Confirm Transaction Duck', function () { payload: transactionFees, }; - assert.deepStrictEqual( - actions.updateTransactionFees(transactionFees), + expect(actions.updateTransactionFees(transactionFees)).toStrictEqual( expectedAction, ); }); @@ -248,8 +233,7 @@ describe('Confirm Transaction Duck', function () { payload: transactionTotals, }; - assert.deepStrictEqual( - actions.updateTransactionTotals(transactionTotals), + expect(actions.updateTransactionTotals(transactionTotals)).toStrictEqual( expectedAction, ); }); @@ -261,20 +245,20 @@ describe('Confirm Transaction Duck', function () { payload: nonce, }; - assert.deepStrictEqual(actions.updateNonce(nonce), expectedAction); + expect(actions.updateNonce(nonce)).toStrictEqual(expectedAction); }); - it('should create an action to clear confirmTransaction', function () { + it('should create an action to clear confirmTransaction', () => { const expectedAction = { type: CLEAR_CONFIRM_TRANSACTION, }; - assert.deepStrictEqual(actions.clearConfirmTransaction(), expectedAction); + expect(actions.clearConfirmTransaction()).toStrictEqual(expectedAction); }); }); - describe('Thunk actions', function () { - beforeEach(function () { + describe('Thunk actions', () => { + beforeEach(() => { global.eth = { getCode: sinon .stub() @@ -288,7 +272,7 @@ describe('Confirm Transaction Duck', function () { global.eth.getCode.resetHistory(); }); - it('updates txData and updates gas values in confirmTransaction', function () { + it('updates txData and updates gas values in confirmTransaction', () => { const txData = { history: [], id: 2603411941761054, @@ -348,13 +332,13 @@ describe('Confirm Transaction Duck', function () { store.dispatch(actions.updateTxDataAndCalculate(txData)); const storeActions = store.getActions(); - assert.strictEqual(storeActions.length, expectedActions.length); + expect(storeActions).toHaveLength(expectedActions.length); storeActions.forEach((action, index) => - assert.strictEqual(action.type, expectedActions[index]), + expect(action.type).toStrictEqual(expectedActions[index]), ); }); - it('updates confirmTransaction transaction', function () { + it('updates confirmTransaction transaction', () => { const mockState = { metamask: { conversionRate: 468.58, @@ -397,10 +381,10 @@ describe('Confirm Transaction Duck', function () { store.dispatch(actions.setTransactionToConfirm(2603411941761054)); const storeActions = store.getActions(); - assert.strictEqual(storeActions.length, expectedActions.length); + expect(storeActions).toHaveLength(expectedActions.length); storeActions.forEach((action, index) => - assert.strictEqual(action.type, expectedActions[index]), + expect(action.type).toStrictEqual(expectedActions[index]), ); }); }); diff --git a/ui/app/ducks/gas/gas-duck.test.js b/ui/app/ducks/gas/gas-duck.test.js index b66b1dadd..eab7a74b6 100644 --- a/ui/app/ducks/gas/gas-duck.test.js +++ b/ui/app/ducks/gas/gas-duck.test.js @@ -1,9 +1,8 @@ -import assert from 'assert'; import nock from 'nock'; import sinon from 'sinon'; import BN from 'bn.js'; -import GasDuck, { +import GasReducer, { basicGasEstimatesLoadingStarted, basicGasEstimatesLoadingFinished, setBasicGasEstimateData, @@ -12,15 +11,31 @@ import GasDuck, { fetchBasicGasEstimates, } from './gas.duck'; -const mockGasPriceApiResponse = { - SafeGasPrice: 10, - ProposeGasPrice: 20, - FastGasPrice: 30, -}; +jest.mock('../../../lib/storage-helpers.js', () => ({ + getStorageItem: jest.fn(), + setStorageItem: jest.fn(), +})); + +describe('Gas Duck', () => { + let tempDateNow; + const mockGasPriceApiResponse = { + SafeGasPrice: 10, + ProposeGasPrice: 20, + FastGasPrice: 30, + }; + + beforeEach(() => { + tempDateNow = global.Date.now; -const GasReducer = GasDuck; + global.Date.now = () => 2000000; + }); + + afterEach(() => { + sinon.restore(); + + global.Date.now = tempDateNow; + }); -describe('Gas Duck', function () { const mockState = { mockProp: 123, }; @@ -55,84 +70,81 @@ describe('Gas Duck', function () { const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT'; const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE'; - describe('GasReducer()', function () { - it('should initialize state', function () { - assert.deepStrictEqual(GasReducer(undefined, {}), initState); + describe('GasReducer()', () => { + it('should initialize state', () => { + expect(GasReducer(undefined, {})).toStrictEqual(initState); }); - it('should return state unchanged if it does not match a dispatched actions type', function () { - assert.deepStrictEqual( + it('should return state unchanged if it does not match a dispatched actions type', () => { + expect( GasReducer(mockState, { type: 'someOtherAction', value: 'someValue', }), - mockState, - ); + ).toStrictEqual(mockState); }); - it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', function () { - assert.deepStrictEqual( + it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => { + expect( GasReducer(mockState, { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }), - { basicEstimateIsLoading: true, ...mockState }, - ); + ).toStrictEqual({ basicEstimateIsLoading: true, ...mockState }); }); - it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', function () { - assert.deepStrictEqual( + it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => { + expect( GasReducer(mockState, { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }), - { basicEstimateIsLoading: false, ...mockState }, - ); + ).toStrictEqual({ basicEstimateIsLoading: false, ...mockState }); }); - it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', function () { - assert.deepStrictEqual( + it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => { + expect( GasReducer(mockState, { type: SET_BASIC_GAS_ESTIMATE_DATA, value: { someProp: 'someData123' }, }), - { basicEstimates: { someProp: 'someData123' }, ...mockState }, - ); + ).toStrictEqual({ + basicEstimates: { someProp: 'someData123' }, + ...mockState, + }); }); - it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', function () { - assert.deepStrictEqual( + it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => { + expect( GasReducer(mockState, { type: SET_CUSTOM_GAS_PRICE, value: 4321, }), - { customData: { price: 4321 }, ...mockState }, - ); + ).toStrictEqual({ customData: { price: 4321 }, ...mockState }); }); - it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', function () { - assert.deepStrictEqual( + it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', () => { + expect( GasReducer(mockState, { type: SET_CUSTOM_GAS_LIMIT, value: 9876, }), - { customData: { limit: 9876 }, ...mockState }, - ); + ).toStrictEqual({ customData: { limit: 9876 }, ...mockState }); }); }); - describe('basicGasEstimatesLoadingStarted', function () { - it('should create the correct action', function () { - assert.deepStrictEqual(basicGasEstimatesLoadingStarted(), { + describe('basicGasEstimatesLoadingStarted', () => { + it('should create the correct action', () => { + expect(basicGasEstimatesLoadingStarted()).toStrictEqual({ type: BASIC_GAS_ESTIMATE_LOADING_STARTED, }); }); }); - describe('basicGasEstimatesLoadingFinished', function () { - it('should create the correct action', function () { - assert.deepStrictEqual(basicGasEstimatesLoadingFinished(), { + describe('basicGasEstimatesLoadingFinished', () => { + it('should create the correct action', () => { + expect(basicGasEstimatesLoadingFinished()).toStrictEqual({ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED, }); }); }); - describe('fetchBasicGasEstimates', function () { - it('should call fetch with the expected params', async function () { + describe('fetchBasicGasEstimates', () => { + it('should call fetch with the expected params', async () => { const mockDistpatch = sinon.spy(); const windowFetchSpy = sinon.spy(window, 'fetch'); @@ -144,23 +156,23 @@ describe('Gas Duck', function () { gas: { ...initState }, metamask: { provider: { ...providerState } }, })); - assert.deepStrictEqual(mockDistpatch.getCall(0).args, [ + + expect(mockDistpatch.getCall(0).args).toStrictEqual([ { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }, ]); - assert.ok( + expect( windowFetchSpy .getCall(0) .args[0].startsWith('https://api.metaswap.codefi.network/gasPrices'), - 'should fetch metaswap /gasPrices', - ); + ).toStrictEqual(true); - assert.deepStrictEqual(mockDistpatch.getCall(2).args, [ + expect(mockDistpatch.getCall(2).args).toStrictEqual([ { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }, ]); }); - it('should call fetch with the expected params for test network', async function () { + it('should call fetch with the expected params for test network', async () => { global.eth = { gasPrice: sinon.fake.returns(new BN(48199313, 10)) }; const mockDistpatch = sinon.spy(); @@ -174,13 +186,13 @@ describe('Gas Duck', function () { }; await fetchBasicGasEstimates()(mockDistpatch, () => ({ - gas: { ...initState }, + gas: { ...initState, basicPriceAEstimatesLastRetrieved: 1000000 }, metamask: { provider: { ...providerStateForTestNetwork } }, })); - assert.deepStrictEqual(mockDistpatch.getCall(0).args, [ + expect(mockDistpatch.getCall(0).args).toStrictEqual([ { type: BASIC_GAS_ESTIMATE_LOADING_STARTED }, ]); - assert.deepStrictEqual(mockDistpatch.getCall(1).args, [ + expect(mockDistpatch.getCall(1).args).toStrictEqual([ { type: SET_BASIC_GAS_ESTIMATE_DATA, value: { @@ -188,33 +200,33 @@ describe('Gas Duck', function () { }, }, ]); - assert.deepStrictEqual(mockDistpatch.getCall(2).args, [ + expect(mockDistpatch.getCall(2).args).toStrictEqual([ { type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }, ]); }); }); - describe('setBasicGasEstimateData', function () { - it('should create the correct action', function () { - assert.deepStrictEqual(setBasicGasEstimateData('mockBasicEstimatData'), { + describe('setBasicGasEstimateData', () => { + it('should create the correct action', () => { + expect(setBasicGasEstimateData('mockBasicEstimatData')).toStrictEqual({ type: SET_BASIC_GAS_ESTIMATE_DATA, value: 'mockBasicEstimatData', }); }); }); - describe('setCustomGasPrice', function () { - it('should create the correct action', function () { - assert.deepStrictEqual(setCustomGasPrice('mockCustomGasPrice'), { + describe('setCustomGasPrice', () => { + it('should create the correct action', () => { + expect(setCustomGasPrice('mockCustomGasPrice')).toStrictEqual({ type: SET_CUSTOM_GAS_PRICE, value: 'mockCustomGasPrice', }); }); }); - describe('setCustomGasLimit', function () { - it('should create the correct action', function () { - assert.deepStrictEqual(setCustomGasLimit('mockCustomGasLimit'), { + describe('setCustomGasLimit', () => { + it('should create the correct action', () => { + expect(setCustomGasLimit('mockCustomGasLimit')).toStrictEqual({ type: SET_CUSTOM_GAS_LIMIT, value: 'mockCustomGasLimit', }); 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 65% rename from test/unit/ui/app/reducers/metamask.test.js rename to ui/app/ducks/metamask/metamask.test.js index faf765b86..2c268acc3 100644 --- a/test/unit/ui/app/reducers/metamask.test.js +++ b/ui/app/ducks/metamask/metamask.test.js @@ -1,14 +1,13 @@ -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 () { +describe('MetaMask Reducers', () => { + it('init state', () => { const initState = reduceMetamask(undefined, {}); - assert(initState); + expect.anything(initState); }); - it('locks MetaMask', function () { + it('locks MetaMask', () => { const unlockMetaMaskState = { isUnlocked: true, selectedAddress: 'test address', @@ -17,10 +16,10 @@ describe('MetaMask Reducers', function () { type: actionConstants.LOCK_METAMASK, }); - assert.equal(lockMetaMask.isUnlocked, false); + expect(lockMetaMask.isUnlocked).toStrictEqual(false); }); - it('sets rpc target', function () { + it('sets rpc target', () => { const state = reduceMetamask( {}, { @@ -29,10 +28,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.provider.rpcUrl, 'https://custom.rpc'); + expect(state.provider.rpcUrl).toStrictEqual('https://custom.rpc'); }); - it('sets provider type', function () { + it('sets provider type', () => { const state = reduceMetamask( {}, { @@ -41,10 +40,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.provider.type, 'provider type'); + expect(state.provider.type).toStrictEqual('provider type'); }); - it('shows account detail', function () { + it('shows account detail', () => { const state = reduceMetamask( {}, { @@ -53,12 +52,12 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.isUnlocked, true); - assert.equal(state.isInitialized, true); - assert.equal(state.selectedAddress, 'test address'); + expect(state.isUnlocked).toStrictEqual(true); + expect(state.isInitialized).toStrictEqual(true); + expect(state.selectedAddress).toStrictEqual('test address'); }); - it('sets account label', function () { + it('sets account label', () => { const state = reduceMetamask( {}, { @@ -70,12 +69,12 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(state.identities, { + expect(state.identities).toStrictEqual({ 'test account': { name: 'test label' }, }); }); - it('sets current fiat', function () { + it('sets current fiat', () => { const value = { currentCurrency: 'yen', conversionRate: 3.14, @@ -90,12 +89,12 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.currentCurrency, value.currentCurrency); - assert.equal(state.conversionRate, value.conversionRate); - assert.equal(state.conversionDate, value.conversionDate); + expect(state.currentCurrency).toStrictEqual(value.currentCurrency); + expect(state.conversionRate).toStrictEqual(value.conversionRate); + expect(state.conversionDate).toStrictEqual(value.conversionDate); }); - it('updates tokens', function () { + it('updates tokens', () => { const newTokens = { address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', decimals: 18, @@ -110,10 +109,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(state.tokens, newTokens); + expect(state.tokens).toStrictEqual(newTokens); }); - it('updates send gas limit', function () { + it('updates send gas limit', () => { const state = reduceMetamask( {}, { @@ -122,10 +121,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.gasLimit, '0xGasLimit'); + expect(state.send.gasLimit).toStrictEqual('0xGasLimit'); }); - it('updates send gas price', function () { + it('updates send gas price', () => { const state = reduceMetamask( {}, { @@ -134,10 +133,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.gasPrice, '0xGasPrice'); + expect(state.send.gasPrice).toStrictEqual('0xGasPrice'); }); - it('toggles account menu ', function () { + it('toggles account menu', () => { const state = reduceMetamask( {}, { @@ -145,10 +144,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.isAccountMenuOpen, true); + expect(state.isAccountMenuOpen).toStrictEqual(true); }); - it('updates gas total', function () { + it('updates gas total', () => { const state = reduceMetamask( {}, { @@ -157,10 +156,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.gasTotal, '0xGasTotal'); + expect(state.send.gasTotal).toStrictEqual('0xGasTotal'); }); - it('updates send token balance', function () { + it('updates send token balance', () => { const state = reduceMetamask( {}, { @@ -169,10 +168,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.tokenBalance, '0xTokenBalance'); + expect(state.send.tokenBalance).toStrictEqual('0xTokenBalance'); }); - it('updates data', function () { + it('updates data', () => { const state = reduceMetamask( {}, { @@ -181,10 +180,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.data, '0xData'); + expect(state.send.data).toStrictEqual('0xData'); }); - it('updates send to', function () { + it('updates send to', () => { const state = reduceMetamask( {}, { @@ -196,11 +195,11 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.to, '0xAddress'); - assert.equal(state.send.toNickname, 'nickname'); + expect(state.send.to).toStrictEqual('0xAddress'); + expect(state.send.toNickname).toStrictEqual('nickname'); }); - it('update send amount', function () { + it('update send amount', () => { const state = reduceMetamask( {}, { @@ -209,10 +208,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.amount, '0xAmount'); + expect(state.send.amount).toStrictEqual('0xAmount'); }); - it('updates max mode', function () { + it('updates max mode', () => { const state = reduceMetamask( {}, { @@ -221,10 +220,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.send.maxModeOn, true); + expect(state.send.maxModeOn).toStrictEqual(true); }); - it('update send', function () { + it('update send', () => { const value = { gasLimit: '0xGasLimit', gasPrice: '0xGasPrice', @@ -250,10 +249,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(sendState.send, value); + expect(sendState.send).toStrictEqual(value); }); - it('clears send', function () { + it('clears send', () => { const initStateSend = { send: { gasLimit: null, @@ -292,10 +291,10 @@ describe('MetaMask Reducers', function () { type: actionConstants.CLEAR_SEND, }); - assert.deepEqual(state.send, initStateSend.send); + expect(state.send).toStrictEqual(initStateSend.send); }); - it('updates value of tx by id', function () { + it('updates value of tx by id', () => { const oldState = { currentNetworkTxList: [ { @@ -311,10 +310,10 @@ describe('MetaMask Reducers', function () { value: 'bar', }); - assert.equal(state.currentNetworkTxList[0].txParams, 'bar'); + expect(state.currentNetworkTxList[0].txParams).toStrictEqual('bar'); }); - it('sets blockies', function () { + it('sets blockies', () => { const state = reduceMetamask( {}, { @@ -323,10 +322,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.useBlockie, true); + expect(state.useBlockie).toStrictEqual(true); }); - it('updates an arbitrary feature flag', function () { + it('updates an arbitrary feature flag', () => { const state = reduceMetamask( {}, { @@ -337,10 +336,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.featureFlags.foo, true); + expect(state.featureFlags.foo).toStrictEqual(true); }); - it('close welcome screen', function () { + it('close welcome screen', () => { const state = reduceMetamask( {}, { @@ -348,10 +347,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.welcomeScreenSeen, true); + expect(state.welcomeScreenSeen).toStrictEqual(true); }); - it('sets current locale', function () { + it('sets current locale', () => { const state = reduceMetamask( {}, { @@ -360,10 +359,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.equal(state.currentLocale, 'ge'); + expect(state.currentLocale).toStrictEqual('ge'); }); - it('sets pending tokens ', function () { + it('sets pending tokens', () => { const payload = { address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', decimals: 18, @@ -378,10 +377,10 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(pendingTokensState.pendingTokens, payload); + expect(pendingTokensState.pendingTokens).toStrictEqual(payload); }); - it('clears pending tokens', function () { + it('clears pending tokens', () => { const payload = { address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', decimals: 18, @@ -396,10 +395,10 @@ describe('MetaMask Reducers', function () { type: actionConstants.CLEAR_PENDING_TOKENS, }); - assert.deepEqual(state.pendingTokens, {}); + expect(state.pendingTokens).toStrictEqual({}); }); - it('update ensResolution', function () { + it('update ensResolution', () => { const state = reduceMetamask( {}, { @@ -408,11 +407,11 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(state.send.ensResolution, '0x1337'); - assert.deepEqual(state.send.ensResolutionError, ''); + expect(state.send.ensResolution).toStrictEqual('0x1337'); + expect(state.send.ensResolutionError).toStrictEqual(''); }); - it('update ensResolutionError', function () { + it('update ensResolutionError', () => { const state = reduceMetamask( {}, { @@ -421,7 +420,7 @@ describe('MetaMask Reducers', function () { }, ); - assert.deepEqual(state.send.ensResolutionError, 'ens name not found'); - assert.deepEqual(state.send.ensResolution, null); + expect(state.send.ensResolutionError).toStrictEqual('ens name not found'); + expect(state.send.ensResolution).toBeNull(); }); }); diff --git a/ui/app/ducks/send/send-duck.test.js b/ui/app/ducks/send/send-duck.test.js index 3f24b7ece..12ef5bbb3 100644 --- a/ui/app/ducks/send/send-duck.test.js +++ b/ui/app/ducks/send/send-duck.test.js @@ -1,5 +1,3 @@ -import assert from 'assert'; - import SendReducer, { openToDropdown, closeToDropdown, @@ -8,7 +6,7 @@ import SendReducer, { hideGasButtonGroup, } from './send.duck'; -describe('Send Duck', function () { +describe('Send Duck', () => { const mockState = { mockProp: 123, }; @@ -24,112 +22,107 @@ describe('Send Duck', function () { const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'; const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'; - describe('SendReducer()', function () { - it('should initialize state', function () { - assert.deepStrictEqual(SendReducer(undefined, {}), initState); + describe('SendReducer()', () => { + it('should initialize state', () => { + expect(SendReducer(undefined, {})).toStrictEqual(initState); }); - it('should return state unchanged if it does not match a dispatched actions type', function () { - assert.deepStrictEqual( + it('should return state unchanged if it does not match a dispatched actions type', () => { + expect( SendReducer(mockState, { type: 'someOtherAction', value: 'someValue', }), - mockState, - ); + ).toStrictEqual(mockState); }); - it('should set toDropdownOpen to true when receiving a OPEN_TO_DROPDOWN action', function () { - assert.deepStrictEqual( + it('should set toDropdownOpen to true when receiving a OPEN_TO_DROPDOWN action', () => { + expect( SendReducer(mockState, { type: OPEN_TO_DROPDOWN, }), - { toDropdownOpen: true, ...mockState }, - ); + ).toStrictEqual({ toDropdownOpen: true, ...mockState }); }); - it('should set toDropdownOpen to false when receiving a CLOSE_TO_DROPDOWN action', function () { - assert.deepStrictEqual( + it('should set toDropdownOpen to false when receiving a CLOSE_TO_DROPDOWN action', () => { + expect( SendReducer(mockState, { type: CLOSE_TO_DROPDOWN, }), - { toDropdownOpen: false, ...mockState }, - ); + ).toStrictEqual({ toDropdownOpen: false, ...mockState }); }); - it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', function () { - assert.deepStrictEqual( + it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => { + expect( SendReducer( { ...mockState, gasButtonGroupShown: false }, { type: SHOW_GAS_BUTTON_GROUP }, ), - { gasButtonGroupShown: true, ...mockState }, - ); + ).toStrictEqual({ gasButtonGroupShown: true, ...mockState }); }); - it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', function () { - assert.deepStrictEqual( + it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => { + expect( SendReducer(mockState, { type: HIDE_GAS_BUTTON_GROUP }), - { gasButtonGroupShown: false, ...mockState }, - ); + ).toStrictEqual({ gasButtonGroupShown: false, ...mockState }); }); - it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', function () { + it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => { const modifiedMockState = { ...mockState, errors: { someError: false, }, }; - assert.deepStrictEqual( + expect( SendReducer(modifiedMockState, { type: UPDATE_SEND_ERRORS, value: { someOtherError: true }, }), - { - ...modifiedMockState, - errors: { - someError: false, - someOtherError: true, - }, + ).toStrictEqual({ + ...modifiedMockState, + errors: { + someError: false, + someOtherError: true, }, - ); + }); }); - it('should return the initial state in response to a RESET_SEND_STATE action', function () { - assert.deepStrictEqual( + it('should return the initial state in response to a RESET_SEND_STATE action', () => { + expect( SendReducer(mockState, { type: RESET_SEND_STATE, }), - initState, - ); + ).toStrictEqual(initState); }); }); - describe('openToDropdown', function () { - assert.deepStrictEqual(openToDropdown(), { type: OPEN_TO_DROPDOWN }); - }); + describe('Send Duck Actions', () => { + it('calls openToDropdown action', () => { + expect(openToDropdown()).toStrictEqual({ type: OPEN_TO_DROPDOWN }); + }); - describe('closeToDropdown', function () { - assert.deepStrictEqual(closeToDropdown(), { type: CLOSE_TO_DROPDOWN }); - }); + it('calls closeToDropdown action', () => { + expect(closeToDropdown()).toStrictEqual({ type: CLOSE_TO_DROPDOWN }); + }); - describe('showGasButtonGroup', function () { - assert.deepStrictEqual(showGasButtonGroup(), { - type: SHOW_GAS_BUTTON_GROUP, + it('calls showGasButtonGroup action', () => { + expect(showGasButtonGroup()).toStrictEqual({ + type: SHOW_GAS_BUTTON_GROUP, + }); }); - }); - describe('hideGasButtonGroup', function () { - assert.deepStrictEqual(hideGasButtonGroup(), { - type: HIDE_GAS_BUTTON_GROUP, + it('calls hideGasButtonGroup action', () => { + expect(hideGasButtonGroup()).toStrictEqual({ + type: HIDE_GAS_BUTTON_GROUP, + }); }); - }); - describe('updateSendErrors', function () { - assert.deepStrictEqual(updateSendErrors('mockErrorObject'), { - type: UPDATE_SEND_ERRORS, - value: 'mockErrorObject', + it('calls updateSendErrors action', () => { + expect(updateSendErrors('mockErrorObject')).toStrictEqual({ + type: UPDATE_SEND_ERRORS, + value: 'mockErrorObject', + }); }); }); }); diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 06e4c759b..f33b06745 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -2,6 +2,8 @@ import { createSlice } from '@reduxjs/toolkit'; import BigNumber from 'bignumber.js'; import log from 'loglevel'; +import { captureMessage } from '@sentry/browser'; + import { addToken, addUnapprovedTransaction, @@ -33,6 +35,7 @@ import { import { fetchSwapsFeatureLiveness, fetchSwapsGasPrices, + isContractAddressValid, } from '../../pages/swaps/swaps.util'; import { calcGasTotal } from '../../pages/send/send.utils'; import { @@ -56,7 +59,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', @@ -362,6 +365,21 @@ export const fetchAndSetSwapsGasPriceInfo = () => { }; }; +export const fetchSwapsLiveness = () => { + return async (dispatch, getState) => { + let swapsFeatureIsLive = false; + try { + swapsFeatureIsLive = await fetchSwapsFeatureLiveness( + getCurrentChainId(getState()), + ); + } catch (error) { + log.error('Failed to fetch Swaps liveness, defaulting to false.', error); + } + await dispatch(setSwapsLiveness(swapsFeatureIsLive)); + return swapsFeatureIsLive; + }; +}; + export const fetchQuotesAndSetQuoteState = ( history, inputValue, @@ -417,6 +435,7 @@ export const fetchQuotesAndSetQuoteState = ( let destinationTokenAddedForSwap = false; if ( + toTokenAddress && toTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[toTokenAddress] ) { @@ -432,6 +451,7 @@ export const fetchQuotesAndSetQuoteState = ( ); } if ( + fromTokenAddress && fromTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[fromTokenAddress] && fromTokenBalance && @@ -659,6 +679,19 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { sensitiveProperties: swapMetaData, }); + if (!isContractAddressValid(usedTradeTxParams.to, swapMetaData, chainId)) { + captureMessage('Invalid contract address', { + extra: { + token_from: swapMetaData.token_from, + token_to: swapMetaData.token_to, + contract_address: usedTradeTxParams.to, + }, + }); + await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR)); + history.push(SWAPS_ERROR_ROUTE); + return; + } + let finalApproveTxMeta; const approveTxParams = getApproveTxParams(state); if (approveTxParams) { @@ -673,7 +706,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { updateTransaction( { ...approveTxMeta, - transactionCategory: TRANSACTION_CATEGORIES.SWAP_APPROVAL, + type: TRANSACTION_TYPES.SWAP_APPROVAL, sourceTokenSymbol: sourceTokenInfo.symbol, }, true, @@ -714,7 +747,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/ducks/swaps/swaps.test.js b/ui/app/ducks/swaps/swaps.test.js new file mode 100644 index 000000000..585571d0e --- /dev/null +++ b/ui/app/ducks/swaps/swaps.test.js @@ -0,0 +1,118 @@ +import nock from 'nock'; + +import { setSwapsLiveness } from '../../store/actions'; +import { setStorageItem } from '../../../lib/storage-helpers'; +import * as swaps from './swaps'; + +jest.mock('../../store/actions.js', () => ({ + setSwapsLiveness: jest.fn(), +})); + +const providerState = { + chainId: '0x1', + nickname: '', + rpcPrefs: {}, + rpcUrl: '', + ticker: 'ETH', + type: 'mainnet', +}; + +describe('Ducks - Swaps', () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe('fetchSwapsLiveness', () => { + const cleanFeatureFlagApiCache = () => { + setStorageItem( + 'cachedFetch:https://api.metaswap.codefi.network/featureFlag', + null, + ); + }; + + afterEach(() => { + cleanFeatureFlagApiCache(); + }); + + const mockFeatureFlagApiResponse = ({ + active = false, + replyWithError = false, + } = {}) => { + const apiNock = nock('https://api.metaswap.codefi.network').get( + '/featureFlag', + ); + if (replyWithError) { + return apiNock.replyWithError({ + message: 'Server error. Try again later', + code: 'serverSideError', + }); + } + return apiNock.reply(200, { + active, + }); + }; + + const createGetState = () => { + return () => ({ + metamask: { provider: { ...providerState } }, + }); + }; + + it('returns true if the Swaps feature is enabled', async () => { + const mockDispatch = jest.fn(); + const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); + const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( + mockDispatch, + createGetState(), + ); + expect(featureFlagApiNock.isDone()).toBe(true); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(setSwapsLiveness).toHaveBeenCalledWith(true); + expect(isSwapsFeatureEnabled).toBe(true); + }); + + it('returns false if the Swaps feature is disabled', async () => { + const mockDispatch = jest.fn(); + const featureFlagApiNock = mockFeatureFlagApiResponse({ active: false }); + const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( + mockDispatch, + createGetState(), + ); + expect(featureFlagApiNock.isDone()).toBe(true); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(setSwapsLiveness).toHaveBeenCalledWith(false); + expect(isSwapsFeatureEnabled).toBe(false); + }); + + it('returns false if the /featureFlag API call throws an error', async () => { + const mockDispatch = jest.fn(); + const featureFlagApiNock = mockFeatureFlagApiResponse({ + replyWithError: true, + }); + const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( + mockDispatch, + createGetState(), + ); + expect(featureFlagApiNock.isDone()).toBe(true); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(setSwapsLiveness).toHaveBeenCalledWith(false); + expect(isSwapsFeatureEnabled).toBe(false); + }); + + it('only calls the API once and returns true from cache for the second call', async () => { + const mockDispatch = jest.fn(); + const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); + await swaps.fetchSwapsLiveness()(mockDispatch, createGetState()); + expect(featureFlagApiNock.isDone()).toBe(true); + const featureFlagApiNock2 = mockFeatureFlagApiResponse({ active: true }); + const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( + mockDispatch, + createGetState(), + ); + expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead. + expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(setSwapsLiveness).toHaveBeenCalledWith(true); + expect(isSwapsFeatureEnabled).toBe(true); + }); + }); +}); 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/routes.js b/ui/app/helpers/constants/routes.js index 61b21b50c..1ed9a7f0d 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -14,11 +14,6 @@ const CONTACT_LIST_ROUTE = '/settings/contact-list'; const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; -const CONTACT_MY_ACCOUNTS_ROUTE = '/settings/contact-list/my-accounts'; -const CONTACT_MY_ACCOUNTS_VIEW_ROUTE = - '/settings/contact-list/my-accounts/view'; -const CONTACT_MY_ACCOUNTS_EDIT_ROUTE = - '/settings/contact-list/my-accounts/edit'; const REVEAL_SEED_ROUTE = '/seed'; const MOBILE_SYNC_ROUTE = '/mobile-sync'; const RESTORE_VAULT_ROUTE = '/restore-vault'; @@ -85,9 +80,6 @@ const PATH_NAME_MAP = { [`${CONTACT_EDIT_ROUTE}/:address`]: 'Edit Contact Settings Page', [CONTACT_ADD_ROUTE]: 'Add Contact Settings Page', [`${CONTACT_VIEW_ROUTE}/:address`]: 'View Contact Settings Page', - [CONTACT_MY_ACCOUNTS_ROUTE]: 'My Accounts List Settings Page', - [`${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/:account`]: 'View Account Settings Page', - [`${CONTACT_MY_ACCOUNTS_EDIT_ROUTE}/:account`]: 'Edit Account Settings Page', [REVEAL_SEED_ROUTE]: 'Reveal Seed Page', [MOBILE_SYNC_ROUTE]: 'Sync With Mobile Page', [RESTORE_VAULT_ROUTE]: 'Restore Vault Page', @@ -183,9 +175,6 @@ export { CONTACT_EDIT_ROUTE, CONTACT_ADD_ROUTE, CONTACT_VIEW_ROUTE, - CONTACT_MY_ACCOUNTS_ROUTE, - CONTACT_MY_ACCOUNTS_VIEW_ROUTE, - CONTACT_MY_ACCOUNTS_EDIT_ROUTE, NETWORKS_ROUTE, NETWORKS_FORM_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, 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 58% 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..cf2f1ee6f 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 @@ -1,8 +1,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: { @@ -18,21 +17,21 @@ const mockState = { }, }; -describe('withModalProps', function () { - it('should return a component wrapped with modal state props', function () { +describe('withModalProps', () => { + it('should return a component wrapped with modal state props', () => { const TestComponent = () =>
    Testing
    ; const WrappedComponent = withModalProps(TestComponent); const store = configureMockStore()(mockState); const wrapper = mount(); - assert.ok(wrapper); + expect(wrapper).toHaveLength(1); const testComponent = wrapper.find(TestComponent).at(0); - assert.strictEqual(testComponent.length, 1); - assert.strictEqual(testComponent.find('.test').text(), 'Testing'); + expect(testComponent).toHaveLength(1); + expect(testComponent.find('.test').text()).toStrictEqual('Testing'); const testComponentProps = testComponent.props(); - assert.strictEqual(testComponentProps.prop1, 'prop1'); - assert.strictEqual(testComponentProps.prop2, 2); - assert.strictEqual(testComponentProps.prop3, true); - assert.strictEqual(typeof testComponentProps.hideModal, 'function'); + expect(testComponentProps.prop1).toStrictEqual('prop1'); + expect(testComponentProps.prop2).toStrictEqual(2); + expect(testComponentProps.prop3).toStrictEqual(true); + expect(typeof testComponentProps.hideModal).toStrictEqual('function'); }); }); diff --git a/ui/app/helpers/utils/common.util.test.js b/ui/app/helpers/utils/common.util.test.js index 50a50d47b..84e5c22b2 100644 --- a/ui/app/helpers/utils/common.util.test.js +++ b/ui/app/helpers/utils/common.util.test.js @@ -1,9 +1,8 @@ -import assert from 'assert'; import * as utils from './common.util'; -describe('Common utils', function () { - describe('camelCaseToCapitalize', function () { - it('should return a capitalized string from a camel-cased string', function () { +describe('Common utils', () => { + describe('camelCaseToCapitalize', () => { + it('should return a capitalized string from a camel-cased string', () => { const tests = [ { test: undefined, @@ -20,7 +19,7 @@ describe('Common utils', function () { ]; tests.forEach(({ test, expected }) => { - assert.strictEqual(utils.camelCaseToCapitalize(test), expected); + expect(utils.camelCaseToCapitalize(test)).toStrictEqual(expected); }); }); }); diff --git a/ui/app/helpers/utils/confirm-tx.util.test.js b/ui/app/helpers/utils/confirm-tx.util.test.js index e4e6287da..2dc8af0ce 100644 --- a/ui/app/helpers/utils/confirm-tx.util.test.js +++ b/ui/app/helpers/utils/confirm-tx.util.test.js @@ -1,60 +1,57 @@ -import assert from 'assert'; import * as utils from './confirm-tx.util'; -describe('Confirm Transaction utils', function () { - describe('increaseLastGasPrice', function () { - it('should increase the gasPrice by 10%', function () { +describe('Confirm Transaction utils', () => { + describe('increaseLastGasPrice', () => { + it('should increase the gasPrice by 10%', () => { const increasedGasPrice = utils.increaseLastGasPrice('0xa'); - assert.strictEqual(increasedGasPrice, '0xb'); + expect(increasedGasPrice).toStrictEqual('0xb'); }); - it('should prefix the result with 0x', function () { + it('should prefix the result with 0x', () => { const increasedGasPrice = utils.increaseLastGasPrice('a'); - assert.strictEqual(increasedGasPrice, '0xb'); + expect(increasedGasPrice).toStrictEqual('0xb'); }); }); - describe('hexGreaterThan', function () { - it('should return true if the first value is greater than the second value', function () { - assert.strictEqual(utils.hexGreaterThan('0xb', '0xa'), true); + describe('hexGreaterThan', () => { + it('should return true if the first value is greater than the second value', () => { + expect(utils.hexGreaterThan('0xb', '0xa')).toStrictEqual(true); }); - it('should return false if the first value is less than the second value', function () { - assert.strictEqual(utils.hexGreaterThan('0xa', '0xb'), false); + it('should return false if the first value is less than the second value', () => { + expect(utils.hexGreaterThan('0xa', '0xb')).toStrictEqual(false); }); - it('should return false if the first value is equal to the second value', function () { - assert.strictEqual(utils.hexGreaterThan('0xa', '0xa'), false); + it('should return false if the first value is equal to the second value', () => { + expect(utils.hexGreaterThan('0xa', '0xa')).toStrictEqual(false); }); - it('should correctly compare prefixed and non-prefixed hex values', function () { - assert.strictEqual(utils.hexGreaterThan('0xb', 'a'), true); + it('should correctly compare prefixed and non-prefixed hex values', () => { + expect(utils.hexGreaterThan('0xb', 'a')).toStrictEqual(true); }); }); - describe('getHexGasTotal', function () { - it('should multiply the hex gasLimit and hex gasPrice values together', function () { - assert.strictEqual( + describe('getHexGasTotal', () => { + it('should multiply the hex gasLimit and hex gasPrice values together', () => { + expect( utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }), - '0x1319718a5000', - ); + ).toStrictEqual('0x1319718a5000'); }); - it('should prefix the result with 0x', function () { - assert.strictEqual( + it('should prefix the result with 0x', () => { + expect( utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }), - '0x1319718a5000', - ); + ).toStrictEqual('0x1319718a5000'); }); }); - describe('addEth', function () { - it('should add two values together rounding to 6 decimal places', function () { - assert.strictEqual(utils.addEth('0.12345678', '0'), '0.123457'); + describe('addEth', () => { + it('should add two values together rounding to 6 decimal places', () => { + expect(utils.addEth('0.12345678', '0')).toStrictEqual('0.123457'); }); - it('should add any number of values together rounding to 6 decimal places', function () { - assert.strictEqual( + it('should add any number of values together rounding to 6 decimal places', () => { + expect( utils.addEth( '0.1', '0.02', @@ -64,18 +61,17 @@ describe('Confirm Transaction utils', function () { '0.000006', '0.0000007', ), - '0.123457', - ); + ).toStrictEqual('0.123457'); }); }); - describe('addFiat', function () { - it('should add two values together rounding to 2 decimal places', function () { - assert.strictEqual(utils.addFiat('0.12345678', '0'), '0.12'); + describe('addFiat', () => { + it('should add two values together rounding to 2 decimal places', () => { + expect(utils.addFiat('0.12345678', '0')).toStrictEqual('0.12'); }); - it('should add any number of values together rounding to 2 decimal places', function () { - assert.strictEqual( + it('should add any number of values together rounding to 2 decimal places', () => { + expect( utils.addFiat( '0.1', '0.02', @@ -85,13 +81,12 @@ describe('Confirm Transaction utils', function () { '0.000006', '0.0000007', ), - '0.12', - ); + ).toStrictEqual('0.12'); }); }); - describe('getValueFromWeiHex', function () { - it('should get the transaction amount in ETH', function () { + describe('getValueFromWeiHex', () => { + it('should get the transaction amount in ETH', () => { const ethTransactionAmount = utils.getValueFromWeiHex({ value: '0xde0b6b3a7640000', toCurrency: 'ETH', @@ -99,10 +94,10 @@ describe('Confirm Transaction utils', function () { numberOfDecimals: 6, }); - assert.strictEqual(ethTransactionAmount, '1'); + expect(ethTransactionAmount).toStrictEqual('1'); }); - it('should get the transaction amount in fiat', function () { + it('should get the transaction amount in fiat', () => { const fiatTransactionAmount = utils.getValueFromWeiHex({ value: '0xde0b6b3a7640000', toCurrency: 'usd', @@ -110,12 +105,12 @@ describe('Confirm Transaction utils', function () { numberOfDecimals: 2, }); - assert.strictEqual(fiatTransactionAmount, '468.58'); + expect(fiatTransactionAmount).toStrictEqual('468.58'); }); }); - describe('getTransactionFee', function () { - it('should get the transaction fee in ETH', function () { + describe('getTransactionFee', () => { + it('should get the transaction fee in ETH', () => { const ethTransactionFee = utils.getTransactionFee({ value: '0x1319718a5000', toCurrency: 'ETH', @@ -123,10 +118,10 @@ describe('Confirm Transaction utils', function () { numberOfDecimals: 6, }); - assert.strictEqual(ethTransactionFee, '0.000021'); + expect(ethTransactionFee).toStrictEqual('0.000021'); }); - it('should get the transaction fee in fiat', function () { + it('should get the transaction fee in fiat', () => { const fiatTransactionFee = utils.getTransactionFee({ value: '0x1319718a5000', toCurrency: 'usd', @@ -134,14 +129,14 @@ describe('Confirm Transaction utils', function () { numberOfDecimals: 2, }); - assert.strictEqual(fiatTransactionFee, '0.01'); + expect(fiatTransactionFee).toStrictEqual('0.01'); }); }); - describe('formatCurrency', function () { - it('should format USD values', function () { + describe('formatCurrency', () => { + it('should format USD values', () => { const value = utils.formatCurrency('123.45', 'usd'); - assert.strictEqual(value, '$123.45'); + expect(value).toStrictEqual('$123.45'); }); }); }); diff --git a/ui/app/helpers/utils/conversion-util.js b/ui/app/helpers/utils/conversion-util.js index fac0dd415..3d61f4410 100644 --- a/ui/app/helpers/utils/conversion-util.js +++ b/ui/app/helpers/utils/conversion-util.js @@ -23,9 +23,7 @@ import BigNumber from 'bignumber.js'; -import ethUtil, { stripHexPrefix } from 'ethereumjs-util'; - -const { BN } = ethUtil; +import { stripHexPrefix, BN } from 'ethereumjs-util'; // Big Number Constants const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000'); diff --git a/ui/app/helpers/utils/conversion-util.test.js b/ui/app/helpers/utils/conversion-util.test.js index bb1f2a690..f97640691 100644 --- a/ui/app/helpers/utils/conversion-util.test.js +++ b/ui/app/helpers/utils/conversion-util.test.js @@ -1,36 +1,35 @@ -import assert from 'assert'; import BigNumber from 'bignumber.js'; import { addCurrencies, conversionUtil } from './conversion-util'; -describe('conversion utils', function () { - describe('addCurrencies()', function () { - it('add whole numbers', function () { +describe('conversion utils', () => { + describe('addCurrencies()', () => { + it('add whole numbers', () => { const result = addCurrencies(3, 9, { aBase: 10, bBase: 10, }); - assert.strictEqual(result.toNumber(), 12); + expect(result.toNumber()).toStrictEqual(12); }); - it('add decimals', function () { + it('add decimals', () => { const result = addCurrencies(1.3, 1.9, { aBase: 10, bBase: 10, }); - assert.strictEqual(result.toNumber(), 3.2); + expect(result.toNumber()).toStrictEqual(3.2); }); - it('add repeating decimals', function () { + it('add repeating decimals', () => { const result = addCurrencies(1 / 3, 1 / 9, { aBase: 10, bBase: 10, }); - assert.strictEqual(result.toNumber(), 0.4444444444444444); + expect(result.toNumber()).toStrictEqual(0.4444444444444444); }); }); - describe('conversionUtil', function () { - it('Returns expected types', function () { + describe('conversionUtil', () => { + it('returns expected types', () => { const conv1 = conversionUtil(1000000000000000000, { fromNumericBase: 'dec', toNumericBase: 'hex', @@ -40,102 +39,89 @@ describe('conversion utils', function () { fromDenomination: 'ETH', toDenomination: 'WEI', }); - assert( - typeof conv1 === 'string', - 'conversion 1 should return type string', - ); - assert(conv2 instanceof BigNumber, 'conversion 2 should be a BigNumber'); + expect(typeof conv1 === 'string').toStrictEqual(true); + expect(conv2 instanceof BigNumber).toStrictEqual(true); }); - it('Converts from dec to hex', function () { - assert.strictEqual( + it('converts from dec to hex', () => { + expect( conversionUtil('1000000000000000000', { fromNumericBase: 'dec', toNumericBase: 'hex', }), - 'de0b6b3a7640000', - ); - assert.strictEqual( + ).toStrictEqual('de0b6b3a7640000'); + expect( conversionUtil('1500000000000000000', { fromNumericBase: 'dec', toNumericBase: 'hex', }), - '14d1120d7b160000', - ); + ).toStrictEqual('14d1120d7b160000'); }); - it('Converts hex formatted numbers to dec', function () { - assert.strictEqual( + it('converts hex formatted numbers to dec', () => { + expect( conversionUtil('0xde0b6b3a7640000', { fromNumericBase: 'hex', toNumericBase: 'dec', }), - '1000000000000000000', - ); - assert.strictEqual( + ).toStrictEqual('1000000000000000000'); + expect( conversionUtil('0x14d1120d7b160000', { fromNumericBase: 'hex', toNumericBase: 'dec', }), - '1500000000000000000', - ); + ).toStrictEqual('1500000000000000000'); }); - it('Converts WEI to ETH', function () { - assert.strictEqual( + it('converts WEI to ETH', () => { + expect( conversionUtil('0xde0b6b3a7640000', { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', toDenomination: 'ETH', }), - '1', - ); - assert.strictEqual( + ).toStrictEqual('1'); + expect( conversionUtil('0x14d1120d7b160000', { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', toDenomination: 'ETH', }), - '1.5', - ); + ).toStrictEqual('1.5'); }); - it('Converts ETH to WEI', function () { - assert.strictEqual( + it('converts ETH to WEI', () => { + expect( conversionUtil('1', { fromNumericBase: 'dec', fromDenomination: 'ETH', toDenomination: 'WEI', }).toNumber(), - 1000000000000000000, - ); - assert.strictEqual( + ).toStrictEqual(1000000000000000000); + expect( conversionUtil('1.5', { fromNumericBase: 'dec', fromDenomination: 'ETH', toDenomination: 'WEI', }).toNumber(), - 1500000000000000000, - ); + ).toStrictEqual(1500000000000000000); }); - it('Converts ETH to GWEI', function () { - assert.strictEqual( + it('converts ETH to GWEI', () => { + expect( conversionUtil('1', { fromNumericBase: 'dec', fromDenomination: 'ETH', toDenomination: 'GWEI', }).toNumber(), - 1000000000, - ); - assert.strictEqual( + ).toStrictEqual(1000000000); + expect( conversionUtil('1.5', { fromNumericBase: 'dec', fromDenomination: 'ETH', toDenomination: 'GWEI', }).toNumber(), - 1500000000, - ); + ).toStrictEqual(1500000000); }); - it('Converts ETH to USD', function () { - assert.strictEqual( + it('converts ETH to USD', () => { + expect( conversionUtil('1', { fromNumericBase: 'dec', toNumericBase: 'dec', @@ -143,9 +129,8 @@ describe('conversion utils', function () { conversionRate: 468.58, numberOfDecimals: 2, }), - '468.58', - ); - assert.strictEqual( + ).toStrictEqual('468.58'); + expect( conversionUtil('1.5', { fromNumericBase: 'dec', toNumericBase: 'dec', @@ -153,11 +138,10 @@ describe('conversion utils', function () { conversionRate: 468.58, numberOfDecimals: 2, }), - '702.87', - ); + ).toStrictEqual('702.87'); }); - it('Converts USD to ETH', function () { - assert.strictEqual( + it('converts USD to ETH', () => { + expect( conversionUtil('468.58', { fromNumericBase: 'dec', toNumericBase: 'dec', @@ -166,9 +150,8 @@ describe('conversion utils', function () { numberOfDecimals: 2, invertConversionRate: true, }), - '1', - ); - assert.strictEqual( + ).toStrictEqual('1'); + expect( conversionUtil('702.87', { fromNumericBase: 'dec', toNumericBase: 'dec', @@ -177,8 +160,7 @@ describe('conversion utils', function () { numberOfDecimals: 2, invertConversionRate: true, }), - '1.5', - ); + ).toStrictEqual('1.5'); }); }); }); diff --git a/ui/app/helpers/utils/conversions.util.test.js b/ui/app/helpers/utils/conversions.util.test.js index 6fe4159ea..1947975a6 100644 --- a/ui/app/helpers/utils/conversions.util.test.js +++ b/ui/app/helpers/utils/conversions.util.test.js @@ -1,43 +1,42 @@ -import assert from 'assert'; import { ETH } from '../constants/common'; import * as utils from './conversions.util'; -describe('conversion utils', function () { - describe('getWeiHexFromDecimalValue', function () { - it('should correctly convert 0 in ETH', function () { +describe('conversion utils', () => { + describe('getWeiHexFromDecimalValue', () => { + it('should correctly convert 0 in ETH', () => { const weiValue = utils.getWeiHexFromDecimalValue({ value: '0', fromCurrency: ETH, fromDenomination: ETH, }); - assert.strictEqual(weiValue, '0'); + expect(weiValue).toStrictEqual('0'); }); }); - describe('decETHToDecWEI', function () { - it('should correctly convert 1 ETH to WEI', function () { + describe('decETHToDecWEI', () => { + it('should correctly convert 1 ETH to WEI', () => { const weiValue = utils.decETHToDecWEI('1'); - assert.strictEqual(weiValue, '1000000000000000000'); + expect(weiValue).toStrictEqual('1000000000000000000'); }); - it('should correctly convert 0.000000000000000001 ETH to WEI', function () { + it('should correctly convert 0.000000000000000001 ETH to WEI', () => { const weiValue = utils.decETHToDecWEI('0.000000000000000001'); - assert.strictEqual(weiValue, '1'); + expect(weiValue).toStrictEqual('1'); }); - it('should correctly convert 1000000.000000000000000001 ETH to WEI', function () { + it('should correctly convert 1000000.000000000000000001 ETH to WEI', () => { const weiValue = utils.decETHToDecWEI('1000000.000000000000000001'); - assert.strictEqual(weiValue, '1000000000000000000000001'); + expect(weiValue).toStrictEqual('1000000000000000000000001'); }); - it('should correctly convert 9876.543210 ETH to WEI', function () { + it('should correctly convert 9876.543210 ETH to WEI', () => { const weiValue = utils.decETHToDecWEI('9876.543210'); - assert.strictEqual(weiValue, '9876543210000000000000'); + expect(weiValue).toStrictEqual('9876543210000000000000'); }); - it('should correctly convert 1.0000000000000000 ETH to WEI', function () { + it('should correctly convert 1.0000000000000000 ETH to WEI', () => { const weiValue = utils.decETHToDecWEI('1.0000000000000000'); - assert.strictEqual(weiValue, '1000000000000000000'); + expect(weiValue).toStrictEqual('1000000000000000000'); }); }); }); diff --git a/ui/app/helpers/utils/fetch-with-cache.test.js b/ui/app/helpers/utils/fetch-with-cache.test.js index 0b6dc3da3..76e295a77 100644 --- a/ui/app/helpers/utils/fetch-with-cache.test.js +++ b/ui/app/helpers/utils/fetch-with-cache.test.js @@ -1,24 +1,22 @@ -import assert from 'assert'; import nock from 'nock'; import sinon from 'sinon'; -import proxyquire from 'proxyquire'; -const fakeStorage = {}; -const fetchWithCache = proxyquire('./fetch-with-cache', { - '../../../lib/storage-helpers': fakeStorage, -}).default; +import { getStorageItem, setStorageItem } from '../../../lib/storage-helpers'; -describe('Fetch with cache', function () { - beforeEach(function () { - fakeStorage.getStorageItem = sinon.stub(); - fakeStorage.setStorageItem = sinon.stub(); - }); - afterEach(function () { +jest.mock('../../../lib/storage-helpers.js', () => ({ + getStorageItem: jest.fn(), + setStorageItem: jest.fn(), +})); + +const fetchWithCache = require('./fetch-with-cache').default; + +describe('Fetch with cache', () => { + afterEach(() => { sinon.restore(); nock.cleanAll(); }); - it('fetches a url', async function () { + it('fetches a url', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(200, '{"average": 1}'); @@ -26,17 +24,17 @@ describe('Fetch with cache', function () { const response = await fetchWithCache( 'https://fetchwithcache.metamask.io/price', ); - assert.deepStrictEqual(response, { + expect(response).toStrictEqual({ average: 1, }); }); - it('returns cached response', async function () { + it('returns cached response', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(200, '{"average": 2}'); - fakeStorage.getStorageItem.returns({ + getStorageItem.mockReturnValueOnce({ cachedResponse: { average: 1 }, cachedTime: Date.now(), }); @@ -44,17 +42,17 @@ describe('Fetch with cache', function () { const response = await fetchWithCache( 'https://fetchwithcache.metamask.io/price', ); - assert.deepStrictEqual(response, { + expect(response).toStrictEqual({ average: 1, }); }); - it('fetches URL again after cache refresh time has passed', async function () { + it('fetches URL again after cache refresh time has passed', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(200, '{"average": 3}'); - fakeStorage.getStorageItem.returns({ + getStorageItem.mockReturnValueOnce({ cachedResponse: { average: 1 }, cachedTime: Date.now() - 1000, }); @@ -64,85 +62,84 @@ describe('Fetch with cache', function () { {}, { cacheRefreshTime: 123 }, ); - assert.deepStrictEqual(response, { + expect(response).toStrictEqual({ average: 3, }); }); - it('should abort the request when the custom timeout is hit', async function () { + it('should abort the request when the custom timeout is hit', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .delay(100) .reply(200, '{"average": 4}'); - await assert.rejects( - () => - fetchWithCache( - 'https://fetchwithcache.metamask.io/price', - {}, - { timeout: 20 }, - ), - { name: 'AbortError', message: 'Aborted' }, - ); + await expect(() => + fetchWithCache( + 'https://fetchwithcache.metamask.io/price', + {}, + { timeout: 20 }, + ), + ).rejects.toThrow({ name: 'AbortError', message: 'Aborted' }); }); - it('throws when the response is unsuccessful', async function () { + it('throws when the response is unsuccessful', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(500, '{"average": 6}'); - await assert.rejects(() => + await expect(() => fetchWithCache('https://fetchwithcache.metamask.io/price'), - ); + ).rejects.toThrow(''); }); - it('throws when a POST request is attempted', async function () { + it('throws when a POST request is attempted', async () => { nock('https://fetchwithcache.metamask.io') .post('/price') .reply(200, '{"average": 7}'); - await assert.rejects(() => + await expect(() => fetchWithCache('https://fetchwithcache.metamask.io/price', { method: 'POST', }), - ); + ).rejects.toThrow(''); }); - it('throws when the request has a truthy body', async function () { + it('throws when the request has a truthy body', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(200, '{"average": 8}'); - await assert.rejects(() => + await expect(() => fetchWithCache('https://fetchwithcache.metamask.io/price', { body: 1 }), - ); + ).rejects.toThrow(''); }); - it('throws when the request has an invalid Content-Type header', async function () { + it('throws when the request has an invalid Content-Type header', async () => { nock('https://fetchwithcache.metamask.io') .get('/price') .reply(200, '{"average": 9}'); - await assert.rejects( - () => - fetchWithCache('https://fetchwithcache.metamask.io/price', { - headers: { 'Content-Type': 'text/plain' }, - }), - { message: 'fetchWithCache only supports JSON responses' }, - ); + await expect(() => + fetchWithCache('https://fetchwithcache.metamask.io/price', { + headers: { 'Content-Type': 'text/plain' }, + }), + ).rejects.toThrow({ + message: 'fetchWithCache only supports JSON responses', + }); }); - it('should correctly cache responses from interwoven requests', async function () { + it('should correctly cache responses from interwoven requests', async () => { nock('https://fetchwithcache.metamask.io') .get('/foo') .reply(200, '{"average": 9}'); + nock('https://fetchwithcache.metamask.io') .get('/bar') .reply(200, '{"average": 9}'); const testCache = {}; - fakeStorage.getStorageItem.callsFake((key) => testCache[key]); - fakeStorage.setStorageItem.callsFake((key, value) => { + getStorageItem.mockImplementation((key) => testCache[key]); + setStorageItem.mockImplementation((key, value) => { testCache[key] = value; }); @@ -159,15 +156,13 @@ describe('Fetch with cache', function () { ), ]); - assert.deepStrictEqual( + expect( testCache['cachedFetch:https://fetchwithcache.metamask.io/foo'] .cachedResponse, - { average: 9 }, - ); - assert.deepStrictEqual( + ).toStrictEqual({ average: 9 }); + expect( testCache['cachedFetch:https://fetchwithcache.metamask.io/bar'] .cachedResponse, - { average: 9 }, - ); + ).toStrictEqual({ average: 9 }); }); }); diff --git a/ui/app/helpers/utils/i18n-helper.test.js b/ui/app/helpers/utils/i18n-helper.test.js index d02053315..c83dad0f1 100644 --- a/ui/app/helpers/utils/i18n-helper.test.js +++ b/ui/app/helpers/utils/i18n-helper.test.js @@ -1,9 +1,8 @@ -import assert from 'assert'; import React from 'react'; import { shallow } from 'enzyme'; import { getMessage } from './i18n-helper'; -describe('i18n helper', function () { +describe('i18n helper', () => { const TEST_LOCALE_CODE = 'TEST_LOCALE_CODE'; const TEST_KEY_1 = 'TEST_KEY_1'; @@ -97,29 +96,27 @@ describe('i18n helper', function () {
    ); - describe('getMessage', function () { - it('should return the exact message paired with key if there are no substitutions', function () { + describe('getMessage', () => { + it('should return the exact message paired with key if there are no substitutions', () => { const result = t(TEST_KEY_1); - assert.strictEqual(result, 'This is a simple message.'); + expect(result).toStrictEqual('This is a simple message.'); }); - it('should return the correct message when a single non-react substitution is made', function () { + it('should return the correct message when a single non-react substitution is made', () => { const result = t(TEST_KEY_2, [TEST_SUBSTITUTION_1]); - assert.strictEqual( - result, + expect(result).toStrictEqual( `This is a message with a single non-react substitution ${TEST_SUBSTITUTION_1}.`, ); }); - it('should return the correct message when two non-react substitutions are made', function () { + it('should return the correct message when two non-react substitutions are made', () => { const result = t(TEST_KEY_3, [TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2]); - assert.strictEqual( - result, + expect(result).toStrictEqual( `This is a message with two non-react substitutions ${TEST_SUBSTITUTION_1} and ${TEST_SUBSTITUTION_2}.`, ); }); - it('should return the correct message when multiple non-react substitutions are made', function () { + it('should return the correct message when multiple non-react substitutions are made', () => { const result = t(TEST_KEY_4, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_2, @@ -127,50 +124,46 @@ describe('i18n helper', function () { TEST_SUBSTITUTION_4, TEST_SUBSTITUTION_5, ]); - assert.strictEqual( - result, + expect(result).toStrictEqual( `${TEST_SUBSTITUTION_1} - ${TEST_SUBSTITUTION_2} - ${TEST_SUBSTITUTION_3} - ${TEST_SUBSTITUTION_4} - ${TEST_SUBSTITUTION_5}`, ); }); - it('should correctly render falsey substitutions', function () { + it('should correctly render falsey substitutions', () => { const result = t(TEST_KEY_4, [0, -0, '', false, NaN]); - assert.strictEqual(result, '0 - 0 - - false - NaN'); + expect(result).toStrictEqual('0 - 0 - - false - NaN'); }); - it('should render nothing for "null" and "undefined" substitutions', function () { + it('should render nothing for "null" and "undefined" substitutions', () => { const result = t(TEST_KEY_5, [null, TEST_SUBSTITUTION_2]); - assert.strictEqual(result, ` - ${TEST_SUBSTITUTION_2} - `); + expect(result).toStrictEqual(` - ${TEST_SUBSTITUTION_2} - `); }); - it('should return the correct message when a single react substitution is made', function () { + it('should return the correct message when a single react substitution is made', () => { const result = t(TEST_KEY_6, [TEST_SUBSTITUTION_6]); - assert.strictEqual( - shallow(result).html(), + expect(shallow(result).html()).toStrictEqual( ' Testing a react substitution
    TEST_SUBSTITUTION_1
    .
    ', ); }); - it('should return the correct message when two react substitutions are made', function () { + it('should return the correct message when two react substitutions are made', () => { const result = t(TEST_KEY_7, [ TEST_SUBSTITUTION_7_1, TEST_SUBSTITUTION_7_2, ]); - assert.strictEqual( - shallow(result).html(), + expect(shallow(result).html()).toStrictEqual( ' Testing a react substitution
    TEST_SUBSTITUTION_1
    and another
    TEST_SUBSTITUTION_2
    .
    ', ); }); - it('should return the correct message when substituting a mix of react elements and strings', function () { + it('should return the correct message when substituting a mix of react elements and strings', () => { const result = t(TEST_KEY_8, [ TEST_SUBSTITUTION_1, TEST_SUBSTITUTION_8_1, TEST_SUBSTITUTION_2, TEST_SUBSTITUTION_8_2, ]); - assert.strictEqual( - shallow(result).html(), + expect(shallow(result).html()).toStrictEqual( ' Testing a mix TEST_SUBSTITUTION_1 of react substitutions
    TEST_SUBSTITUTION_3
    and string substitutions TEST_SUBSTITUTION_2 +
    TEST_SUBSTITUTION_4
    .
    ', ); }); 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..9af77ef0f 100644 --- a/ui/app/helpers/utils/transactions.util.test.js +++ b/ui/app/helpers/utils/transactions.util.test.js @@ -1,33 +1,32 @@ -import assert from 'assert'; import { - TRANSACTION_CATEGORIES, + TRANSACTION_TYPES, TRANSACTION_GROUP_STATUSES, TRANSACTION_STATUSES, } from '../../../../shared/constants/transaction'; import * as utils from './transactions.util'; -describe('Transactions utils', function () { - describe('getTokenData', function () { - it('should return token data', function () { +describe('Transactions utils', () => { + describe('getTokenData', () => { + it('should return token data', () => { const tokenData = utils.getTokenData( '0xa9059cbb00000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000004e20', ); - assert.ok(tokenData); + expect(tokenData).toStrictEqual(expect.anything()); const { name, args } = tokenData; - assert.strictEqual(name, TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER); + expect(name).toStrictEqual(TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER); const to = args._to; const value = args._value.toString(); - assert.strictEqual(to, '0x50A9D56C2B8BA9A5c7f2C08C3d26E0499F23a706'); - assert.strictEqual(value, '20000'); + expect(to).toStrictEqual('0x50A9D56C2B8BA9A5c7f2C08C3d26E0499F23a706'); + expect(value).toStrictEqual('20000'); }); - it('should not throw errors when called without arguments', function () { - assert.doesNotThrow(() => utils.getTokenData()); + it('should not throw errors when called without arguments', () => { + expect(() => utils.getTokenData()).not.toThrow(); }); }); - describe('getStatusKey', function () { - it('should return the correct status', function () { + describe('getStatusKey', () => { + it('should return the correct status', () => { const tests = [ { transaction: { @@ -56,7 +55,7 @@ describe('Transactions utils', function () { ]; tests.forEach(({ transaction, expected }) => { - assert.strictEqual(utils.getStatusKey(transaction), expected); + expect(utils.getStatusKey(transaction)).toStrictEqual(expected); }); }); }); diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index 1480781c0..64c11a135 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -1,9 +1,17 @@ import punycode from 'punycode/punycode'; import abi from 'human-standard-token-abi'; import BigNumber from 'bignumber.js'; -import ethUtil from 'ethereumjs-util'; +import * as 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") { @@ -21,33 +29,19 @@ export function formatDateWithYearContext( now.year === dateTime.year ? formatThisYear : fallback, ); } - -const valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -}; -const bnTable = {}; -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; } @@ -89,6 +83,9 @@ export function isValidAddress(address) { return false; } const prefixed = addHexPrefix(address); + if (!isHex(prefixed)) { + return false; + } return ( (isAllOneCase(prefixed.slice(2)) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) @@ -185,84 +182,6 @@ export function formatBalance( return formatted; } -export function generateBalanceObject(formattedBalance, decimalsToKeep = 1) { - let balance = formattedBalance.split(' ')[0]; - const label = formattedBalance.split(' ')[1]; - const beforeDecimal = balance.split('.')[0]; - const afterDecimal = balance.split('.')[1]; - const shortBalance = shortenBalance(balance, decimalsToKeep); - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0'; - } else { - balance = '<1.0e-5'; - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}`; - } - - return { balance, label, shortBalance }; -} - -export function shortenBalance(balance, decimalsToKeep = 1) { - let truncatedValue; - const convertedBalance = parseFloat(balance); - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep); - return `${truncatedValue}m`; - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep); - return `${truncatedValue}k`; - } else if (convertedBalance === 0) { - return '0'; - } else if (convertedBalance < 0.001) { - return '<0.001'; - } else if (convertedBalance < 1) { - const stringBalance = convertedBalance.toString(); - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3); - } - return stringBalance; - } - return convertedBalance.toFixed(decimalsToKeep); -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -export function normalizeToWei(amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]); - } catch (e) { - return amount; - } -} - -export function normalizeEthStringToWei(str) { - const parts = str.split('.'); - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei); - if (parts[1]) { - let decimal = parts[1]; - while (decimal.length < 18) { - decimal += '0'; - } - if (decimal.length > 18) { - decimal = decimal.slice(0, 18); - } - const decimalBN = new ethUtil.BN(decimal, 10); - eth = eth.add(decimalBN); - } - return eth; -} - -const multiple = new ethUtil.BN('10000', 10); -export function normalizeNumberToWei(n, currency) { - const enlarged = n * 10000; - const amount = new ethUtil.BN(String(enlarged), 10); - return normalizeToWei(amount, currency).div(multiple); -} - export function isHex(str) { return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/u)); } diff --git a/ui/app/helpers/utils/util.test.js b/ui/app/helpers/utils/util.test.js index fc79ef3b0..0dfa431f6 100644 --- a/ui/app/helpers/utils/util.test.js +++ b/ui/app/helpers/utils/util.test.js @@ -1,378 +1,293 @@ -import assert from 'assert'; -import ethUtil from 'ethereumjs-util'; +import { BN, toChecksumAddress } from 'ethereumjs-util'; import * as util from './util'; -describe('util', function () { +describe('util', () => { let ethInWei = '1'; for (let i = 0; i < 18; i++) { ethInWei += '0'; } - describe('#parseBalance', function () { - it('should render 0.01 eth correctly', function () { + describe('#parseBalance', () => { + it('should render 0.01 eth correctly', () => { const input = '0x2386F26FC10000'; const output = util.parseBalance(input); - assert.deepStrictEqual(output, ['0', '01']); + expect(output).toStrictEqual(['0', '01']); }); - it('should render 12.023 eth correctly', function () { + it('should render 12.023 eth correctly', () => { const input = 'A6DA46CCA6858000'; const output = util.parseBalance(input); - assert.deepStrictEqual(output, ['12', '023']); + expect(output).toStrictEqual(['12', '023']); }); - it('should render 0.0000000342422 eth correctly', function () { + it('should render 0.0000000342422 eth correctly', () => { const input = '0x7F8FE81C0'; const output = util.parseBalance(input); - assert.deepStrictEqual(output, ['0', '0000000342422']); + expect(output).toStrictEqual(['0', '0000000342422']); }); - it('should render 0 eth correctly', function () { + it('should render 0 eth correctly', () => { const input = '0x0'; const output = util.parseBalance(input); - assert.deepStrictEqual(output, ['0', '0']); + expect(output).toStrictEqual(['0', '0']); }); }); - describe('#addressSummary', function () { - it('should add case-sensitive checksum', function () { + describe('#addressSummary', () => { + it('should add case-sensitive checksum', () => { const address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = util.addressSummary(address); - assert.strictEqual(result, '0xFDEa65C8...b825'); + expect(result).toStrictEqual('0xFDEa65C8...b825'); }); - it('should accept arguments for firstseg, lastseg, and keepPrefix', function () { + it('should accept arguments for firstseg, lastseg, and keepPrefix', () => { const address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = util.addressSummary(address, 4, 4, false); - assert.strictEqual(result, 'FDEa...b825'); + expect(result).toStrictEqual('FDEa...b825'); }); }); - describe('#isValidAddress', function () { - it('should allow 40-char non-prefixed hex', function () { + describe('#isValidAddress', () => { + it('should allow 40-char non-prefixed hex', () => { const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = util.isValidAddress(address); - assert.ok(result); + expect(result).toStrictEqual(true); }); - it('should allow 42-char non-prefixed hex', function () { + it('should allow 42-char non-prefixed hex', () => { const address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'; const result = util.isValidAddress(address); - assert.ok(result); + expect(result).toStrictEqual(true); }); - it('should not allow less non hex-prefixed', function () { + it('should not allow less non hex-prefixed', () => { const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85'; const result = util.isValidAddress(address); - assert.ok(!result); + expect(result).toStrictEqual(false); }); - it('should not allow less hex-prefixed', function () { + it('should not allow less hex-prefixed', () => { const address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85'; const result = util.isValidAddress(address); - assert.ok(!result); + expect(result).toStrictEqual(false); }); - it('should recognize correct capitalized checksum', function () { + it('should recognize correct capitalized checksum', () => { const address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825'; const result = util.isValidAddress(address); - assert.ok(result); + expect(result).toStrictEqual(true); }); - it('should recognize incorrect capitalized checksum', function () { + it('should recognize incorrect capitalized checksum', () => { const address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825'; const result = util.isValidAddress(address); - assert.ok(!result); + expect(result).toStrictEqual(false); }); - it('should recognize this sample hashed address', function () { + it('should recognize this sample hashed address', () => { const address = '0x5Fda30Bb72B8Dfe20e48A00dFc108d0915BE9Bb0'; const result = util.isValidAddress(address); - const hashed = ethUtil.toChecksumAddress(address.toLowerCase()); - assert.strictEqual(hashed, address, 'example is hashed correctly'); - assert.ok(result, 'is valid by our check'); + const hashed = toChecksumAddress(address.toLowerCase()); + expect(hashed).toStrictEqual(address); + expect(result).toStrictEqual(true); }); }); - describe('isValidDomainName', function () { - it('should return true when given a valid domain name', function () { - assert.strictEqual(util.isValidDomainName('foo.bar'), true); + describe('isValidDomainName', () => { + it('should return true when given a valid domain name', () => { + expect(util.isValidDomainName('foo.bar')).toStrictEqual(true); }); - it('should return true when given a valid subdomain', function () { - assert.strictEqual(util.isValidDomainName('foo.foo.bar'), true); + it('should return true when given a valid subdomain', () => { + expect(util.isValidDomainName('foo.foo.bar')).toStrictEqual(true); }); - it('should return true when given a single-character domain', function () { - assert.strictEqual(util.isValidDomainName('f.bar'), true); + it('should return true when given a single-character domain', () => { + expect(util.isValidDomainName('f.bar')).toStrictEqual(true); }); - it('should return true when given a unicode TLD', function () { - assert.strictEqual(util.isValidDomainName('台灣.中国'), true); + it('should return true when given a unicode TLD', () => { + expect(util.isValidDomainName('台灣.中国')).toStrictEqual(true); }); - it('should return false when given a domain with unacceptable ASCII characters', function () { - assert.strictEqual(util.isValidDomainName('$.bar'), false); + it('should return false when given a domain with unacceptable ASCII characters', () => { + expect(util.isValidDomainName('$.bar')).toStrictEqual(false); }); - it('should return false when given a TLD that starts with a dash', function () { - assert.strictEqual(util.isValidDomainName('foo.-bar'), false); + it('should return false when given a TLD that starts with a dash', () => { + expect(util.isValidDomainName('foo.-bar')).toStrictEqual(false); }); - it('should return false when given a TLD that ends with a dash', function () { - assert.strictEqual(util.isValidDomainName('foo.bar-'), false); + it('should return false when given a TLD that ends with a dash', () => { + expect(util.isValidDomainName('foo.bar-')).toStrictEqual(false); }); - it('should return false when given a domain name with a chunk that starts with a dash', function () { - assert.strictEqual(util.isValidDomainName('-foo.bar'), false); + it('should return false when given a domain name with a chunk that starts with a dash', () => { + expect(util.isValidDomainName('-foo.bar')).toStrictEqual(false); }); - it('should return false when given a domain name with a chunk that ends with a dash', function () { - assert.strictEqual(util.isValidDomainName('foo-.bar'), false); + it('should return false when given a domain name with a chunk that ends with a dash', () => { + expect(util.isValidDomainName('foo-.bar')).toStrictEqual(false); }); - it('should return false when given a bare TLD', function () { - assert.strictEqual(util.isValidDomainName('bar'), false); + it('should return false when given a bare TLD', () => { + expect(util.isValidDomainName('bar')).toStrictEqual(false); }); - it('should return false when given a domain that starts with a period', function () { - assert.strictEqual(util.isValidDomainName('.bar'), false); + it('should return false when given a domain that starts with a period', () => { + expect(util.isValidDomainName('.bar')).toStrictEqual(false); }); - it('should return false when given a subdomain that starts with a period', function () { - assert.strictEqual(util.isValidDomainName('.foo.bar'), false); + it('should return false when given a subdomain that starts with a period', () => { + expect(util.isValidDomainName('.foo.bar')).toStrictEqual(false); }); - it('should return false when given a domain that ends with a period', function () { - assert.strictEqual(util.isValidDomainName('bar.'), false); + it('should return false when given a domain that ends with a period', () => { + expect(util.isValidDomainName('bar.')).toStrictEqual(false); }); - it('should return false when given a 1-character TLD', function () { - assert.strictEqual(util.isValidDomainName('foo.b'), false); + it('should return false when given a 1-character TLD', () => { + expect(util.isValidDomainName('foo.b')).toStrictEqual(false); }); }); - describe('isOriginContractAddress', function () { - it('should return true when the send address is the same as the selected tokens contract address', function () { - assert.equal( + describe('isOriginContractAddress', () => { + it('should return true when the send address is the same as the selected tokens contract address', () => { + expect( util.isOriginContractAddress( '0x8d6b81208414189a58339873ab429b6c47ab92d3', '0x8d6b81208414189a58339873ab429b6c47ab92d3', ), - true, - ); + ).toStrictEqual(true); }); - it('should return true when the send address is the same as the selected tokens contract address, capitalized input', function () { - assert.equal( + it('should return true when the send address is the same as the selected tokens contract address, capitalized input', () => { + expect( util.isOriginContractAddress( '0x8d6b81208414189a58339873ab429b6c47ab92d3', '0X8D6B81208414189A58339873AB429B6C47AB92D3', ), - true, - ); + ).toStrictEqual(true); }); - it('should return false when the recipient address differs', function () { - assert.equal( + it('should return false when the recipient address differs', () => { + expect( util.isOriginContractAddress( '0x8d6b81208414189a58339873ab429b6c47ab92d3', '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', ), - false, - ); + ).toStrictEqual(false); }); }); - describe('#numericBalance', function () { - it('should return a BN 0 if given nothing', function () { + describe('#numericBalance', () => { + it('should return a BN 0 if given nothing', () => { const result = util.numericBalance(); - assert.strictEqual(result.toString(10), '0'); + expect(result.toString(10)).toStrictEqual('0'); }); - it('should work with hex prefix', function () { + it('should work with hex prefix', () => { const result = util.numericBalance('0x012'); - assert.strictEqual(result.toString(10), '18'); + expect(result.toString(10)).toStrictEqual('18'); }); - it('should work with no hex prefix', function () { + it('should work with no hex prefix', () => { const result = util.numericBalance('012'); - assert.strictEqual(result.toString(10), '18'); + expect(result.toString(10)).toStrictEqual('18'); }); }); - describe('#formatBalance', function () { - it('should return None when given nothing', function () { + describe('#formatBalance', () => { + it('should return None when given nothing', () => { const result = util.formatBalance(); - assert.strictEqual(result, 'None', 'should return "None"'); + expect(result).toStrictEqual('None', 'should return "None"'); }); - it('should return 1.0000 ETH', function () { - const input = new ethUtil.BN(ethInWei, 10).toJSON(); + it('should return 1.0000 ETH', () => { + const input = new BN(ethInWei, 10).toJSON(); const result = util.formatBalance(input, 4); - assert.strictEqual(result, '1.0000 ETH'); + expect(result).toStrictEqual('1.0000 ETH'); }); it('should return 0.500 ETH', function () { - const input = new ethUtil.BN(ethInWei, 10) - .div(new ethUtil.BN('2', 10)) - .toJSON(); + const input = new BN(ethInWei, 10).div(new BN('2', 10)).toJSON(); const result = util.formatBalance(input, 3); - assert.strictEqual(result, '0.500 ETH'); + expect(result).toStrictEqual('0.500 ETH'); }); - it('should display specified decimal points', function () { + it('should display specified decimal points', () => { const input = '0x128dfa6a90b28000'; const result = util.formatBalance(input, 2); - assert.strictEqual(result, '1.33 ETH'); + expect(result).toStrictEqual('1.33 ETH'); }); - it('should default to 3 decimal points', function () { + it('should default to 3 decimal points', () => { const input = '0x128dfa6a90b28000'; const result = util.formatBalance(input); - assert.strictEqual(result, '1.337 ETH'); + expect(result).toStrictEqual('1.337 ETH'); }); - it('should show 2 significant digits for tiny balances', function () { + it('should show 2 significant digits for tiny balances', () => { const input = '0x1230fa6a90b28'; const result = util.formatBalance(input); - assert.strictEqual(result, '0.00032 ETH'); + expect(result).toStrictEqual('0.00032 ETH'); }); - it('should not parse the balance and return value with 2 decimal points with ETH at the end', function () { + it('should not parse the balance and return value with 2 decimal points with ETH at the end', () => { const value = '1.2456789'; const needsParse = false; const result = util.formatBalance(value, 2, needsParse); - assert.strictEqual(result, '1.24 ETH'); + expect(result).toStrictEqual('1.24 ETH'); }); }); describe('normalizing values', function () { - describe('#normalizeToWei', function () { - it('should convert an eth to the appropriate equivalent values', function () { - const valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - // kether:'0.001', - // mether:'0.000001', - // AUDIT: We're getting BN numbers on these ones. - // I think they're big enough to ignore for now. - // gether:'0.000000001', - // tether:'0.000000000001', - }; - const oneEthBn = new ethUtil.BN(ethInWei, 10); - - Object.keys(valueTable).forEach((currency) => { - const value = new ethUtil.BN(valueTable[currency], 10); - const output = util.normalizeToWei(value, currency); - assert.strictEqual( - output.toString(10), - valueTable.wei, - `value of ${output.toString( - 10, - )} ${currency} should convert to ${oneEthBn}`, - ); - }); - }); - }); - - describe('#normalizeEthStringToWei', function () { - it('should convert decimal eth to pure wei BN', function () { - const input = '1.23456789'; - const output = util.normalizeEthStringToWei(input); - assert.strictEqual(output.toString(10), '1234567890000000000'); - }); - - it('should convert 1 to expected wei', function () { - const input = '1'; - const output = util.normalizeEthStringToWei(input); - assert.strictEqual(output.toString(10), ethInWei); - }); - - it('should account for overflow numbers gracefully by dropping extra precision.', function () { - const input = '1.11111111111111111111'; - const output = util.normalizeEthStringToWei(input); - assert.strictEqual(output.toString(10), '1111111111111111111'); - }); - - it('should not truncate very exact wei values that do not have extra precision.', function () { - const input = '1.100000000000000001'; - const output = util.normalizeEthStringToWei(input); - assert.strictEqual(output.toString(10), '1100000000000000001'); - }); - }); - - describe('#normalizeNumberToWei', function () { - it('should handle a simple use case', function () { - const input = 0.0002; - const output = util.normalizeNumberToWei(input, 'ether'); - const str = output.toString(10); - assert.strictEqual(str, '200000000000000'); - }); - - it('should convert a kwei number to the appropriate equivalent wei', function () { - const result = util.normalizeNumberToWei(1.111, 'kwei'); - assert.strictEqual(result.toString(10), '1111', 'accepts decimals'); - }); - - it('should convert a ether number to the appropriate equivalent wei', function () { - const result = util.normalizeNumberToWei(1.111, 'ether'); - assert.strictEqual( - result.toString(10), - '1111000000000000000', - 'accepts decimals', - ); - }); - }); describe('#isHex', function () { it('should return true when given a hex string', function () { const result = util.isHex( 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', ); - assert(result); + expect(result).toStrictEqual(true); }); - it('should return false when given a non-hex string', function () { + it('should return false when given a non-hex string', () => { const result = util.isHex( 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714imnotreal', ); - assert(!result); + expect(result).toStrictEqual(false); }); - it('should return false when given a string containing a non letter/number character', function () { + it('should return false when given a string containing a non letter/number character', () => { const result = util.isHex( 'c3ab8ff13720!8ad9047dd39466b3c%8974e592c2fa383d4a396071imnotreal', ); - assert(!result); + expect(result).toStrictEqual(false); }); - it('should return true when given a hex string with hex-prefix', function () { + it('should return true when given a hex string with hex-prefix', () => { const result = util.isHex( '0xc3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', ); - assert(result); + expect(result).toStrictEqual(true); }); }); - describe('#getRandomFileName', function () { - it('should only return a string containing alphanumeric characters', function () { + describe('#getRandomFileName', () => { + it('should only return a string containing alphanumeric characters', () => { const result = util.getRandomFileName(); - assert(result.match(/^[a-zA-Z0-9]*$/gu)); + expect(result[0]).toStrictEqual( + expect.stringMatching(/^[a-zA-Z0-9]*$/gu), + ); }); // 50 samples - it('should return a string that is between 6 and 12 characters in length', function () { + it('should return a string that is between 6 and 12 characters in length', () => { for (let i = 0; i < 50; i++) { const result = util.getRandomFileName(); - assert(result.length >= 6 && result.length <= 12); + expect(result.length >= 6 && result.length <= 12).toStrictEqual(true); } }); }); }); - describe('checkExistingAddresses', function () { + describe('checkExistingAddresses', () => { const tokenList = [ { address: 'A' }, { address: 'n' }, @@ -380,28 +295,28 @@ describe('util', function () { { address: 'z' }, ]; - it('should return true when a lowercase address matches an uppercase address in the passed list', function () { - assert(util.checkExistingAddresses('q', tokenList) === true); + it('should return true when a lowercase address matches an uppercase address in the passed list', () => { + expect(util.checkExistingAddresses('q', tokenList)).toStrictEqual(true); }); - it('should return true when an uppercase address matches a lowercase address in the passed list', function () { - assert(util.checkExistingAddresses('N', tokenList) === true); + it('should return true when an uppercase address matches a lowercase address in the passed list', () => { + expect(util.checkExistingAddresses('N', tokenList)).toStrictEqual(true); }); - it('should return true when a lowercase address matches a lowercase address in the passed list', function () { - assert(util.checkExistingAddresses('z', tokenList) === true); + it('should return true when a lowercase address matches a lowercase address in the passed list', () => { + expect(util.checkExistingAddresses('z', tokenList)).toStrictEqual(true); }); - it('should return true when an uppercase address matches an uppercase address in the passed list', function () { - assert(util.checkExistingAddresses('Q', tokenList) === true); + it('should return true when an uppercase address matches an uppercase address in the passed list', () => { + expect(util.checkExistingAddresses('Q', tokenList)).toStrictEqual(true); }); - it('should return false when the passed address is not in the passed list', function () { - assert(util.checkExistingAddresses('b', tokenList) === false); + it('should return false when the passed address is not in the passed list', () => { + expect(util.checkExistingAddresses('b', tokenList)).toStrictEqual(false); }); }); - describe('toPrecisionWithoutTrailingZeros', function () { + describe('toPrecisionWithoutTrailingZeros', () => { const testData = [ { args: ['0', 9], result: '0' }, { args: [0, 9], result: '0' }, @@ -439,29 +354,27 @@ describe('util', function () { ]; testData.forEach(({ args, result }) => { - it(`should return ${result} when passed number ${args[0]} and precision ${args[1]}`, function () { - assert.strictEqual( - util.toPrecisionWithoutTrailingZeros(...args), + it(`should return ${result} when passed number ${args[0]} and precision ${args[1]}`, () => { + expect(util.toPrecisionWithoutTrailingZeros(...args)).toStrictEqual( result, ); }); }); }); - describe('addHexPrefixToObjectValues()', function () { - it('should return a new object with the same properties with a 0x prefix', function () { - assert.deepStrictEqual( + describe('addHexPrefixToObjectValues()', () => { + it('should return a new object with the same properties with a 0x prefix', () => { + expect( util.addHexPrefixToObjectValues({ prop1: '0x123', prop2: '456', prop3: 'x', }), - { - prop1: '0x123', - prop2: '0x456', - prop3: '0xx', - }, - ); + ).toStrictEqual({ + prop1: '0x123', + prop2: '0x456', + prop3: '0xx', + }); }); }); }); diff --git a/ui/app/hooks/tests/useRetryTransaction.test.js b/ui/app/hooks/tests/useRetryTransaction.test.js deleted file mode 100644 index 52be98fc1..000000000 --- a/ui/app/hooks/tests/useRetryTransaction.test.js +++ /dev/null @@ -1,71 +0,0 @@ -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'; - -describe('useRetryTransaction', function () { - describe('when transaction meets retry enabled criteria', function () { - const dispatch = sinon.spy(() => Promise.resolve({ blockTime: 0 })); - const trackEvent = sinon.spy(); - const event = { - preventDefault: () => undefined, - stopPropagation: () => undefined, - }; - - before(function () { - sinon.stub(reactRedux, 'useDispatch').returns(dispatch); - sinon.stub(methodDataHook, 'useMethodData').returns({}); - sinon.stub(metricEventHook, 'useMetricEvent').returns(trackEvent); - }); - - afterEach(function () { - dispatch.resetHistory(); - trackEvent.resetHistory(); - }); - const retryEnabledTransaction = { - ...transactions[0], - transactions: [ - { - submittedTime: new Date() - 5001, - }, - ], - hasRetried: false, - }; - - it('retryTransaction function should track metrics', function () { - const { result } = renderHook(() => - useRetryTransaction(retryEnabledTransaction, true), - ); - const retry = result.current; - retry(event); - assert.strictEqual(trackEvent.calledOnce, true); - }); - - it('retryTransaction function should show retry sidebar', async function () { - const { result } = renderHook(() => - useRetryTransaction(retryEnabledTransaction, true), - ); - const retry = result.current; - await retry(event); - assert.strictEqual( - dispatch.calledWith( - showSidebar({ - transitionName: 'sidebar-left', - type: 'customize-gas', - props: { transaction: retryEnabledTransaction.initialTransaction }, - }), - ), - true, - ); - }); - - after(function () { - sinon.restore(); - }); - }); -}); 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 57% rename from ui/app/hooks/tests/useCancelTransaction.test.js rename to ui/app/hooks/useCancelTransaction.test.js index 28957eb9c..773f2dbb5 100644 --- a/ui/app/hooks/tests/useCancelTransaction.test.js +++ b/ui/app/hooks/useCancelTransaction.test.js @@ -1,18 +1,18 @@ -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; const dispatch = sinon.spy(); - before(function () { + beforeAll(function () { sinon.stub(reactRedux, 'useDispatch').returns(dispatch); }); @@ -20,8 +20,12 @@ describe('useCancelTransaction', function () { dispatch.resetHistory(); }); + afterAll(function () { + sinon.restore(); + }); + describe('when account has insufficient balance to cover gas', function () { - before(function () { + beforeAll(function () { useSelector = sinon.stub(reactRedux, 'useSelector'); useSelector.callsFake((selector) => { if (selector === getConversionRate) { @@ -34,6 +38,9 @@ describe('useCancelTransaction', function () { return undefined; }); }); + afterAll(function () { + useSelector.restore(); + }); transactions.forEach((transactionGroup) => { const originalGasPrice = transactionGroup.primaryTransaction.txParams?.gasPrice; @@ -44,36 +51,53 @@ describe('useCancelTransaction', function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); - assert.strictEqual(result.current[0], false); + expect(result.current[0]).toStrictEqual(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), ); - assert.strictEqual(typeof result.current[1], 'function'); + expect(typeof result.current[1]).toStrictEqual('function'); result.current[1]({ preventDefault: () => undefined, stopPropagation: () => undefined, }); - assert.strictEqual( + const dispatchAction = dispatch.args; + + // calls customize-gas sidebar + // also check type= customize-gas + expect(dispatchAction[dispatchAction.length - 1][0].type).toStrictEqual( + actionConstants.SIDEBAR_OPEN, + ); + + expect( + dispatchAction[dispatchAction.length - 1][0].value.props.transaction + .id, + ).toStrictEqual(transactionId); + + // call onSubmit myself + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( + '0x5208', + '0x1', + ); + + expect( dispatch.calledWith( showModal({ name: 'CANCEL_TRANSACTION', transactionId, - originalGasPrice, + newGasFee: '0x5208', + defaultNewGasPrice: '0x1', + gasLimit: '0x5208', }), ), - true, - ); + ).toStrictEqual(true); }); }); - after(function () { - useSelector.restore(); - }); }); describe('when account has sufficient balance to cover gas', function () { - before(function () { + beforeAll(function () { useSelector = sinon.stub(reactRedux, 'useSelector'); useSelector.callsFake((selector) => { if (selector === getConversionRate) { @@ -86,6 +110,11 @@ describe('useCancelTransaction', function () { return undefined; }); }); + + afterAll(function () { + useSelector.restore(); + }); + transactions.forEach((transactionGroup) => { const originalGasPrice = transactionGroup.primaryTransaction.txParams?.gasPrice; @@ -96,35 +125,44 @@ describe('useCancelTransaction', function () { const { result } = renderHook(() => useCancelTransaction(transactionGroup), ); - assert.strictEqual(result.current[0], true); + expect(result.current[0]).toStrictEqual(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), ); - assert.strictEqual(typeof result.current[1], 'function'); + expect(typeof result.current[1]).toStrictEqual('function'); result.current[1]({ preventDefault: () => undefined, stopPropagation: () => undefined, }); - assert.strictEqual( + const dispatchAction = dispatch.args; + + expect(dispatchAction[dispatchAction.length - 1][0].type).toStrictEqual( + actionConstants.SIDEBAR_OPEN, + ); + expect( + dispatchAction[dispatchAction.length - 1][0].value.props.transaction + .id, + ).toStrictEqual(transactionId); + + dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( + '0x5208', + '0x1', + ); + + expect( dispatch.calledWith( showModal({ name: 'CANCEL_TRANSACTION', transactionId, - originalGasPrice, + newGasFee: '0x5208', + defaultNewGasPrice: '0x1', + gasLimit: '0x5208', }), ), - true, - ); + ).toStrictEqual(true); }); }); - after(function () { - useSelector.restore(); - }); - }); - - after(function () { - sinon.restore(); }); }); diff --git a/ui/app/hooks/tests/useCurrencyDisplay.test.js b/ui/app/hooks/useCurrencyDisplay.test.js similarity index 83% rename from ui/app/hooks/tests/useCurrencyDisplay.test.js rename to ui/app/hooks/useCurrencyDisplay.test.js index 8b019ab87..1231e5f14 100644 --- a/ui/app/hooks/tests/useCurrencyDisplay.test.js +++ b/ui/app/hooks/useCurrencyDisplay.test.js @@ -1,13 +1,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 = [ { @@ -99,9 +98,9 @@ const tests = [ }, ]; -describe('useCurrencyDisplay', function () { +describe('useCurrencyDisplay', () => { tests.forEach(({ input: { value, ...restProps }, result }) => { - describe(`when input is { value: ${value}, decimals: ${restProps.numberOfDecimals}, denomation: ${restProps.denomination} }`, function () { + describe(`when input is { value: ${value}, decimals: ${restProps.numberOfDecimals}, denomation: ${restProps.denomination} }`, () => { const stub = sinon.stub(reactRedux, 'useSelector'); stub.callsFake((selector) => { if (selector === getCurrentCurrency) { @@ -116,14 +115,14 @@ describe('useCurrencyDisplay', function () { const hookReturn = renderHook(() => useCurrencyDisplay(value, restProps)); const [displayValue, parts] = hookReturn.result.current; stub.restore(); - it(`should return ${result.displayValue} as displayValue`, function () { - assert.strictEqual(displayValue, result.displayValue); + it(`should return ${result.displayValue} as displayValue`, () => { + expect(displayValue).toStrictEqual(result.displayValue); }); - it(`should return ${result.value} as value`, function () { - assert.strictEqual(parts.value, result.value); + it(`should return ${result.value} as value`, () => { + expect(parts.value).toStrictEqual(result.value); }); - it(`should return ${result.suffix} as suffix`, function () { - assert.strictEqual(parts.suffix, result.suffix); + it(`should return ${result.suffix} as suffix`, () => { + expect(parts.suffix).toStrictEqual(result.suffix); }); }); }); 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/useRetryTransaction.test.js b/ui/app/hooks/useRetryTransaction.test.js new file mode 100644 index 000000000..c32d98174 --- /dev/null +++ b/ui/app/hooks/useRetryTransaction.test.js @@ -0,0 +1,113 @@ +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 { showSidebar } from '../store/actions'; +import * as methodDataHook from './useMethodData'; +import * as metricEventHook from './useMetricEvent'; +import { useRetryTransaction } from './useRetryTransaction'; + +describe('useRetryTransaction', () => { + describe('when transaction meets retry enabled criteria', () => { + const dispatch = sinon.spy(() => Promise.resolve({ blockTime: 0 })); + const trackEvent = sinon.spy(); + const event = { + preventDefault: () => undefined, + stopPropagation: () => undefined, + }; + + beforeAll(() => { + sinon.stub(reactRedux, 'useDispatch').returns(dispatch); + sinon.stub(methodDataHook, 'useMethodData').returns({}); + sinon.stub(metricEventHook, 'useMetricEvent').returns(trackEvent); + }); + + afterEach(() => { + dispatch.resetHistory(); + trackEvent.resetHistory(); + }); + + afterAll(() => { + sinon.restore(); + }); + + const retryEnabledTransaction = { + ...transactions[0], + transactions: [ + { + submittedTime: new Date() - 5001, + }, + ], + hasRetried: false, + }; + + it('retryTransaction function should track metrics', () => { + const { result } = renderHook(() => + useRetryTransaction(retryEnabledTransaction, true), + ); + const retry = result.current; + retry(event); + expect(trackEvent.calledOnce).toStrictEqual(true); + }); + + it('retryTransaction function should show retry sidebar', async () => { + const { result } = renderHook(() => + useRetryTransaction(retryEnabledTransaction, true), + ); + const retry = result.current; + await retry(event); + expect( + dispatch.calledWith( + showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { transaction: retryEnabledTransaction.initialTransaction }, + }), + ), + ).toStrictEqual(true); + }); + + it('should handle cancelled or multiple speedup transactions', async () => { + 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); + expect( + dispatch.calledWith( + showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { transaction: cancelledTransaction.primaryTransaction }, + }), + ), + ).toStrictEqual(true); + }); + }); +}); 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 59% rename from ui/app/hooks/tests/useTokenData.test.js rename to ui/app/hooks/useTokenData.test.js index 593176105..7c5ed8572 100644 --- a/ui/app/hooks/tests/useTokenData.test.js +++ b/ui/app/hooks/useTokenData.test.js @@ -1,15 +1,15 @@ -import assert from 'assert'; +/* eslint-disable jest/no-conditional-expect */ 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), @@ -44,23 +44,23 @@ const tests = [ }, ]; -describe('useTokenData', function () { +describe('useTokenData', () => { tests.forEach((test) => { const testTitle = - test.tokenData === null - ? `should return null when no data provided` - : `should return properly decoded data with _value ${test.tokenData.args[1].value}`; - it(testTitle, function () { + // eslint-disable-next-line no-negated-condition + test.tokenData !== null + ? `should return properly decoded data with _value ${test.tokenData.args[1]}` + : `should return null when no data provided`; + it(`${testTitle}`, () => { const { result } = renderHook(() => useTokenData(test.data)); if (test.tokenData) { - assert.strictEqual(result.current.name, test.tokenData.name); - assert.strictEqual( - result.current.args[0].toLowerCase(), + expect(result.current.name).toStrictEqual(test.tokenData.name); + expect(result.current.args[0].toLowerCase()).toStrictEqual( test.tokenData.args[0], ); - assert.ok(test.tokenData.args[1].eq(result.current.args[1])); + expect(test.tokenData.args[1]).toStrictEqual(result.current.args[1]); } else { - assert.strictEqual(result.current, test.tokenData); + expect(result.current).toStrictEqual(test.tokenData); } }); }); diff --git a/ui/app/hooks/tests/useTokenDisplayValue.test.js b/ui/app/hooks/useTokenDisplayValue.test.js similarity index 84% rename from ui/app/hooks/tests/useTokenDisplayValue.test.js rename to ui/app/hooks/useTokenDisplayValue.test.js index 6fc6f6105..91dc6df0b 100644 --- a/ui/app/hooks/tests/useTokenDisplayValue.test.js +++ b/ui/app/hooks/useTokenDisplayValue.test.js @@ -1,9 +1,8 @@ -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 = [ { @@ -118,19 +117,21 @@ const tests = [ }, ]; -describe('useTokenDisplayValue', function () { +describe('useTokenDisplayValue', () => { tests.forEach((test, idx) => { - describe(`when input is decimals: ${test.token.decimals} and value: ${test.tokenValue}`, function () { - it(`should return ${test.displayValue} as displayValue`, function () { + describe(`when input is decimals: ${test.token.decimals} and value: ${test.tokenValue}`, () => { + it(`should return ${test.displayValue} as displayValue`, () => { const getTokenValueStub = sinon.stub(tokenUtil, 'getTokenValueParam'); const getTokenDataStub = sinon.stub(txUtil, 'getTokenData'); + getTokenDataStub.callsFake(() => test.tokenData); getTokenValueStub.callsFake(() => test.tokenValue); + const { result } = renderHook(() => useTokenDisplayValue(`${idx}-fakestring`, test.token), ); sinon.restore(); - assert.strictEqual(result.current, test.displayValue); + expect(result.current).toStrictEqual(test.displayValue); }); }); }); 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 77% rename from ui/app/hooks/tests/useTransactionDisplayData.test.js rename to ui/app/hooks/useTransactionDisplayData.test.js index 4f6d4b3ff..aa0a9832d 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/useTransactionDisplayData.test.js @@ -1,34 +1,33 @@ -import assert from 'assert'; import React from 'react'; 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 +41,7 @@ const expectedResults = [ isSubmitted: false, }, { - title: 'Send ETH', + title: 'Send', category: TRANSACTION_GROUP_CATEGORIES.SEND, subtitle: 'To: 0x0ccc...8848', subtitleContainsOrigin: false, @@ -55,7 +54,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 +107,7 @@ const expectedResults = [ }, { title: 'Swap ETH to ABC', - category: TRANSACTION_CATEGORIES.SWAP, + category: TRANSACTION_TYPES.SWAP, subtitle: '', subtitleContainsOrigin: false, date: 'May 12, 2020', @@ -133,8 +132,8 @@ const renderHookWithRouter = (cb, tokenAddress) => { return renderHook(cb, { wrapper }); }; -describe('useTransactionDisplayData', function () { - before(function () { +describe('useTransactionDisplayData', () => { + beforeAll(() => { useSelector = sinon.stub(reactRedux, 'useSelector'); useTokenFiatAmount = sinon.stub( useTokenFiatAmountHooks, @@ -172,92 +171,88 @@ describe('useTransactionDisplayData', function () { return null; }); }); + + afterAll(() => { + sinon.restore(); + }); + transactions.forEach((transactionGroup, idx) => { - describe(`when called with group containing primaryTransaction id ${transactionGroup.primaryTransaction.id}`, function () { + describe(`when called with group containing primaryTransaction id ${transactionGroup.primaryTransaction.id}`, () => { const expected = expectedResults[idx]; const tokenAddress = transactionGroup.primaryTransaction?.destinationTokenAddress; - it(`should return a title of ${expected.title}`, function () { + it(`should return a title of ${expected.title}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual(result.current.title, expected.title); + expect(result.current.title).toStrictEqual(expected.title); }); - it(`should return a subtitle of ${expected.subtitle}`, function () { + it(`should return a subtitle of ${expected.subtitle}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual(result.current.subtitle, expected.subtitle); + expect(result.current.subtitle).toStrictEqual(expected.subtitle); }); - it(`should return a category of ${expected.category}`, function () { + it(`should return a category of ${expected.category}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual(result.current.category, expected.category); + expect(result.current.category).toStrictEqual(expected.category); }); - it(`should return a primaryCurrency of ${expected.primaryCurrency}`, function () { + it(`should return a primaryCurrency of ${expected.primaryCurrency}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual( - result.current.primaryCurrency, + expect(result.current.primaryCurrency).toStrictEqual( expected.primaryCurrency, ); }); - it(`should return a secondaryCurrency of ${expected.secondaryCurrency}`, function () { + it(`should return a secondaryCurrency of ${expected.secondaryCurrency}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual( - result.current.secondaryCurrency, + expect(result.current.secondaryCurrency).toStrictEqual( expected.secondaryCurrency, ); }); - it(`should return a displayedStatusKey of ${expected.displayedStatusKey}`, function () { + it(`should return a displayedStatusKey of ${expected.displayedStatusKey}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual( - result.current.displayedStatusKey, + expect(result.current.displayedStatusKey).toStrictEqual( expected.displayedStatusKey, ); }); - it(`should return a recipientAddress of ${expected.recipientAddress}`, function () { + it(`should return a recipientAddress of ${expected.recipientAddress}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual( - result.current.recipientAddress, + expect(result.current.recipientAddress).toStrictEqual( expected.recipientAddress, ); }); - it(`should return a senderAddress of ${expected.senderAddress}`, function () { + it(`should return a senderAddress of ${expected.senderAddress}`, () => { const { result } = renderHookWithRouter( () => useTransactionDisplayData(transactionGroup), tokenAddress, ); - assert.strictEqual( - result.current.senderAddress, + expect(result.current.senderAddress).toStrictEqual( expected.senderAddress, ); }); }); }); - it('should return an appropriate object', function () { + it('should return an appropriate object', () => { const { result } = renderHookWithRouter(() => useTransactionDisplayData(transactions[0]), ); - assert.deepStrictEqual(result.current, expectedResults[0]); - }); - after(function () { - useSelector.restore(); - useI18nContext.restore(); + expect(result.current).toStrictEqual(expectedResults[0]); }); }); diff --git a/ui/app/hooks/tests/useUserPreferencedCurrency.test.js b/ui/app/hooks/useUserPreferencedCurrency.test.js similarity index 86% rename from ui/app/hooks/tests/useUserPreferencedCurrency.test.js rename to ui/app/hooks/useUserPreferencedCurrency.test.js index 75ee4cf66..5689dc11e 100644 --- a/ui/app/hooks/tests/useUserPreferencedCurrency.test.js +++ b/ui/app/hooks/useUserPreferencedCurrency.test.js @@ -1,9 +1,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 = [ { @@ -122,9 +121,9 @@ function getFakeUseSelector(state) { }; } -describe('useUserPreferencedCurrency', function () { +describe('useUserPreferencedCurrency', () => { tests.forEach(({ params: { type, ...otherParams }, state, result }) => { - describe(`when showFiat is ${state.showFiat}, useNativeCurrencyAsPrimary is ${state.useNativeCurrencyAsPrimaryCurrency} and type is ${type}`, function () { + describe(`when showFiat is ${state.showFiat}, useNativeCurrencyAsPrimary is ${state.useNativeCurrencyAsPrimaryCurrency} and type is ${type}`, () => { const stub = sinon.stub(reactRedux, 'useSelector'); stub.callsFake(getFakeUseSelector(state)); @@ -134,14 +133,13 @@ describe('useUserPreferencedCurrency', function () { stub.restore(); it(`should return currency as ${ result.currency || 'not modified by user preferences' - }`, function () { - assert.strictEqual(hookResult.current.currency, result.currency); + }`, () => { + expect(hookResult.current.currency).toStrictEqual(result.currency); }); it(`should return decimals as ${ result.numberOfDecimals || 'not modified by user preferences' - }`, function () { - assert.strictEqual( - hookResult.current.numberOfDecimals, + }`, () => { + expect(hookResult.current.numberOfDecimals).toStrictEqual( result.numberOfDecimals, ); }); diff --git a/ui/app/pages/add-token/add-token.component.js b/ui/app/pages/add-token/add-token.component.js index 40f4a3675..c2c9e9fa3 100644 --- a/ui/app/pages/add-token/add-token.component.js +++ b/ui/app/pages/add-token/add-token.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import ethUtil from 'ethereumjs-util'; +import { isValidAddress } from 'ethereumjs-util'; import { checkExistingAddresses } from '../../helpers/utils/util'; import { tokenInfoGetter } from '../../helpers/utils/token-util'; import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'; @@ -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, }; @@ -161,11 +165,11 @@ class AddToken extends Component { autoFilled: false, }); - const isValidAddress = ethUtil.isValidAddress(customAddress); + const addressIsValid = isValidAddress(customAddress); const standardAddress = addHexPrefix(customAddress).toLowerCase(); switch (true) { - case !isValidAddress: + case !addressIsValid: this.setState({ customAddressError: this.context.t('invalidAddress'), customSymbol: '', @@ -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 67% 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..e91feafa7 100644 --- a/ui/app/pages/add-token/tests/add-token.test.js +++ b/ui/app/pages/add-token/add-token.test.js @@ -1,12 +1,11 @@ -import assert from 'assert'; 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 () { +describe('Add Token', () => { let wrapper; const state = { @@ -26,10 +25,11 @@ describe('Add Token', function () { tokens: [], identities: {}, mostRecentOverviewPage: '/', + showSearchTab: true, }; - describe('Add Token', function () { - before(function () { + describe('Add Token', () => { + beforeAll(() => { wrapper = mountWithRouter( @@ -40,76 +40,72 @@ describe('Add Token', function () { wrapper.find({ name: 'customToken' }).simulate('click'); }); - afterEach(function () { + afterEach(() => { props.history.push.reset(); }); - it('next button is disabled when no fields are populated', function () { + it('next button is disabled when no fields are populated', () => { const nextButton = wrapper.find( '.button.btn-secondary.page-container__footer-button', ); - assert.strictEqual(nextButton.props().disabled, true); + expect(nextButton.props().disabled).toStrictEqual(true); }); - it('edits token address', function () { + it('edits token address', () => { const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; const event = { target: { value: tokenAddress } }; const customAddress = wrapper.find('input#custom-address'); customAddress.simulate('change', event); - assert.strictEqual( + expect( wrapper.find('AddToken').instance().state.customAddress, - tokenAddress, - ); + ).toStrictEqual(tokenAddress); }); - it('edits token symbol', function () { + it('edits token symbol', () => { const tokenSymbol = 'META'; const event = { target: { value: tokenSymbol } }; const customAddress = wrapper.find('#custom-symbol'); customAddress.last().simulate('change', event); - assert.strictEqual( + expect( wrapper.find('AddToken').instance().state.customSymbol, - tokenSymbol, - ); + ).toStrictEqual(tokenSymbol); }); - it('edits token decimal precision', function () { + it('edits token decimal precision', () => { const tokenPrecision = '2'; const event = { target: { value: tokenPrecision } }; const customAddress = wrapper.find('#custom-decimals'); customAddress.last().simulate('change', event); - assert.strictEqual( + expect( wrapper.find('AddToken').instance().state.customDecimals, - tokenPrecision, - ); + ).toStrictEqual(tokenPrecision); }); - it('next', function () { + it('next', () => { const nextButton = wrapper.find( '.button.btn-secondary.page-container__footer-button', ); nextButton.simulate('click'); - assert(props.setPendingTokens.calledOnce); - assert(props.history.push.calledOnce); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.setPendingTokens.calledOnce).toStrictEqual(true); + expect(props.history.push.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/confirm-add-token', ); }); - it('cancels', function () { + it('cancels', () => { const cancelButton = wrapper.find( 'button.btn-default.page-container__footer-button', ); cancelButton.simulate('click'); - assert(props.clearPendingTokens.calledOnce); - assert.strictEqual(props.history.push.getCall(0).args[0], '/'); + expect(props.clearPendingTokens.calledOnce).toStrictEqual(true); + expect(props.history.push.getCall(0).args[0]).toStrictEqual('/'); }); }); }); 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..eb4ecb3f9 100644 --- a/ui/app/pages/asset/asset.scss +++ b/ui/app/pages/asset/asset.scss @@ -32,12 +32,12 @@ } } -.token-options { +.asset-options { &__button { 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/asset/components/asset-options.js b/ui/app/pages/asset/components/asset-options.js new file mode 100644 index 000000000..3b3ee21c2 --- /dev/null +++ b/ui/app/pages/asset/components/asset-options.js @@ -0,0 +1,80 @@ +import React, { useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { I18nContext } from '../../../contexts/i18n'; +import { Menu, MenuItem } from '../../../components/ui/menu'; + +const AssetOptions = ({ + onRemove, + onViewEtherscan, + onViewAccountDetails, + tokenSymbol, + isNativeAsset, +}) => { + const t = useContext(I18nContext); + const [assetOptionsButtonElement, setAssetOptionsButtonElement] = useState( + null, + ); + const [assetOptionsOpen, setAssetOptionsOpen] = useState(false); + + return ( + <> + + + )} +
    + )}
    {content}
    {footer}
    @@ -147,6 +171,58 @@ export default class ConfirmApproveContent extends Component { ); } + renderCustomNonceContent() { + const { t } = this.context; + const { + useNonceField, + customNonceValue, + updateCustomNonce, + getNextNonce, + nextNonce, + showCustomizeNonceModal, + } = this.props; + return ( + <> + {useNonceField && ( +
    + + + {t('nonce')} + + + + + {customNonceValue || nextNonce} + +
    + )} + + ); + } + render() { const { t } = this.context; const { @@ -160,6 +236,8 @@ export default class ConfirmApproveContent extends Component { showEditApprovalPermissionModal, setCustomAmount, tokenBalance, + useNonceField, + warning, } = this.props; const { showFullTxDetails } = this.state; @@ -169,6 +247,11 @@ export default class ConfirmApproveContent extends Component { 'confirm-approve-content--full': showFullTxDetails, })} > + {warning && ( +
    + +
    + )}
    @@ -232,6 +315,35 @@ export default class ConfirmApproveContent extends Component {
    ), })} + {useNonceField && + this.renderApproveContentCard({ + showHeader: false, + content: this.renderCustomNonceContent(), + useNonceField, + noBorder: !showFullTxDetails, + footer: ( +
    + this.setState({ + showFullTxDetails: !this.state.showFullTxDetails, + }) + } + > +
    +
    + View full transaction details +
    + +
    +
    + ), + })} {showFullTxDetails ? ( diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss index e912f6675..9122644a4 100644 --- a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss @@ -292,6 +292,38 @@ margin-left: 16px; } } + + &__custom-nonce-warning { + width: 100%; + height: 30px; + } + + &__custom-nonce-content { + display: flex; + height: 49px; + margin-top: 5px; + margin-bottom: 6px; + padding: 12px 12px 14px 12px; + border: 1px solid #bbc0c5; + box-sizing: border-box; + border-radius: 6px; + align-items: center; + } + + &__custom-nonce-header { + flex: 1; + align-items: center; + } + + &__custom-nonce-value { + flex: 0; + } + + & &__custom-nonce-edit { + @include H7; + + width: auto; + } } .confirm-approve-content--full { diff --git a/ui/app/pages/confirm-approve/confirm-approve.js b/ui/app/pages/confirm-approve/confirm-approve.js index c27b1160a..9eb51bd2a 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.js +++ b/ui/app/pages/confirm-approve/confirm-approve.js @@ -2,7 +2,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import ConfirmTransactionBase from '../confirm-transaction-base'; -import { showModal } from '../../store/actions'; +import { + showModal, + updateCustomNonce, + getNextNonce, +} from '../../store/actions'; import { getTokenData } from '../../helpers/utils/transactions.util'; import { calcTokenAmount, @@ -17,6 +21,9 @@ import { getCurrentCurrency, getDomainMetadata, getNativeCurrency, + getUseNonceField, + getCustomNonceValue, + getNextSuggestedNonce, } from '../../selectors'; import { currentNetworkTxListSelector } from '../../selectors/transactions'; import Loading from '../../components/ui/loading-screen'; @@ -36,6 +43,9 @@ export default function ConfirmApprove() { const currentNetworkTxList = useSelector(currentNetworkTxListSelector); const domainMetadata = useSelector(getDomainMetadata); const tokens = useSelector(getTokens); + const useNonceField = useSelector(getUseNonceField); + const nextNonce = useSelector(getNextSuggestedNonce); + const customNonceValue = useSelector(getCustomNonceValue); const transaction = currentNetworkTxList.find( @@ -72,6 +82,25 @@ export default function ConfirmApprove() { previousTokenAmount.current = tokenAmount; }, [customPermissionAmount, tokenAmount]); + const [submitWarning, setSubmitWarning] = useState(''); + const prevNonce = useRef(nextNonce); + const prevCustomNonce = useRef(customNonceValue); + useEffect(() => { + if ( + prevNonce.current !== nextNonce || + prevCustomNonce.current !== customNonceValue + ) { + if (nextNonce !== null && customNonceValue > nextNonce) { + setSubmitWarning( + `Nonce is higher than suggested nonce of ${nextNonce}`, + ); + } else { + setSubmitWarning(''); + } + } + prevCustomNonce.current = customNonceValue; + prevNonce.current = nextNonce; + }, [customNonceValue, nextNonce]); const { origin } = transaction; const formattedOrigin = origin ? origin[0].toUpperCase() + origin.slice(1) @@ -139,6 +168,34 @@ export default function ConfirmApprove() { nativeCurrency={nativeCurrency} ethTransactionTotal={ethTransactionTotal} fiatTransactionTotal={fiatTransactionTotal} + useNonceField={useNonceField} + nextNonce={nextNonce} + customNonceValue={customNonceValue} + updateCustomNonce={(value) => { + dispatch(updateCustomNonce(value)); + }} + getNextNonce={() => dispatch(getNextNonce())} + showCustomizeNonceModal={({ + /* eslint-disable no-shadow */ + useNonceField, + nextNonce, + customNonceValue, + updateCustomNonce, + getNextNonce, + /* eslint-disable no-shadow */ + }) => + dispatch( + showModal({ + name: 'CUSTOMIZE_NONCE', + useNonceField, + nextNonce, + customNonceValue, + updateCustomNonce, + getNextNonce, + }), + ) + } + warning={submitWarning} /> } hideSenderToRecipient 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-deploy-contract/confirm-deploy-contract.component.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js index 49a192071..0c37ca083 100644 --- a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js +++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import ethUtil from 'ethereumjs-util'; +import { toBuffer } from 'ethereumjs-util'; import ConfirmTransactionBase from '../confirm-transaction-base'; export default class ConfirmDeployContract extends Component { @@ -29,7 +29,7 @@ export default class ConfirmDeployContract extends Component {
    {`${t('bytes')}:`}
    -
    {ethUtil.toBuffer(data).length}
    +
    {toBuffer(data).length}
    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..54646ac64 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 @@ -1,4 +1,4 @@ -import ethUtil from 'ethereumjs-util'; +import { toBuffer } from 'ethereumjs-util'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app'; @@ -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, }, }); @@ -365,7 +365,7 @@ export default class ConfirmTransactionBase extends Component {
    )}
    - {`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`} + {`${t('hexData')}: ${toBuffer(data).length} bytes`}
    {data}
    @@ -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/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js new file mode 100644 index 000000000..b4d9caa37 --- /dev/null +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.test.js @@ -0,0 +1,15 @@ +import { getMethodName } from './confirm-transaction-base.component'; + +describe('ConfirmTransactionBase Component', () => { + describe('getMethodName', () => { + it('should get correct method names', () => { + expect(getMethodName(undefined)).toStrictEqual(''); + expect(getMethodName({})).toStrictEqual(''); + expect(getMethodName('confirm')).toStrictEqual('confirm'); + expect(getMethodName('balanceOf')).toStrictEqual('balance Of'); + expect(getMethodName('ethToTokenSwapInput')).toStrictEqual( + 'eth To Token Swap Input', + ); + }); + }); +}); 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..7a761bd9b 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)), @@ -154,6 +149,7 @@ const mapStateToProps = (state, ownProps) => { }, }; } + customNonceValue = getCustomNonceValue(state); return { balance, @@ -184,12 +180,12 @@ const mapStateToProps = (state, ownProps) => { }, advancedInlineGasShown: getAdvancedInlineGasShown(state), useNonceField: getUseNonceField(state), - customNonceValue: getCustomNonceValue(state), + customNonceValue, insufficientBalance, hideSubtitle: !isMainnet && !showFiatInTestnets, hideFiatConversion: !isMainnet && !showFiatInTestnets, metaMetricsSendCount, - transactionCategory, + type, nextNonce, mostRecentOverviewPage: getMostRecentOverviewPage(state), isMainnet, diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js deleted file mode 100644 index a46d89f95..000000000 --- a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import assert from 'assert'; -import { getMethodName } from '../confirm-transaction-base.component'; - -describe('ConfirmTransactionBase Component', function () { - describe('getMethodName', function () { - it('should get correct method names', function () { - assert.strictEqual(getMethodName(undefined), ''); - assert.strictEqual(getMethodName({}), ''); - assert.strictEqual(getMethodName('confirm'), 'confirm'); - assert.strictEqual(getMethodName('balanceOf'), 'balance Of'); - assert.strictEqual( - getMethodName('ethToTokenSwapInput'), - 'eth To Token Swap Input', - ); - }); - }); -}); 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/confirmation/confirmation.scss b/ui/app/pages/confirmation/confirmation.scss index dea6190ee..70e456d05 100644 --- a/ui/app/pages/confirmation/confirmation.scss +++ b/ui/app/pages/confirmation/confirmation.scss @@ -21,6 +21,7 @@ &__content { grid-area: content; padding: 16px 16px 0; + min-width: 0; & > :last-child { margin-bottom: 16px; @@ -59,4 +60,12 @@ &__navigation &__navigation-button:last-child { margin-left: 8px; } + + .chip { + max-width: 100%; + + &__label { + word-break: break-all; + } + } } 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 60% 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..0a445f688 100644 --- a/ui/app/pages/create-account/tests/create-account.test.js +++ b/ui/app/pages/create-account/create-account.test.js @@ -1,10 +1,9 @@ -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 () { +describe('Create Account Page', () => { let wrapper; const props = { @@ -16,34 +15,32 @@ describe('Create Account Page', function () { }, }; - before(function () { + beforeAll(() => { wrapper = mountWithRouter(); }); - afterEach(function () { + afterEach(() => { props.history.push.resetHistory(); }); - it('clicks create account and routes to new-account path', function () { + it('clicks create account and routes to new-account path', () => { const createAccount = wrapper.find('.new-account__tabs__tab').at(0); createAccount.simulate('click'); - assert.strictEqual(props.history.push.getCall(0).args[0], '/new-account'); + expect(props.history.push.getCall(0).args[0]).toStrictEqual('/new-account'); }); - it('clicks import account and routes to import new account path', function () { + it('clicks import account and routes to import new account path', () => { const importAccount = wrapper.find('.new-account__tabs__tab').at(1); importAccount.simulate('click'); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/new-account/import', ); }); - it('clicks connect HD Wallet and routes to connect new account path', function () { + it('clicks connect HD Wallet and routes to connect new account path', () => { const connectHdWallet = wrapper.find('.new-account__tabs__tab').at(2); connectHdWallet.simulate('click'); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/new-account/connect', ); }); 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 59% 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..e26639c49 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 @@ -1,8 +1,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(, { @@ -14,88 +13,87 @@ function shallowRender(props = {}, context = {}) { }); } -describe('ImportWithSeedPhrase Component', function () { - it('should render without error', function () { +describe('ImportWithSeedPhrase Component', () => { + it('should render without error', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const textareaCount = root.find('.first-time-flow__textarea').length; - assert.strictEqual(textareaCount, 1, 'should render 12 seed phrases'); + expect(textareaCount).toStrictEqual(1); }); - describe('parseSeedPhrase', function () { - it('should handle a regular seed phrase', function () { + describe('parseSeedPhrase', () => { + it('should handle a regular seed phrase', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase('foo bar baz'), 'foo bar baz'); + expect(parseSeedPhrase('foo bar baz')).toStrictEqual('foo bar baz'); }); - it('should handle a mixed-case seed phrase', function () { + it('should handle a mixed-case seed phrase', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase('FOO bAr baZ'), 'foo bar baz'); + expect(parseSeedPhrase('FOO bAr baZ')).toStrictEqual('foo bar baz'); }); - it('should handle an upper-case seed phrase', function () { + it('should handle an upper-case seed phrase', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase('FOO BAR BAZ'), 'foo bar baz'); + expect(parseSeedPhrase('FOO BAR BAZ')).toStrictEqual('foo bar baz'); }); - it('should trim extraneous whitespace from the given seed phrase', function () { + it('should trim extraneous whitespace from the given seed phrase', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual( - parseSeedPhrase(' foo bar baz '), + expect(parseSeedPhrase(' foo bar baz ')).toStrictEqual( 'foo bar baz', ); }); - it('should return an empty string when given a whitespace-only string', function () { + it('should return an empty string when given a whitespace-only string', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase(' '), ''); + expect(parseSeedPhrase(' ')).toStrictEqual(''); }); - it('should return an empty string when given a string with only symbols', function () { + it('should return an empty string when given a string with only symbols', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase('$'), ''); + expect(parseSeedPhrase('$')).toStrictEqual(''); }); - it('should return an empty string for both null and undefined', function () { + it('should return an empty string for both null and undefined', () => { const root = shallowRender({ onSubmit: sinon.spy(), }); const { parseSeedPhrase } = root.instance(); - assert.deepStrictEqual(parseSeedPhrase(undefined), ''); - assert.deepStrictEqual(parseSeedPhrase(null), ''); + expect(parseSeedPhrase(undefined)).toStrictEqual(''); + expect(parseSeedPhrase(null)).toStrictEqual(''); }); }); }); diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js new file mode 100644 index 000000000..314025110 --- /dev/null +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.test.js @@ -0,0 +1,38 @@ +import React from 'react'; +import sinon from 'sinon'; +import { tick } from '../../../../../test/lib/tick'; +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', () => { + let wrapper; + + const props = { + history: { + push: sinon.stub(), + }, + setCompletedOnboarding: sinon.stub().resolves(), + }; + + beforeEach(() => { + wrapper = mountWithRouter(); + }); + + it('renders', () => { + expect(wrapper).toHaveLength(1); + }); + + it('should navigate to the default route on click', async () => { + const endOfFlowButton = wrapper.find( + '.btn-primary.first-time-flow__button', + ); + endOfFlowButton.simulate('click'); + + await tick(); + + expect( + props.history.push.calledOnceWithExactly(DEFAULT_ROUTE), + ).toStrictEqual(true); + }); +}); 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/tests/end-of-flow.test.js deleted file mode 100644 index 2e25aa6b9..000000000 --- a/ui/app/pages/first-time-flow/end-of-flow/tests/end-of-flow.test.js +++ /dev/null @@ -1,37 +0,0 @@ -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 '..'; - -describe('End of Flow Screen', function () { - let wrapper; - - const props = { - history: { - push: sinon.spy(), - }, - setCompletedOnboarding: sinon.spy(), - }; - - beforeEach(function () { - wrapper = mountWithRouter(); - }); - - it('renders', function () { - assert.strictEqual(wrapper.length, 1); - }); - - it('should navigate to the default route on click', function (done) { - const endOfFlowButton = wrapper.find( - '.btn-primary.first-time-flow__button', - ); - endOfFlowButton.simulate('click'); - - setImmediate(() => { - assert(props.history.push.calledOnceWithExactly(DEFAULT_ROUTE)); - done(); - }); - }); -}); 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 67% 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..ab057080d 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,17 +1,16 @@ -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 () { +describe('FirstTimeFlowSwitch', () => { + it('redirects to /welcome route with null props', () => { const props = { completedOnboarding: null, isInitialized: null, @@ -21,15 +20,14 @@ describe('FirstTimeFlowSwitch', function () { const wrapper = mountWithRouter( , ); - assert.strictEqual( + expect( wrapper .find('Lifecycle') - .find({ to: { pathname: INITIALIZE_WELCOME_ROUTE } }).length, - 1, - ); + .find({ to: { pathname: INITIALIZE_WELCOME_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to / route when completedOnboarding is true', function () { + it('redirects to / route when completedOnboarding is true', () => { const props = { completedOnboarding: true, }; @@ -37,14 +35,12 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( - wrapper.find('Lifecycle').find({ to: { pathname: DEFAULT_ROUTE } }) - .length, - 1, - ); + expect( + wrapper.find('Lifecycle').find({ to: { pathname: DEFAULT_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to end of flow route when seedPhraseBackedUp is true', function () { + it('redirects to end of flow route when seedPhraseBackedUp is true', () => { const props = { completedOnboarding: false, seedPhraseBackedUp: true, @@ -53,15 +49,14 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( + expect( wrapper .find('Lifecycle') - .find({ to: { pathname: INITIALIZE_END_OF_FLOW_ROUTE } }).length, - 1, - ); + .find({ to: { pathname: INITIALIZE_END_OF_FLOW_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to end of flow route when seedPhraseBackedUp is false', function () { + it('redirects to end of flow route when seedPhraseBackedUp is false', () => { const props = { completedOnboarding: false, seedPhraseBackedUp: false, @@ -70,15 +65,14 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( + expect( wrapper .find('Lifecycle') - .find({ to: { pathname: INITIALIZE_END_OF_FLOW_ROUTE } }).length, - 1, - ); + .find({ to: { pathname: INITIALIZE_END_OF_FLOW_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to /lock route when isUnlocked is true ', function () { + it('redirects to /lock route when isUnlocked is true', () => { const props = { completedOnboarding: false, isUnlocked: true, @@ -89,13 +83,12 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( - wrapper.find('Lifecycle').find({ to: { pathname: LOCK_ROUTE } }).length, - 1, - ); + expect( + wrapper.find('Lifecycle').find({ to: { pathname: LOCK_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to /welcome route when isInitialized is false', function () { + it('redirects to /welcome route when isInitialized is false', () => { const props = { completedOnboarding: false, isUnlocked: false, @@ -107,15 +100,14 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( + expect( wrapper .find('Lifecycle') - .find({ to: { pathname: INITIALIZE_WELCOME_ROUTE } }).length, - 1, - ); + .find({ to: { pathname: INITIALIZE_WELCOME_ROUTE } }), + ).toHaveLength(1); }); - it('redirects to /unlock route when isInitialized is true', function () { + it('redirects to /unlock route when isInitialized is true', () => { const props = { completedOnboarding: false, isUnlocked: false, @@ -127,11 +119,10 @@ describe('FirstTimeFlowSwitch', function () { , ); - assert.strictEqual( + expect( wrapper .find('Lifecycle') - .find({ to: { pathname: INITIALIZE_UNLOCK_ROUTE } }).length, - 1, - ); + .find({ to: { pathname: INITIALIZE_UNLOCK_ROUTE } }), + ).toHaveLength(1); }); }); 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 67% 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..fa1d61889 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 @@ -1,12 +1,11 @@ -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 () { +describe('MetaMetricsOptIn', () => { + it('opt out of MetaMetrics', () => { const props = { history: { push: sinon.spy(), @@ -26,7 +25,9 @@ describe('MetaMetricsOptIn', function () { ); noThanksButton.simulate('click'); - assert.ok(props.setParticipateInMetaMetrics.calledOnceWithExactly(false)); + expect( + props.setParticipateInMetaMetrics.calledOnceWithExactly(false), + ).toStrictEqual(true); props.setParticipateInMetaMetrics.resetHistory(); }); }); 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 73% 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..6627eabc7 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 @@ -1,8 +1,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(, { @@ -13,20 +12,18 @@ function shallowRender(props = {}, context = {}) { }); } -describe('ConfirmSeedPhrase Component', function () { - it('should render correctly', function () { +describe('ConfirmSeedPhrase Component', () => { + it('should render correctly', () => { const root = shallowRender({ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', }); - assert.strictEqual( - root.find('.confirm-seed-phrase__seed-word--sorted').length, + expect(root.find('.confirm-seed-phrase__seed-word--sorted')).toHaveLength( 12, - 'should render 12 seed phrases', ); }); - it('should add/remove selected on click', function () { + it('should add/remove selected on click', () => { const metricsEventSpy = sinon.spy(); const pushSpy = sinon.spy(); const root = shallowRender( @@ -46,11 +43,7 @@ describe('ConfirmSeedPhrase Component', function () { seeds.at(1).simulate('click'); seeds.at(2).simulate('click'); - assert.deepStrictEqual( - root.state().selectedSeedIndices, - [0, 1, 2], - 'should add seed phrase to selected on click', - ); + expect(root.state().selectedSeedIndices).toStrictEqual([0, 1, 2]); // Click on a selected seed to remove root.state(); @@ -60,14 +53,10 @@ describe('ConfirmSeedPhrase Component', function () { .find('.confirm-seed-phrase__seed-word--sorted') .at(1) .simulate('click'); - assert.deepStrictEqual( - root.state().selectedSeedIndices, - [0, 2], - 'should remove seed phrase from selected when click again', - ); + expect(root.state().selectedSeedIndices).toStrictEqual([0, 2]); }); - it('should render correctly on hover', function () { + it('should render correctly on hover', () => { const metricsEventSpy = sinon.spy(); const pushSpy = sinon.spy(); const root = shallowRender( @@ -97,12 +86,12 @@ describe('ConfirmSeedPhrase Component', function () { '.confirm-seed-phrase__selected-seed-words__pending-seed', ); - assert.strictEqual(pendingSeeds.at(0).props().seedIndex, 2); - assert.strictEqual(pendingSeeds.at(1).props().seedIndex, 0); - assert.strictEqual(pendingSeeds.at(2).props().seedIndex, 1); + expect(pendingSeeds.at(0).props().seedIndex).toStrictEqual(2); + expect(pendingSeeds.at(1).props().seedIndex).toStrictEqual(0); + expect(pendingSeeds.at(2).props().seedIndex).toStrictEqual(1); }); - it('should insert seed in place on drop', function () { + it('should insert seed in place on drop', () => { const metricsEventSpy = sinon.spy(); const pushSpy = sinon.spy(); const root = shallowRender( @@ -129,11 +118,11 @@ describe('ConfirmSeedPhrase Component', function () { root.update(); - assert.deepStrictEqual(root.state().selectedSeedIndices, [2, 0, 1]); - assert.deepStrictEqual(root.state().pendingSeedIndices, [2, 0, 1]); + expect(root.state().selectedSeedIndices).toStrictEqual([2, 0, 1]); + expect(root.state().pendingSeedIndices).toStrictEqual([2, 0, 1]); }); - it('should submit correctly', async function () { + it('should submit correctly', async () => { const originalSeed = [ '鼠', '牛', @@ -177,14 +166,14 @@ describe('ConfirmSeedPhrase Component', function () { await new Promise((resolve) => setTimeout(resolve, 100)); - assert.deepStrictEqual(metricsEventSpy.args[0][0], { + expect(metricsEventSpy.args[0][0]).toStrictEqual({ eventOpts: { category: 'Onboarding', action: 'Seed Phrase Setup', name: 'Verify Complete', }, }); - assert(initialize3BoxSpy.calledOnce); - assert.strictEqual(pushSpy.args[0][0], '/initialize/end-of-flow'); + expect(initialize3BoxSpy.calledOnce).toStrictEqual(true); + expect(pushSpy.args[0][0]).toStrictEqual('/initialize/end-of-flow'); }); }); 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 63% 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..ba7191871 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 @@ -1,10 +1,9 @@ -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 () { +describe('Reveal Seed Phrase', () => { let wrapper; const TEST_SEED = @@ -19,7 +18,7 @@ describe('Reveal Seed Phrase', function () { setCompletedOnboarding: sinon.spy(), }; - beforeEach(function () { + beforeEach(() => { wrapper = mount(, { context: { t: (str) => str, @@ -28,22 +27,22 @@ describe('Reveal Seed Phrase', function () { }); }); - it('seed phrase', function () { + it('seed phrase', () => { const seedPhrase = wrapper.find( '.reveal-seed-phrase__secret-words--hidden', ); - assert.strictEqual(seedPhrase.length, 1); - assert.strictEqual(seedPhrase.text(), TEST_SEED); + expect(seedPhrase).toHaveLength(1); + expect(seedPhrase.text()).toStrictEqual(TEST_SEED); }); - it('clicks to reveal', function () { + it('clicks to reveal', () => { const reveal = wrapper.find('.reveal-seed-phrase__secret-blocker'); - assert.strictEqual(wrapper.state().isShowingSeedPhrase, false); + expect(wrapper.state().isShowingSeedPhrase).toStrictEqual(false); reveal.simulate('click'); - assert.strictEqual(wrapper.state().isShowingSeedPhrase, true); + expect(wrapper.state().isShowingSeedPhrase).toStrictEqual(true); const showSeed = wrapper.find('.reveal-seed-phrase__secret-words'); - assert.strictEqual(showSeed.length, 1); + expect(showSeed).toHaveLength(1); }); }); diff --git a/ui/app/pages/first-time-flow/select-action/select-action.test.js b/ui/app/pages/first-time-flow/select-action/select-action.test.js new file mode 100644 index 000000000..013176b30 --- /dev/null +++ b/ui/app/pages/first-time-flow/select-action/select-action.test.js @@ -0,0 +1,51 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mountWithRouter } from '../../../../../test/lib/render-helpers'; +import SelectAction from './select-action.container'; + +describe('Selection Action', () => { + let wrapper; + + const props = { + isInitialized: false, + setFirstTimeFlowType: sinon.spy(), + history: { + push: sinon.spy(), + }, + }; + + beforeEach(() => { + wrapper = mountWithRouter(); + }); + + afterEach(() => { + props.setFirstTimeFlowType.resetHistory(); + props.history.push.resetHistory(); + }); + + it('clicks import wallet to route to import FTF', () => { + const importWalletButton = wrapper + .find('.btn-primary.first-time-flow__button') + .at(0); + importWalletButton.simulate('click'); + + expect(props.setFirstTimeFlowType.calledOnce).toStrictEqual(true); + expect(props.setFirstTimeFlowType.getCall(0).args[0]).toStrictEqual( + 'import', + ); + expect(props.history.push.calledOnce).toStrictEqual(true); + }); + + it('clicks create wallet to route to create FTF', () => { + const createWalletButton = wrapper + .find('.btn-primary.first-time-flow__button') + .at(1); + createWalletButton.simulate('click'); + + expect(props.setFirstTimeFlowType.calledOnce).toStrictEqual(true); + expect(props.setFirstTimeFlowType.getCall(0).args[0]).toStrictEqual( + 'create', + ); + expect(props.history.push.calledOnce).toStrictEqual(true); + }); +}); 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/tests/select-action.test.js deleted file mode 100644 index 9f5b66fc6..000000000 --- a/ui/app/pages/first-time-flow/select-action/tests/select-action.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import assert from 'assert'; -import React from 'react'; -import sinon from 'sinon'; -import { mountWithRouter } from '../../../../../../test/lib/render-helpers'; -import SelectAction from '..'; - -describe('Selection Action', function () { - let wrapper; - - const props = { - isInitialized: false, - setFirstTimeFlowType: sinon.spy(), - history: { - push: sinon.spy(), - }, - }; - - beforeEach(function () { - wrapper = mountWithRouter(); - }); - - afterEach(function () { - props.setFirstTimeFlowType.resetHistory(); - props.history.push.resetHistory(); - }); - - it('clicks import wallet to route to import FTF', function () { - const importWalletButton = wrapper - .find('.btn-primary.first-time-flow__button') - .at(0); - importWalletButton.simulate('click'); - - assert(props.setFirstTimeFlowType.calledOnce); - assert.strictEqual(props.setFirstTimeFlowType.getCall(0).args[0], 'import'); - assert(props.history.push.calledOnce); - }); - - it('clicks create wallet to route to create FTF ', function () { - const createWalletButton = wrapper - .find('.btn-primary.first-time-flow__button') - .at(1); - createWalletButton.simulate('click'); - - assert(props.setFirstTimeFlowType.calledOnce); - assert.strictEqual(props.setFirstTimeFlowType.getCall(0).args[0], 'create'); - assert(props.history.push.calledOnce); - }); -}); 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 75% 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..2d60b4d57 100644 --- a/ui/app/pages/first-time-flow/welcome/tests/welcome.test.js +++ b/ui/app/pages/first-time-flow/welcome/welcome.test.js @@ -1,22 +1,21 @@ -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 () { +describe('Welcome', () => { const mockStore = { metamask: {}, }; const store = configureMockStore()(mockStore); - after(function () { + afterAll(() => { sinon.restore(); }); - it('routes to select action when participateInMetaMetrics is not initialized', function () { + it('routes to select action when participateInMetaMetrics is not initialized', () => { const props = { history: { push: sinon.spy(), @@ -32,13 +31,12 @@ describe('Welcome', function () { '.btn-primary.first-time-flow__button', ); getStartedButton.simulate('click'); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/initialize/select-action', ); }); - it('routes to correct password when participateInMetaMetrics is initialized', function () { + it('routes to correct password when participateInMetaMetrics is initialized', () => { const props = { welcomeScreenSeen: true, participateInMetaMetrics: false, @@ -56,8 +54,7 @@ describe('Welcome', function () { '.btn-primary.first-time-flow__button', ); getStartedButton.simulate('click'); - assert.strictEqual( - props.history.push.getCall(0).args[0], + expect(props.history.push.getCall(0).args[0]).toStrictEqual( '/initialize/create-password', ); }); diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index fb66fc964..faf86eb91 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -13,7 +13,7 @@ import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; -import SwapsIntroPopup from '../swaps/intro-popup'; +import WhatsNewPopup from '../../components/app/whats-new-popup'; import { ASSET_ROUTE, @@ -64,19 +64,18 @@ export default class Home extends PureComponent { connectedStatusPopoverHasBeenShown: PropTypes.bool, defaultHomeActiveTabName: PropTypes.string, onTabClick: PropTypes.func.isRequired, - setSwapsWelcomeMessageHasBeenShown: PropTypes.func.isRequired, - swapsWelcomeMessageHasBeenShown: PropTypes.bool.isRequired, haveSwapsQuotes: PropTypes.bool.isRequired, showAwaitingSwapScreen: PropTypes.bool.isRequired, swapsFetchParams: PropTypes.object, - swapsEnabled: PropTypes.bool, - isMainnet: PropTypes.bool, shouldShowWeb3ShimUsageNotification: PropTypes.bool.isRequired, setWeb3ShimUsageAlertDismissed: PropTypes.func.isRequired, originOfCurrentTab: PropTypes.string, disableWeb3ShimUsageAlert: PropTypes.func.isRequired, pendingConfirmations: PropTypes.arrayOf(PropTypes.object).isRequired, infuraBlocked: PropTypes.bool.isRequired, + showWhatsNewPopup: PropTypes.bool.isRequired, + hideWhatsNewPopup: PropTypes.func.isRequired, + notificationsToShow: PropTypes.bool.isRequired, }; state = { @@ -323,10 +322,9 @@ export default class Home extends PureComponent { history, connectedStatusPopoverHasBeenShown, isPopup, - swapsWelcomeMessageHasBeenShown, - setSwapsWelcomeMessageHasBeenShown, - swapsEnabled, - isMainnet, + notificationsToShow, + showWhatsNewPopup, + hideWhatsNewPopup, } = this.props; if (forgottenPassword) { @@ -344,8 +342,8 @@ export default class Home extends PureComponent { exact />
    - {!swapsWelcomeMessageHasBeenShown && swapsEnabled && isMainnet ? ( - + {notificationsToShow && showWhatsNewPopup ? ( + ) : null} {isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() @@ -381,7 +379,20 @@ export default class Home extends PureComponent { +
    + {t('needHelp', [ + + {t('needHelpLinkText')} + , + ])} +
    + {this.renderNotifications()} diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index 721c7a15e..6020c1510 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -12,6 +12,8 @@ import { getWeb3ShimUsageStateForOrigin, unconfirmedTransactionsCountSelector, getInfuraBlocked, + getShowWhatsNewPopup, + getSortedNotificationsToShow, } from '../../selectors'; import { @@ -21,16 +23,12 @@ import { setShowRestorePromptToFalse, setConnectedStatusPopoverHasBeenShown, setDefaultHomeActiveTabName, - setSwapsWelcomeMessageHasBeenShown, setWeb3ShimUsageAlertDismissed, setAlertEnabledness, } from '../../store/actions'; -import { setThreeBoxLastUpdated } from '../../ducks/app/app'; +import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; -import { - getSwapsWelcomeMessageSeenStatus, - getSwapsFeatureLiveness, -} from '../../ducks/swaps/swaps'; +import { getSwapsFeatureLiveness } from '../../ducks/swaps/swaps'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_NOTIFICATION, @@ -97,7 +95,6 @@ const mapStateToProps = (state) => { totalUnapprovedCount, connectedStatusPopoverHasBeenShown, defaultHomeActiveTabName, - swapsWelcomeMessageHasBeenShown: getSwapsWelcomeMessageSeenStatus(state), haveSwapsQuotes: Boolean(Object.values(swapsState.quotes || {}).length), swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', @@ -106,6 +103,8 @@ const mapStateToProps = (state) => { shouldShowWeb3ShimUsageNotification, pendingConfirmations, infuraBlocked: getInfuraBlocked(state), + notificationsToShow: getSortedNotificationsToShow(state).length > 0, + showWhatsNewPopup: getShowWhatsNewPopup(state), }; }; @@ -126,12 +125,11 @@ const mapDispatchToProps = (dispatch) => ({ setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()), onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)), - setSwapsWelcomeMessageHasBeenShown: () => - dispatch(setSwapsWelcomeMessageHasBeenShown()), setWeb3ShimUsageAlertDismissed: (origin) => setWeb3ShimUsageAlertDismissed(origin), disableWeb3ShimUsageAlert: () => setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), + hideWhatsNewPopup: () => dispatch(hideWhatsNewPopup()), }); export default compose( diff --git a/ui/app/pages/home/index.scss b/ui/app/pages/home/index.scss index 2841155dd..d3eaefa78 100644 --- a/ui/app/pages/home/index.scss +++ b/ui/app/pages/home/index.scss @@ -124,4 +124,13 @@ height: 100%; width: 100%; } + + &__support { + padding: 10px 0 20px 0; + text-align: center; + + a { + color: $primary-1; + } + } } 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 ? (