From 13c758a831ad1dbbdd4a3cacfd6dd999c45f9bba Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Wed, 9 Oct 2024 17:09:23 +0000 Subject: [PATCH 01/41] Version v12.4.1 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5fbcff7a94f..7cae12b166c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.4.1] + ## [12.4.0] ### Added - Added a receive button to the home screen, allowing users to easily get their address or QR-code for receiving cryptocurrency ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148)) @@ -5139,7 +5141,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...HEAD +[12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 [12.4.0]: https://github.com/MetaMask/metamask-extension/compare/v12.3.1...v12.4.0 [12.3.1]: https://github.com/MetaMask/metamask-extension/compare/v12.3.0...v12.3.1 [12.3.0]: https://github.com/MetaMask/metamask-extension/compare/v12.2.4...v12.3.0 diff --git a/package.json b/package.json index d8fece95d0c0..19c9a6c0400d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.4.0", + "version": "12.4.1", "private": true, "repository": { "type": "git", From 3d096c2d673aa8756a27d14887f2ea748a2ea780 Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Thu, 10 Oct 2024 06:56:47 -0400 Subject: [PATCH 02/41] fix: remove old phishfort list from clients (#27743) (#27746) Cherry-pick #27743 for v12.4.1 Co-authored-by: Mark Stacey --- app/scripts/migrations/126.1.test.ts | 142 +++++++++++++++++++++++++++ app/scripts/migrations/126.1.ts | 54 ++++++++++ app/scripts/migrations/index.js | 1 + 3 files changed, 197 insertions(+) create mode 100644 app/scripts/migrations/126.1.test.ts create mode 100644 app/scripts/migrations/126.1.ts diff --git a/app/scripts/migrations/126.1.test.ts b/app/scripts/migrations/126.1.test.ts new file mode 100644 index 000000000000..0d21a675ebcc --- /dev/null +++ b/app/scripts/migrations/126.1.test.ts @@ -0,0 +1,142 @@ +import { migrate, version } from './126.1'; + +const oldVersion = 126.1; + +const mockPhishingListMetaMask = { + allowlist: [], + blocklist: ['malicious1.com'], + c2DomainBlocklist: ['malicious2.com'], + fuzzylist: [], + tolerance: 0, + version: 1, + lastUpdated: Date.now(), + name: 'MetaMask', +}; + +const mockPhishingListPhishfort = { + allowlist: [], + blocklist: ['phishfort1.com'], + c2DomainBlocklist: ['phishfort2.com'], + fuzzylist: [], + tolerance: 0, + version: 1, + lastUpdated: Date.now(), + name: 'Phishfort', +}; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('keeps only the MetaMask phishing list in PhishingControllerState', async () => { + const oldState = { + PhishingController: { + phishingLists: [mockPhishingListMetaMask, mockPhishingListPhishfort], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([ + mockPhishingListMetaMask, + ]); + }); + + it('removes all phishing lists if MetaMask is not present', async () => { + const oldState = { + PhishingController: { + phishingLists: [mockPhishingListPhishfort], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([]); + }); + + it('does nothing if PhishingControllerState is empty', async () => { + const oldState = { + PhishingController: { + phishingLists: [], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + const updatedPhishingController = transformedState.data + .PhishingController as Record; + + expect(updatedPhishingController.phishingLists).toStrictEqual([]); + }); + + it('does nothing if PhishingController is not in the state', async () => { + const oldState = { + NetworkController: { + providerConfig: { + chainId: '0x1', + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('does nothing if phishingLists is not an array (null)', async () => { + const oldState: Record = { + PhishingController: { + phishingLists: null, + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + c2DomainBlocklistLastFetched: 0, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/126.1.ts b/app/scripts/migrations/126.1.ts new file mode 100644 index 000000000000..81e609e672f1 --- /dev/null +++ b/app/scripts/migrations/126.1.ts @@ -0,0 +1,54 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 126.1; + +/** + * This migration removes `providerConfig` from the network controller state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record, +): Record { + if ( + hasProperty(state, 'PhishingController') && + isObject(state.PhishingController) && + hasProperty(state.PhishingController, 'phishingLists') + ) { + const phishingController = state.PhishingController; + + if (!Array.isArray(phishingController.phishingLists)) { + console.error( + `Migration ${version}: Invalid PhishingController.phishingLists state`, + ); + return state; + } + + phishingController.phishingLists = phishingController.phishingLists.filter( + (list) => list.name === 'MetaMask', + ); + + state.PhishingController = phishingController; + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index e5cfb6218019..119dfd79ede1 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -146,6 +146,7 @@ const migrations = [ require('./125'), require('./125.1'), require('./126'), + require('./126.1'), ]; export default migrations; From 3ebc8a73f9aed0bef56f1230027b104a92a61bae Mon Sep 17 00:00:00 2001 From: Jack Clancy Date: Thu, 10 Oct 2024 14:06:50 +0100 Subject: [PATCH 03/41] fix: cherry pick swaps undefined object access crash hotfix into v12.4.1 RC (#27757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Cherry picks swaps undefined object access crash hotfix intov12.4.1RC. See [here](https://github.com/MetaMask/metamask-extension/pull/27708) for more info [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27757?quickstart=1) ## **Related issues** https://consensyssoftware.atlassian.net/jira/software/projects/MMS/boards/447/backlog?assignee=5ae37c7e42b8a62c4e15d92a&selectedIssue=MMS-1569 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Mark Stacey --- CHANGELOG.md | 2 ++ ui/pages/swaps/prepare-swap-page/prepare-swap-page.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cae12b166c2..d69b3b7c6fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [12.4.1] +### Fixed +- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708)) ## [12.4.0] ### Added diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 98bb6933d0c3..45120d9f6a6b 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -52,6 +52,7 @@ import { getTransactionSettingsOpened, setTransactionSettingsOpened, getLatestAddedTokenTo, + getUsedQuote, } from '../../../ducks/swaps/swaps'; import { getSwapsDefaultToken, @@ -190,9 +191,10 @@ export default function PrepareSwapPage({ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); const tokenList = useSelector(getTokenList, isEqual); const quotes = useSelector(getQuotes, isEqual); + const usedQuote = useSelector(getUsedQuote, isEqual); const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual); const numberOfQuotes = Object.keys(quotes).length; - const areQuotesPresent = numberOfQuotes > 0; + const areQuotesPresent = numberOfQuotes > 0 && usedQuote; const swapsErrorKey = useSelector(getSwapsErrorKey); const aggregatorMetadata = useSelector(getAggregatorMetadata, shallowEqual); const transactionSettingsOpened = useSelector( From ac46289e78a401af2dc2bcdd7a387cfdd1549e6b Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 10 Oct 2024 16:03:52 -0230 Subject: [PATCH 04/41] Update CHANGELOG.md for v12.4.1 (#27775) --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d69b3b7c6fb1..f7b07834e387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [12.4.1] ### Fixed -- Fix crash on swaps review page ([27708](https://github.com/MetaMask/metamask-extension/pull/27708)) +- Fix crash on swaps review page ([#27708](https://github.com/MetaMask/metamask-extension/pull/27708)) +- Fix bug that could prevent the phishing detection feature from having the most up to date info on which web pages to block ([#27743](https://github.com/MetaMask/metamask-extension/pull/27743)) ## [12.4.0] ### Added From c678db23f437865334c9c4362d52827451036928 Mon Sep 17 00:00:00 2001 From: David Drazic Date: Mon, 14 Oct 2024 15:11:12 +0200 Subject: [PATCH 05/41] fix: sticky footer UI issue on Snaps Home Page in extended view (#27799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with sticky Snaps UI Footer component in extended view. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27799?quickstart=1) ## **Related issues** Fixes: n/a ## **Manual testing steps** 1. Try all the Snaps that use custom footer (Home Page Snap, Custom Dialog Snap with custom UI, etc.). 2. Make sure that footer has correct width matching the width of the content view. ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/f2a2c924-2bd9-451e-9b26-aadda9e94b22) ### **After** ![Screenshot 2024-10-11 at 20 38 13](https://github.com/user-attachments/assets/644d97f6-89b3-4971-bc8e-d51322888788) ![Screenshot 2024-10-11 at 20 38 48](https://github.com/user-attachments/assets/b65b113c-8aa1-4d54-b70c-ab88dad41505) ![Screenshot 2024-10-11 at 20 40 55](https://github.com/user-attachments/assets/215ae7a8-4e20-4a6b-a2ff-f4276515ded4) ![Screenshot 2024-10-11 at 20 41 15](https://github.com/user-attachments/assets/fa3f324d-3fc3-473b-81ea-bc4ce65ebaf3) ![Screenshot 2024-10-11 at 20 56 29](https://github.com/user-attachments/assets/6ad44d4b-a869-4f2a-9043-397e5730e757) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/snaps/snap-ui-renderer/index.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index b1bc569af333..7e18e72c917f 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -1,4 +1,10 @@ +@use "design-system"; + .snap-ui-renderer { + $width-screen-sm-min: 85vw; + $width-screen-md-min: 80vw; + $width-screen-lg-min: 62vw; + &__content { margin-bottom: 0 !important; } @@ -69,5 +75,17 @@ &__footer { margin-top: auto; + + @include design-system.screen-sm-min { + max-width: $width-screen-sm-min; + } + + @include design-system.screen-md-min { + max-width: $width-screen-md-min; + } + + @include design-system.screen-lg-min { + max-width: $width-screen-lg-min; + } } } From ca14e7b82ed7889c3c2d52664edff38bfcdcfbe3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 14 Oct 2024 15:28:11 +0200 Subject: [PATCH 06/41] ci: Revert minimum E2E timeout to 20 minutes (#27827) ## **Description** Reverts the minimum E2E timeout back to 20 minutes to unblock merges to `develop` while we investigate why E2E's have slowed down dramatically. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27827?quickstart=1) --- .circleci/scripts/test-run-e2e-timeout-minutes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/scripts/test-run-e2e-timeout-minutes.ts b/.circleci/scripts/test-run-e2e-timeout-minutes.ts index 1fc06696712a..c539133b0c60 100644 --- a/.circleci/scripts/test-run-e2e-timeout-minutes.ts +++ b/.circleci/scripts/test-run-e2e-timeout-minutes.ts @@ -2,7 +2,7 @@ import { filterE2eChangedFiles } from '../../test/e2e/changedFilesUtil'; const changedOrNewTests = filterE2eChangedFiles(); -//15 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes -const extraTime = Math.min(15 + changedOrNewTests.length * 3, 30); +// 20 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes +const extraTime = Math.min(20 + changedOrNewTests.length * 3, 30); console.log(extraTime); From 1f1e142f498c96c142731cea602abd8069b2f5e0 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 14 Oct 2024 16:19:01 +0200 Subject: [PATCH 07/41] fix: Fix Snaps usage of PhishingController (#27817) ## **Description** Fixes two problems with Snaps usage of `PhishingController`. Following https://github.com/MetaMask/metamask-extension/pull/25839 the PhishingController expects full URLs instead of hostnames as the input to `testOrigin`. In that PR, the argument of `isOnPhishingList` was incorrectly changed. This PR also patches in some changes from the `snaps` repo that are currently blocked by a release: https://github.com/MetaMask/snaps/pull/2835, https://github.com/MetaMask/snaps/pull/2750 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27817?quickstart=1) ## **Manual testing steps** 1. Create a Snap that links to an URL blocked with `eth-phishing-detect` 2. See that triggering the Snap is disallowed if the user has phishing detection enabled --- ...ask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch | 120 ++++++++++++++++++ app/scripts/metamask-controller.js | 4 +- package.json | 5 +- yarn.lock | 39 +++++- 4 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 .yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch new file mode 100644 index 000000000000..3361025d4860 --- /dev/null +++ b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch @@ -0,0 +1,120 @@ +diff --git a/dist/ui.cjs b/dist/ui.cjs +index 300fe9e97bba85945e3c2d200e736987453f8268..d6fa322e2b3629f41d653b91db52c3db85064276 100644 +--- a/dist/ui.cjs ++++ b/dist/ui.cjs +@@ -200,13 +200,23 @@ function getMarkdownLinks(text) { + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + function validateLink(link, isOnPhishingList) { + try { + const url = new URL(link); + (0, utils_1.assert)(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); +- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; +- (0, utils_1.assert)(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); ++ if (url.protocol === 'mailto:') { ++ const emails = url.pathname.split(','); ++ for (const email of emails) { ++ const hostname = email.split('@')[1]; ++ (0, utils_1.assert)(!hostname.includes(':')); ++ const href = `https://${hostname}`; ++ (0, utils_1.assert)(!isOnPhishingList(href), 'The specified URL is not allowed.'); ++ } ++ return; ++ } ++ (0, utils_1.assert)(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); + } + catch (error) { + throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); +diff --git a/dist/ui.cjs.map b/dist/ui.cjs.map +index 71b5ecb9eb8bc8bdf919daccf24b25737ee69819..6d6e56cd7fea85e4d477c0399506a03d465ca740 100644 +--- a/dist/ui.cjs.map ++++ b/dist/ui.cjs.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file ++{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file +diff --git a/dist/ui.d.cts b/dist/ui.d.cts +index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 +--- a/dist/ui.d.cts ++++ b/dist/ui.d.cts +@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; + /** +diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map +index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 +--- a/dist/ui.d.cts.map ++++ b/dist/ui.d.cts.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file ++{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file +diff --git a/dist/ui.d.mts b/dist/ui.d.mts +index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 +--- a/dist/ui.d.mts ++++ b/dist/ui.d.mts +@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; + /** +diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map +index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 +--- a/dist/ui.d.mts.map ++++ b/dist/ui.d.mts.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file ++{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} +\ No newline at end of file +diff --git a/dist/ui.mjs b/dist/ui.mjs +index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 +--- a/dist/ui.mjs ++++ b/dist/ui.mjs +@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { + * @param link - The link to validate. + * @param isOnPhishingList - The function that checks the link against the + * phishing list. ++ * @throws If the link is invalid. + */ + export function validateLink(link, isOnPhishingList) { + try { + const url = new URL(link); + assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); +- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; +- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); ++ if (url.protocol === 'mailto:') { ++ const emails = url.pathname.split(','); ++ for (const email of emails) { ++ const hostname = email.split('@')[1]; ++ assert(!hostname.includes(':')); ++ const href = `https://${hostname}`; ++ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); ++ } ++ return; ++ } ++ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); + } + catch (error) { + throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); +diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map +index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 +--- a/dist/ui.mjs.map ++++ b/dist/ui.mjs.map +@@ -1 +1 @@ +-{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file ++{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} +\ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 96a081e3308d..83edd7ade418 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2781,7 +2781,7 @@ export default class MetamaskController extends EventEmitter { 'PhishingController:maybeUpdateState', ); }, - isOnPhishingList: (sender) => { + isOnPhishingList: (url) => { const { usePhishDetect } = this.preferencesController.store.getState(); @@ -2791,7 +2791,7 @@ export default class MetamaskController extends EventEmitter { return this.controllerMessenger.call( 'PhishingController:testOrigin', - sender.url, + url, ).result; }, createInterface: this.controllerMessenger.call.bind( diff --git a/package.json b/package.json index 201f76f3c473..90da21554bc2 100644 --- a/package.json +++ b/package.json @@ -264,7 +264,8 @@ "@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "path-to-regexp": "1.9.0" + "path-to-regexp": "1.9.0", + "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -356,7 +357,7 @@ "@metamask/snaps-execution-environments": "^6.7.2", "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "^8.1.1", + "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", diff --git a/yarn.lock b/yarn.lock index a003e9a42cd4..bc7fc36aabbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6357,6 +6357,37 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:8.1.1": + version: 8.1.1 + resolution: "@metamask/snaps-utils@npm:8.1.1" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@metamask/base-controller": "npm:^6.0.2" + "@metamask/key-tree": "npm:^9.1.2" + "@metamask/permission-controller": "npm:^11.0.0" + "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/slip44": "npm:^4.0.0" + "@metamask/snaps-registry": "npm:^3.2.1" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.2.1" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.1" + chalk: "npm:^4.1.2" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + fast-json-stable-stringify: "npm:^2.1.0" + fast-xml-parser: "npm:^4.4.1" + marked: "npm:^12.0.1" + rfdc: "npm:^1.3.0" + semver: "npm:^7.5.4" + ses: "npm:^1.1.0" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" @@ -6388,9 +6419,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1": +"@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch": version: 8.1.1 - resolution: "@metamask/snaps-utils@npm:8.1.1" + resolution: "@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch::version=8.1.1&hash=d09097" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6415,7 +6446,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655 languageName: node linkType: hard @@ -26149,7 +26180,7 @@ __metadata: "@metamask/snaps-execution-environments": "npm:^6.7.2" "@metamask/snaps-rpc-methods": "npm:^11.1.1" "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.2.0" From bf318762609ee65c5ca3e8e117a1f1ecf1ae3d5c Mon Sep 17 00:00:00 2001 From: Jony Bursztyn Date: Mon, 14 Oct 2024 16:12:43 +0100 Subject: [PATCH 08/41] feat: remove phishing detection from onboarding Security group (#27819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Screenshot 2024-10-14 at 11 57 53` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27819?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3497 ## **Manual testing steps** 1. Onboard 2. Go to "Reminder set!" screen 3. Click on "Manage default settings" 4. Click on "Security" 5. Check that there's no ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../privacy-settings/privacy-settings.js | 15 --------------- .../privacy-settings/privacy-settings.test.js | 11 ++--------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index ca3bd0af2ff4..ee11f63caf2a 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -57,7 +57,6 @@ import { setIpfsGateway, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, - setUsePhishDetect, setUse4ByteResolution, setUseTokenDetection, setUseAddressBarEnsResolution, @@ -132,7 +131,6 @@ export default function PrivacySettings() { } = defaultState; const petnamesEnabled = useSelector(getPetnamesEnabled); - const [usePhishingDetection, setUsePhishingDetection] = useState(null); const [turnOn4ByteResolution, setTurnOn4ByteResolution] = useState(use4ByteResolution); const [turnOnTokenDetection, setTurnOnTokenDetection] = @@ -160,17 +158,11 @@ export default function PrivacySettings() { getExternalServicesOnboardingToggleState, ); - const phishingToggleState = - usePhishingDetection === null - ? externalServicesOnboardingToggleState - : usePhishingDetection; - const profileSyncingProps = useProfileSyncingProps( externalServicesOnboardingToggleState, ); const handleSubmit = () => { - dispatch(setUsePhishDetect(phishingToggleState)); dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); dispatch( @@ -199,7 +191,6 @@ export default function PrivacySettings() { is_profile_syncing_enabled: profileSyncingProps.isProfileSyncingEnabled, is_basic_functionality_enabled: externalServicesOnboardingToggleState, show_incoming_tx: incomingTransactionsPreferences, - use_phising_detection: usePhishingDetection, turnon_token_detection: turnOnTokenDetection, }, }); @@ -720,12 +711,6 @@ export default function PrivacySettings() { ) : null} {selectedItem && selectedItem.id === 3 ? ( <> - { [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }, - usePhishDetect: true, use4ByteResolution: true, useTokenDetection: false, useCurrencyRateCheck: true, @@ -59,7 +58,6 @@ describe('Privacy Settings Onboarding View', () => { const store = configureMockStore([thunk])(mockStore); const setFeatureFlagStub = jest.fn(); - const setUsePhishDetectStub = jest.fn(); const setUse4ByteResolutionStub = jest.fn(); const setUseTokenDetectionStub = jest.fn(); const setUseCurrencyRateCheckStub = jest.fn(); @@ -79,7 +77,6 @@ describe('Privacy Settings Onboarding View', () => { setBackgroundConnection({ setFeatureFlag: setFeatureFlagStub, - setUsePhishDetect: setUsePhishDetectStub, setUse4ByteResolution: setUse4ByteResolutionStub, setUseTokenDetection: setUseTokenDetectionStub, setUseCurrencyRateCheck: setUseCurrencyRateCheckStub, @@ -104,7 +101,6 @@ describe('Privacy Settings Onboarding View', () => { ); // All settings are initialized toggled to be same as default expect(toggleExternalServicesStub).toHaveBeenCalledTimes(0); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(0); expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(0); expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0); expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(0); @@ -148,9 +144,8 @@ describe('Privacy Settings Onboarding View', () => { toggles = container.querySelectorAll('input[type=checkbox]'); - fireEvent.click(toggles[0]); // setUsePhishDetectStub - fireEvent.click(toggles[1]); // setUse4ByteResolutionStub - fireEvent.click(toggles[2]); // setPreferenceStub + fireEvent.click(toggles[0]); // setUse4ByteResolutionStub + fireEvent.click(toggles[1]); // setPreferenceStub fireEvent.click(backButton); @@ -179,8 +174,6 @@ describe('Privacy Settings Onboarding View', () => { false, ); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); - expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); expect(setPreferenceStub).toHaveBeenCalledTimes(1); From d9d6fab1be802871131ce772a570ee7b0a81f4e9 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Mon, 14 Oct 2024 16:20:57 +0100 Subject: [PATCH 09/41] fix: no connected state for permissions page (#27660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to add padding to the permissions page when no site or snap is connected ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to All permissions page 2. Disconnect all sites and snaps 3. Check the copy is center aligned when not connected ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-07 at 2 02 28 PM](https://github.com/user-attachments/assets/b3a46aba-a5f0-4ad3-a1dc-4e59d6d70c88) ### **After** ![Screenshot 2024-10-07 at 2 02 10 PM](https://github.com/user-attachments/assets/07ccf534-3c8e-4ee4-904a-77c6ed0a606a) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../multichain/pages/permissions-page/permissions-page.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index 491e041d7ac5..8cdeae0ed57d 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -120,6 +120,7 @@ export const PermissionsPage = () => { justifyContent={JustifyContent.center} height={BlockSize.Full} gap={2} + padding={4} > Date: Mon, 14 Oct 2024 16:36:42 +0100 Subject: [PATCH 10/41] feat: Added metrics for edit networks and accounts (#27820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to add metrics for edit networks and accounts screen ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3263](https://github.com/MetaMask/MetaMask-planning/issues/3263) ## **Manual testing steps** 1. Go to ui/contexts/metametrics.js 2. In trackEvent in this file, add a log to see the payload 3. Run extension with yarn start. Go to Permissions Page, click on edit button for accounts and networks and check the console to verify the payload. In edit modals, if we update accounts or networks. Check the payload as well ## **Screenshots/Recordings** ### **Before** NA ### **After** NA ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/constants/metametrics.ts | 6 ++++ .../edit-accounts-modal.tsx | 34 ++++++++++++++++++- .../edit-networks-modal.js | 31 +++++++++++++++-- .../site-cell/site-cell.tsx | 31 ++++++++++++++--- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index b3e6f252d23d..544d24ce1271 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -562,6 +562,10 @@ export enum MetaMetricsEventName { NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', NavPermissionsOpened = 'Permissions Opened', + UpdatePermissionedNetworks = 'Update Permissioned Networks', + UpdatePermissionedAccounts = 'Update Permissioned Accounts', + ViewPermissionedNetworks = 'View Permissioned Networks', + ViewPermissionedAccounts = 'View Permissioned Accounts', NavNetworkMenuOpened = 'Network Menu Opened', NavSettingsOpened = 'Settings Opened', NavAccountSwitched = 'Account Switched', @@ -782,6 +786,8 @@ export enum MetaMetricsEventCategory { NotificationsActivationFlow = 'Notifications Activation Flow', NotificationSettings = 'Notification Settings', Petnames = 'Petnames', + // eslint-disable-next-line @typescript-eslint/no-shadow + Permissions = 'Permissions', Phishing = 'Phishing', ProfileSyncing = 'Profile Syncing', PushNotifications = 'Notifications', diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx index d9303951af2d..ba842efc6a11 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Modal, @@ -30,6 +30,11 @@ import { } from '../../../helpers/constants/design-system'; import { getURLHost } from '../../../helpers/utils/util'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; type EditAccountsModalProps = { activeTabOrigin: string; @@ -47,6 +52,8 @@ export const EditAccountsModal: React.FC = ({ onSubmit, }) => { const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const [showAddNewAccounts, setShowAddNewAccounts] = useState(false); const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( @@ -85,6 +92,9 @@ export const EditAccountsModal: React.FC = ({ const hostName = getURLHost(activeTabOrigin); + const defaultSet = new Set(defaultSelectedAccountAddresses); + const selectedSet = new Set(selectedAccountAddresses); + return ( <> = ({ { + // Get accounts that are in `selectedAccountAddresses` but not in `defaultSelectedAccountAddresses` + const addedAccounts = selectedAccountAddresses.filter( + (address) => !defaultSet.has(address), + ); + + // Get accounts that are in `defaultSelectedAccountAddresses` but not in `selectedAccountAddresses` + const removedAccounts = + defaultSelectedAccountAddresses.filter( + (address) => !selectedSet.has(address), + ); + onSubmit(selectedAccountAddresses); + trackEvent({ + category: MetaMetricsEventCategory.Permissions, + event: + MetaMetricsEventName.UpdatePermissionedAccounts, + properties: { + addedAccounts: addedAccounts.length, + removedAccounts: removedAccounts.length, + location: 'Edit Accounts Modal', + }, + }); + onClose(); }} size={ButtonPrimarySize.Lg} diff --git a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js index 8f599d149689..e4a7c391b4df 100644 --- a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js +++ b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { AlignItems, @@ -28,6 +28,11 @@ import { import { NetworkListItem } from '..'; import { getURLHost } from '../../../helpers/utils/util'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; export const EditNetworksModal = ({ activeTabOrigin, @@ -38,7 +43,7 @@ export const EditNetworksModal = ({ onSubmit, }) => { const t = useI18nContext(); - + const trackEvent = useContext(MetaMetricsContext); const allNetworks = [...nonTestNetworks, ...testNetworks]; const [selectedChainIds, setSelectedChainIds] = useState( @@ -77,6 +82,9 @@ export const EditNetworksModal = ({ const hostName = getURLHost(activeTabOrigin); + const defaultChainIdsSet = new Set(defaultSelectedChainIds); + const selectedChainIdsSet = new Set(selectedChainIds); + return ( { onSubmit(selectedChainIds); + // Get networks that are in `selectedChainIds` but not in `defaultSelectedChainIds` + const addedNetworks = selectedChainIds.filter( + (chainId) => !defaultChainIdsSet.has(chainId), + ); + + // Get networks that are in `defaultSelectedChainIds` but not in `selectedChainIds` + const removedNetworks = defaultSelectedChainIds.filter( + (chainId) => !selectedChainIdsSet.has(chainId), + ); + + trackEvent({ + category: MetaMetricsEventCategory.Permissions, + event: MetaMetricsEventName.UpdatePermissionedNetworks, + properties: { + addedNetworks: addedNetworks.length, + removedNetworks: removedNetworks.length, + location: 'Edit Networks Modal', + }, + }); onClose(); }} size={ButtonPrimarySize.Lg} diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index 4bc42604adf3..bb3a14a8f5e8 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { Hex } from '@metamask/utils'; import { BackgroundColor, @@ -14,6 +14,11 @@ import { } from '../../../../component-library'; import { EditAccountsModal, EditNetworksModal } from '../../..'; import { MergedInternalAccount } from '../../../../../selectors/selectors.types'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../../../shared/constants/metametrics'; import { SiteCellTooltip } from './site-cell-tooltip'; import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; @@ -47,7 +52,7 @@ export const SiteCell: React.FC = ({ isConnectFlow, }) => { const t = useI18nContext(); - + const trackEvent = useContext(MetaMetricsContext); const allNetworks = [...nonTestNetworks, ...testNetworks]; const [showEditAccountsModal, setShowEditAccountsModal] = useState(false); @@ -90,7 +95,16 @@ export const SiteCell: React.FC = ({ connectedMessage={accountMessageConnectedState} unconnectedMessage={accountMessageNotConnectedState} isConnectFlow={isConnectFlow} - onClick={() => setShowEditAccountsModal(true)} + onClick={() => { + setShowEditAccountsModal(true); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'Connect view, Permissions toast, Permissions (dapp)', + }, + }); + }} paddingBottomValue={2} paddingTopValue={0} content={ @@ -114,7 +128,16 @@ export const SiteCell: React.FC = ({ ])} unconnectedMessage={t('requestingFor')} isConnectFlow={isConnectFlow} - onClick={() => setShowEditNetworksModal(true)} + onClick={() => { + setShowEditNetworksModal(true); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'Connect view, Permissions toast, Permissions (dapp)', + }, + }); + }} paddingTopValue={2} paddingBottomValue={0} content={} From acbb17e26382bbd86f3407ac85c5ff2e38a75034 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 14 Oct 2024 09:53:18 -0700 Subject: [PATCH 11/41] revert: use networkClientId to resolve chainId in PPOM Middleware (#27570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Reverts [this PPOM change](https://github.com/MetaMask/metamask-extension/pull/27263) due to [issue with network configuration for the newly added rpc endpoint not being available when queried immediately after being added](https://github.com/MetaMask/metamask-extension/issues/27447) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27570?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/ppom/ppom-middleware.test.ts | 139 +++++++++++-------- app/scripts/lib/ppom/ppom-middleware.ts | 21 ++- app/scripts/metamask-controller.js | 1 - 3 files changed, 89 insertions(+), 72 deletions(-) diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index aafde70f2072..d0adbefb264b 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -8,6 +8,7 @@ import { BlockaidResultType, } from '../../../../shared/constants/security-provider'; import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { mockNetworkState } from '../../../../test/stub/networks'; import { createPPOMMiddleware, PPOMMiddlewareRequest } from './ppom-middleware'; import { generateSecurityAlertId, @@ -36,18 +37,22 @@ const REQUEST_MOCK = { params: [], id: '', jsonrpc: '2.0' as const, - origin: 'test.com', - networkClientId: 'networkClientId', }; const createMiddleware = ( options: { - chainId?: Hex; + chainId?: Hex | null; error?: Error; securityAlertsEnabled?: boolean; - } = {}, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateSecurityAlertResponse?: any; + } = { + updateSecurityAlertResponse: () => undefined, + }, ) => { - const { chainId, error, securityAlertsEnabled } = options; + const { chainId, error, securityAlertsEnabled, updateSecurityAlertResponse } = + options; const ppomController = {}; @@ -66,9 +71,10 @@ const createMiddleware = ( } const networkController = { - getNetworkConfigurationByNetworkClientId: jest - .fn() - .mockReturnValue({ chainId: chainId || CHAIN_IDS.MAINNET }), + state: { + ...mockNetworkState({ chainId: chainId || CHAIN_IDS.MAINNET }), + ...(chainId === null ? { providerConfig: {} } : undefined), + }, }; const appStateController = { @@ -79,9 +85,7 @@ const createMiddleware = ( listAccounts: () => [{ address: INTERNAL_ACCOUNT_ADDRESS }], }; - const updateSecurityAlertResponse = jest.fn(); - - const middleware = createPPOMMiddleware( + return createPPOMMiddleware( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any ppomController as any, @@ -98,16 +102,6 @@ const createMiddleware = ( accountsController as any, updateSecurityAlertResponse, ); - - return { - middleware, - ppomController, - preferenceController, - networkController, - appStateController, - accountsController, - updateSecurityAlertResponse, - }; }; describe('PPOMMiddleware', () => { @@ -135,29 +129,12 @@ describe('PPOMMiddleware', () => { }; }); - it('gets the network configuration for the request networkClientId', async () => { - const { middleware, networkController } = createMiddleware(); - - const req = { - ...REQUEST_MOCK, - method: 'eth_sendTransaction', - securityAlertResponse: undefined, - }; - - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); - - await flushPromises(); - - expect( - networkController.getNetworkConfigurationByNetworkClientId, - ).toHaveBeenCalledTimes(1); - expect( - networkController.getNetworkConfigurationByNetworkClientId, - ).toHaveBeenCalledWith('networkClientId'); - }); - it('updates alert response after validating request', async () => { - const { middleware, updateSecurityAlertResponse } = createMiddleware(); + const updateSecurityAlertResponse = jest.fn(); + + const middlewareFunction = createMiddleware({ + updateSecurityAlertResponse, + }); const req = { ...REQUEST_MOCK, @@ -165,7 +142,11 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); await flushPromises(); @@ -178,7 +159,7 @@ describe('PPOMMiddleware', () => { }); it('adds loading response to confirmation requests while validation is in progress', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req: PPOMMiddlewareRequest<(string | { to: string })[]> = { ...REQUEST_MOCK, @@ -186,7 +167,11 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse?.reason).toBe(BlockaidReason.inProgress); expect(req.securityAlertResponse?.result_type).toBe( @@ -195,7 +180,7 @@ describe('PPOMMiddleware', () => { }); it('does not do validation if the user has not enabled the preference', async () => { - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ securityAlertsEnabled: false, }); @@ -206,7 +191,29 @@ describe('PPOMMiddleware', () => { }; // @ts-expect-error Passing in invalid input for testing purposes - await middleware(req, undefined, () => undefined); + await middlewareFunction(req, undefined, () => undefined); + + expect(req.securityAlertResponse).toBeUndefined(); + expect(validateRequestWithPPOM).not.toHaveBeenCalled(); + }); + + it('does not do validation if unable to get the chainId from the network provider config', async () => { + isChainSupportedMock.mockResolvedValue(false); + const middlewareFunction = createMiddleware({ + chainId: null, + }); + + const req = { + ...REQUEST_MOCK, + method: 'eth_sendTransaction', + securityAlertResponse: undefined, + }; + + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); @@ -214,7 +221,7 @@ describe('PPOMMiddleware', () => { it('does not do validation if user is not on a supported network', async () => { isChainSupportedMock.mockResolvedValue(false); - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ chainId: '0x2', }); @@ -224,14 +231,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation when request is not for confirmation method', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req = { ...REQUEST_MOCK, @@ -239,14 +250,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation when request is send to users own account', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const req = { ...REQUEST_MOCK, @@ -255,14 +270,18 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('does not do validation for SIWE signature', async () => { - const { middleware } = createMiddleware({ + const middlewareFunction = createMiddleware({ securityAlertsEnabled: true, }); @@ -283,17 +302,17 @@ describe('PPOMMiddleware', () => { detectSIWEMock.mockReturnValue({ isSIWEMessage: true } as SIWEMessage); // @ts-expect-error Passing invalid input for testing purposes - await middleware(req, undefined, () => undefined); + await middlewareFunction(req, undefined, () => undefined); expect(req.securityAlertResponse).toBeUndefined(); expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); it('calls next method', async () => { - const { middleware } = createMiddleware(); + const middlewareFunction = createMiddleware(); const nextMock = jest.fn(); - await middleware( + await middlewareFunction( { ...REQUEST_MOCK, method: 'eth_sendTransaction' }, { ...JsonRpcResponseStruct.TYPE }, nextMock, @@ -308,7 +327,7 @@ describe('PPOMMiddleware', () => { const nextMock = jest.fn(); - const { middleware } = createMiddleware({ error }); + const middlewareFunction = createMiddleware({ error }); const req = { ...REQUEST_MOCK, @@ -316,7 +335,7 @@ describe('PPOMMiddleware', () => { securityAlertResponse: undefined, }; - await middleware(req, { ...JsonRpcResponseStruct.TYPE }, nextMock); + await middlewareFunction(req, { ...JsonRpcResponseStruct.TYPE }, nextMock); expect(req.securityAlertResponse).toStrictEqual( SECURITY_ALERT_RESPONSE_MOCK, diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 5b9107337a05..1bad576e3881 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -1,9 +1,6 @@ import { AccountsController } from '@metamask/accounts-controller'; import { PPOMController } from '@metamask/ppom-validator'; -import { - NetworkClientId, - NetworkController, -} from '@metamask/network-controller'; +import { NetworkController } from '@metamask/network-controller'; import { Json, JsonRpcParams, @@ -17,6 +14,8 @@ import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import PreferencesController from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; +// eslint-disable-next-line import/no-restricted-paths +import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; import { generateSecurityAlertId, @@ -35,7 +34,6 @@ const CONFIRMATION_METHODS = Object.freeze([ export type PPOMMiddlewareRequest< Params extends JsonRpcParams = JsonRpcParams, > = Required> & { - networkClientId: NetworkClientId; securityAlertResponse?: SecurityAlertResponse | undefined; traceContext?: TraceContext; }; @@ -81,13 +79,14 @@ export function createPPOMMiddleware< const securityAlertsEnabled = preferencesController.store.getState()?.securityAlertsEnabled; - // This will always exist as the SelectedNetworkMiddleware - // adds networkClientId to the request before this middleware runs const { chainId } = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - networkController.getNetworkConfigurationByNetworkClientId( - req.networkClientId, - )!; + getProviderConfig({ + metamask: networkController.state, + }) ?? {}; + if (!chainId) { + return; + } + const isSupportedChain = await isChainSupported(chainId); if ( diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 83edd7ade418..98af29fe38f1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5624,7 +5624,6 @@ export default class MetamaskController extends EventEmitter { engine.push(createTracingMiddleware()); - // PPOMMiddleware come after the SelectedNetworkMiddleware engine.push( createPPOMMiddleware( this.ppomController, From cedabc62e45601c77871689425320c54d717275e Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Mon, 14 Oct 2024 18:29:15 +0100 Subject: [PATCH 12/41] feat: preferences controller to base controller v2 (#27398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In this PR, we want to bring PreferencesController up to date with our latest controller patterns by upgrading to BaseControllerV2. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27398?quickstart=1) ## **Related issues** Fixes: #25917 ## **Manual testing steps** Use case 1 1. Install the previous release 2. Complete user onboarding 3. Go to settings and change couple of user settings. For example language, currency and theme. 4. Close and disable MM in the extension 5. Checkout the version with these changes 6. Build and login 7. Make sure, the user preferences set earlier are still there Use case 2 1. Disable all the MM extensions 2. Install the version with these changes 3. When you click on MM, the default language should be English 4. Complete user onboarding 5. Go to settings and change couple of user settings. For example language, currency and theme. 6. Close and disable and enable the MM in extension. which forces user to login MM in the extension 7. Once you login again, make sure, the user preferences set earlier are still there ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Co-authored-by: MetaMask Bot Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- .eslintrc.js | 2 +- app/scripts/background.js | 5 +- .../account-tracker-controller.test.ts | 11 +- .../controllers/account-tracker-controller.ts | 18 +- app/scripts/controllers/app-state.js | 23 +- app/scripts/controllers/app-state.test.js | 7 +- app/scripts/controllers/metametrics.js | 17 +- app/scripts/controllers/metametrics.test.js | 49 +- .../controllers/mmi-controller.test.ts | 13 +- app/scripts/controllers/mmi-controller.ts | 2 +- .../preferences-controller.test.ts | 653 ++++++++++++------ .../controllers/preferences-controller.ts | 642 +++++++++++------ app/scripts/lib/backup.js | 6 +- app/scripts/lib/backup.test.js | 24 +- .../createRPCMethodTrackingMiddleware.test.js | 10 +- app/scripts/lib/ppom/ppom-middleware.test.ts | 14 +- app/scripts/lib/ppom/ppom-middleware.ts | 5 +- app/scripts/metamask-controller.js | 156 ++--- app/scripts/metamask-controller.test.js | 26 +- .../files-to-convert.json | 2 - lavamoat/browserify/beta/policy.json | 6 + lavamoat/browserify/flask/policy.json | 6 + lavamoat/browserify/main/policy.json | 6 + lavamoat/browserify/mmi/policy.json | 6 + package.json | 1 + shared/constants/mmi-controller.ts | 2 +- test/e2e/default-fixture.js | 26 + test/e2e/fixture-builder.js | 28 + ...rs-after-init-opt-in-background-state.json | 4 +- .../errors-after-init-opt-in-ui-state.json | 2 + ...s-before-init-opt-in-background-state.json | 4 +- .../errors-before-init-opt-in-ui-state.json | 4 +- yarn.lock | 13 + 33 files changed, 1147 insertions(+), 646 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 64c51bd1e503..a53619b179ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -316,7 +316,7 @@ module.exports = { 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', - 'app/scripts/controllers/preferences.test.js', + 'app/scripts/controllers/preferences-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/background.js b/app/scripts/background.js index 4fbbee449160..7d9d0f5684a6 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -235,7 +235,7 @@ function maybeDetectPhishing(theController) { return {}; } - const prefState = theController.preferencesController.store.getState(); + const prefState = theController.preferencesController.state; if (!prefState.usePhishDetect) { return {}; } @@ -758,8 +758,7 @@ export function setupController( controller.preferencesController, ), getUseAddressBarEnsResolution: () => - controller.preferencesController.store.getState() - .useAddressBarEnsResolution, + controller.preferencesController.state.useAddressBarEnsResolution, provider: controller.provider, }); diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts index dbabb927fa71..ad33541fb5b6 100644 --- a/app/scripts/controllers/account-tracker-controller.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -5,7 +5,6 @@ import { BlockTracker, Provider } from '@metamask/network-controller'; import { flushPromises } from '../../../test/lib/timer-helpers'; import { createTestProviderTools } from '../../../test/stub/provider'; -import PreferencesController from './preferences-controller'; import type { AccountTrackerControllerOptions, AllowedActions, @@ -166,13 +165,9 @@ function withController( provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), - preferencesController: { - store: { - getState: () => ({ - useMultiAccountBalanceChecker, - }), - }, - } as PreferencesController, + preferencesControllerState: { + useMultiAccountBalanceChecker, + }, messenger: controllerMessenger.getRestricted({ name: 'AccountTrackerController', allowedActions: [ diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts index e2c78ea3f3f9..ec4789189a0c 100644 --- a/app/scripts/controllers/account-tracker-controller.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -45,7 +45,7 @@ import type { OnboardingControllerGetStateAction, OnboardingControllerStateChangeEvent, } from './onboarding'; -import PreferencesController from './preferences-controller'; +import { PreferencesControllerState } from './preferences-controller'; // Unique name for the controller const controllerName = 'AccountTrackerController'; @@ -170,7 +170,7 @@ export type AccountTrackerControllerOptions = { provider: Provider; blockTracker: BlockTracker; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; - preferencesController: PreferencesController; + preferencesControllerState: Partial; }; /** @@ -198,7 +198,7 @@ export default class AccountTrackerController extends BaseController< #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesController: AccountTrackerControllerOptions['preferencesController']; + #preferencesControllerState: AccountTrackerControllerOptions['preferencesControllerState']; #selectedAccount: InternalAccount; @@ -209,7 +209,7 @@ export default class AccountTrackerController extends BaseController< * @param options.provider - An EIP-1193 provider instance that uses the current global network * @param options.blockTracker - A block tracker, which emits events for each new block * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration - * @param options.preferencesController - The preferences controller + * @param options.preferencesControllerState - The state of preferences controller */ constructor(options: AccountTrackerControllerOptions) { super({ @@ -226,7 +226,7 @@ export default class AccountTrackerController extends BaseController< this.#blockTracker = options.blockTracker; this.#getNetworkIdentifier = options.getNetworkIdentifier; - this.#preferencesController = options.preferencesController; + this.#preferencesControllerState = options.preferencesControllerState; // subscribe to account removal this.messagingSystem.subscribe( @@ -257,7 +257,7 @@ export default class AccountTrackerController extends BaseController< 'AccountsController:selectedEvmAccountChange', (newAccount) => { const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + this.#preferencesControllerState; if ( this.#selectedAccount.id !== newAccount.id && @@ -672,8 +672,7 @@ export default class AccountTrackerController extends BaseController< const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let addresses = []; if (useMultiAccountBalanceChecker) { @@ -724,8 +723,7 @@ export default class AccountTrackerController extends BaseController< provider: Provider, chainId: Hex, ): Promise { - const { useMultiAccountBalanceChecker } = - this.#preferencesController.store.getState(); + const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; let balance = '0x0'; diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 3d8f9d176fb6..9dabf2313e57 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -29,7 +29,7 @@ export default class AppStateController extends EventEmitter { isUnlocked, initState, onInactiveTimeout, - preferencesStore, + preferencesController, messenger, extension, } = opts; @@ -86,12 +86,18 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); - preferencesStore.subscribe(({ preferences }) => { - const currentState = this.store.getState(); - if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { - this._setInactiveTimeout(preferences.autoLockTimeLimit); - } - }); + messenger.subscribe( + 'PreferencesController:stateChange', + ({ preferences }) => { + const currentState = this.store.getState(); + if ( + preferences && + currentState.timeoutMinutes !== preferences.autoLockTimeLimit + ) { + this._setInactiveTimeout(preferences.autoLockTimeLimit); + } + }, + ); messenger.subscribe( 'KeyringController:qrKeyringStateChange', @@ -101,7 +107,8 @@ export default class AppStateController extends EventEmitter { }), ); - const { preferences } = preferencesStore.getState(); + const { preferences } = preferencesController.state; + this._setInactiveTimeout(preferences.autoLockTimeLimit); this.messagingSystem = messenger; diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index c9ce8243b05c..46fe87d29add 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -13,13 +13,12 @@ describe('AppStateController', () => { initState, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: { call: jest.fn(() => ({ diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 15f4fa9b7788..aa5546ef7899 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -118,8 +118,9 @@ export default class MetaMetricsController { * @param {object} options * @param {object} options.segment - an instance of analytics for tracking * events that conform to the new MetaMetrics tracking plan. - * @param {object} options.preferencesStore - The preferences controller store, used - * to access and subscribe to preferences that will be attached to events + * @param {object} options.preferencesControllerState - The state of preferences controller + * @param {Function} options.onPreferencesStateChange - Used to attach a listener to the + * stateChange event emitted by the PreferencesController * @param {Function} options.onNetworkDidChange - Used to attach a listener to the * networkDidChange event emitted by the networkController * @param {Function} options.getCurrentChainId - Gets the current chain id from the @@ -132,7 +133,8 @@ export default class MetaMetricsController { */ constructor({ segment, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, onNetworkDidChange, getCurrentChainId, version, @@ -148,16 +150,15 @@ export default class MetaMetricsController { captureException(err); } }; - const prefState = preferencesStore.getState(); this.chainId = getCurrentChainId(); - this.locale = prefState.currentLocale.replace('_', '-'); + this.locale = preferencesControllerState.currentLocale.replace('_', '-'); this.version = environment === 'production' ? version : `${version}-${environment}`; this.extension = extension; this.environment = environment; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - this.selectedAddress = prefState.selectedAddress; + this.selectedAddress = preferencesControllerState.selectedAddress; ///: END:ONLY_INCLUDE_IF const abandonedFragments = omitBy(initState?.fragments, 'persist'); @@ -181,8 +182,8 @@ export default class MetaMetricsController { }, }); - preferencesStore.subscribe(({ currentLocale }) => { - this.locale = currentLocale.replace('_', '-'); + onPreferencesStateChange(({ currentLocale }) => { + this.locale = currentLocale?.replace('_', '-'); }); onNetworkDidChange(() => { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index a0505700ef01..ca5602de33c8 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -74,22 +74,6 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { - let preferencesStore = { - currentLocale, - }; - const subscribe = jest.fn(); - const updateState = (newState) => { - preferencesStore = { ...preferencesStore, ...newState }; - subscribe.mock.calls[0][0](preferencesStore); - }; - return { - getState: jest.fn().mockReturnValue(preferencesStore), - updateState, - subscribe, - }; -} - const SAMPLE_PERSISTED_EVENT = { id: 'testid', persist: true, @@ -117,7 +101,10 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, marketingCampaignCookieId = null, - preferencesStore = getMockPreferencesStore(), + preferencesControllerState = { currentLocale: LOCALE }, + onPreferencesStateChange = () => { + // do nothing + }, getCurrentChainId = () => FAKE_CHAIN_ID, onNetworkDidChange = () => { // do nothing @@ -128,7 +115,8 @@ function getMetaMetricsController({ segment: segmentInstance || segment, getCurrentChainId, onNetworkDidChange, - preferencesStore, + preferencesControllerState, + onPreferencesStateChange, version: '0.0.1', environment: 'test', initState: { @@ -209,11 +197,16 @@ describe('MetaMetricsController', function () { }); it('should update when preferences changes', function () { - const preferencesStore = getMockPreferencesStore(); + let subscribeListener; + const onPreferencesStateChange = (listener) => { + subscribeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - preferencesStore, + preferencesControllerState: { currentLocale: LOCALE }, + onPreferencesStateChange, }); - preferencesStore.updateState({ currentLocale: 'en_UK' }); + + subscribeListener({ currentLocale: 'en_UK' }); expect(metaMetricsController.locale).toStrictEqual('en-UK'); }); }); @@ -732,9 +725,11 @@ describe('MetaMetricsController', function () { it('should track a page view if isOptInPath is true and user not yet opted in', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -746,6 +741,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith( { @@ -765,9 +761,11 @@ describe('MetaMetricsController', function () { it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () { const metaMetricsController = getMetaMetricsController({ - preferencesStore: getMockPreferencesStore({ + preferencesControllerState: { + currentLocale: LOCALE, participateInMetaMetrics: null, - }), + }, + onPreferencesStateChange: jest.fn(), }); const spy = jest.spyOn(segment, 'page'); metaMetricsController.trackPage( @@ -790,6 +788,7 @@ describe('MetaMetricsController', function () { }, { isOptInPath: true }, ); + expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith( { diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 348ccd40916b..dbef190a5573 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -203,10 +203,10 @@ describe('MMIController', function () { }); metaMetricsController = new MetaMetricsController({ - preferencesStore: { - getState: jest.fn().mockReturnValue({ currentLocale: 'en' }), - subscribe: jest.fn(), + preferencesControllerState: { + currentLocale: 'en' }, + onPreferencesStateChange: jest.fn(), getCurrentChainId: jest.fn(), onNetworkDidChange: jest.fn(), }); @@ -245,13 +245,12 @@ describe('MMIController', function () { initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ + preferencesController: { + state: { preferences: { autoLockTimeLimit: 0, }, - })), + }, }, messenger: mockMessenger, }), diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index d0e905d673d8..2373484d4a6e 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -44,8 +44,8 @@ import { import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; +import { PreferencesController } from './preferences-controller'; import AccountTrackerController from './account-tracker-controller'; -import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; type UpdateCustodianTransactionsParameters = { diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index f825c1eb5aee..9c28ed7c43a0 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -3,13 +3,7 @@ */ import { ControllerMessenger } from '@metamask/base-controller'; import { AccountsController } from '@metamask/accounts-controller'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerStateChangeEvent, - KeyringControllerAccountRemovedEvent, -} from '@metamask/keyring-controller'; +import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { Hex } from '@metamask/utils'; import { CHAIN_IDS } from '../../../shared/constants/network'; @@ -18,10 +12,10 @@ import { ThemeType } from '../../../shared/constants/preferences'; import type { AllowedActions, AllowedEvents, - PreferencesControllerActions, - PreferencesControllerEvents, + PreferencesControllerMessenger, + PreferencesControllerState, } from './preferences-controller'; -import PreferencesController from './preferences-controller'; +import { PreferencesController } from './preferences-controller'; const NETWORK_CONFIGURATION_DATA = mockNetworkState( { @@ -40,102 +34,104 @@ const NETWORK_CONFIGURATION_DATA = mockNetworkState( }, ).networkConfigurationsByChainId; -describe('preferences controller', () => { - let controllerMessenger: ControllerMessenger< - | PreferencesControllerActions - | AllowedActions - | KeyringControllerGetAccountsAction - | KeyringControllerGetKeyringsByTypeAction - | KeyringControllerGetKeyringForAccountAction, - | PreferencesControllerEvents +const setupController = ({ + state, +}: { + state?: Partial; +}) => { + const controllerMessenger = new ControllerMessenger< + AllowedActions, + | AllowedEvents | KeyringControllerStateChangeEvent - | KeyringControllerAccountRemovedEvent | SnapControllerStateChangeEvent - | AllowedEvents - >; - let preferencesController: PreferencesController; - let accountsController: AccountsController; - - beforeEach(() => { - controllerMessenger = new ControllerMessenger(); - - const accountsControllerMessenger = controllerMessenger.getRestricted({ - name: 'AccountsController', - allowedEvents: [ - 'SnapController:stateChange', - 'KeyringController:accountRemoved', - 'KeyringController:stateChange', - ], - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getKeyringsByType', - 'KeyringController:getKeyringForAccount', - ], - }); - - const mockAccountsControllerState = { - internalAccounts: { - accounts: {}, - selectedAccount: '', - }, - }; - accountsController = new AccountsController({ - messenger: accountsControllerMessenger, - state: mockAccountsControllerState, - }); - - const preferencesMessenger = controllerMessenger.getRestricted({ + >(); + const preferencesControllerMessenger: PreferencesControllerMessenger = + controllerMessenger.getRestricted({ name: 'PreferencesController', allowedActions: [ - `AccountsController:setSelectedAccount`, - `AccountsController:getAccountByAddress`, - `AccountsController:setAccountName`, + 'AccountsController:getAccountByAddress', + 'AccountsController:setAccountName', + 'AccountsController:getSelectedAccount', + 'AccountsController:setSelectedAccount', + 'NetworkController:getState', ], - allowedEvents: [`AccountsController:stateChange`], + allowedEvents: ['AccountsController:stateChange'], }); - preferencesController = new PreferencesController({ - initLangCode: 'en_US', + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue({ networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - messenger: preferencesMessenger, - }); + }), + ); + const controller = new PreferencesController({ + messenger: preferencesControllerMessenger, + state, + }); + + const accountsControllerMessenger = controllerMessenger.getRestricted({ + name: 'AccountsController', + allowedEvents: [ + 'KeyringController:stateChange', + 'SnapController:stateChange', + ], + allowedActions: [], }); + const mockAccountsControllerState = { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }; + const accountsController = new AccountsController({ + messenger: accountsControllerMessenger, + state: mockAccountsControllerState, + }); + + return { + controller, + messenger: controllerMessenger, + accountsController, + }; +}; +describe('preferences controller', () => { describe('useBlockie', () => { it('defaults useBlockie to false', () => { - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - false, - ); + const { controller } = setupController({}); + expect(controller.state.useBlockie).toStrictEqual(false); }); it('setUseBlockie to true', () => { - preferencesController.setUseBlockie(true); - expect(preferencesController.store.getState().useBlockie).toStrictEqual( - true, - ); + const { controller } = setupController({}); + controller.setUseBlockie(true); + expect(controller.state.useBlockie).toStrictEqual(true); }); }); describe('setCurrentLocale', () => { it('checks the default currentLocale', () => { - const { currentLocale } = preferencesController.store.getState(); - expect(currentLocale).toStrictEqual('en_US'); + const { controller } = setupController({}); + const { currentLocale } = controller.state; + expect(currentLocale).toStrictEqual(''); }); it('sets current locale in preferences controller', () => { - preferencesController.setCurrentLocale('ja'); - const { currentLocale } = preferencesController.store.getState(); + const { controller } = setupController({}); + controller.setCurrentLocale('ja'); + const { currentLocale } = controller.state; expect(currentLocale).toStrictEqual('ja'); }); }); describe('setAccountLabel', () => { + const { controller, messenger, accountsController } = setupController({}); const mockName = 'mockName'; const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; it('updating name from preference controller will update the name in accounts controller and preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -150,21 +146,20 @@ describe('preferences controller', () => { ); let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; expect(firstAccount.metadata.name).toBe(firstPreferenceAccount.name); expect(secondAccount.metadata.name).toBe(secondPreferenceAccount.name); - preferencesController.setAccountLabel(firstAccount.address, mockName); + controller.setAccountLabel(firstAccount.address, mockName); // refresh state after state changed [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -181,7 +176,7 @@ describe('preferences controller', () => { }); it('updating name from accounts controller updates the name in preferences controller', () => { - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -197,7 +192,7 @@ describe('preferences controller', () => { let [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; const firstPreferenceAccount = identities[firstAccount.address]; const secondPreferenceAccount = identities[secondAccount.address]; @@ -210,8 +205,7 @@ describe('preferences controller', () => { [firstAccount, secondAccount] = accountsController.listAccounts(); - const { identities: updatedIdentities } = - preferencesController.store.getState(); + const { identities: updatedIdentities } = controller.state; const updatedFirstPreferenceAccount = updatedIdentities[firstAccount.address]; @@ -229,10 +223,11 @@ describe('preferences controller', () => { }); describe('setSelectedAddress', () => { + const { controller, messenger, accountsController } = setupController({}); it('updating selectedAddress from preferences controller updates the selectedAccount in accounts controller and preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -248,25 +243,26 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); - preferencesController.setSelectedAddress(secondAddress); + controller.setSelectedAddress(secondAddress); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); expect(updatedSelectedAddress).toBe(updatedSelectedAccount.address); + + expect(controller.getSelectedAddress()).toBe(secondAddress); }); it('updating selectedAccount from accounts controller updates the selectedAddress in preferences controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -283,15 +279,14 @@ describe('preferences controller', () => { const selectedAccount = accountsController.getSelectedAccount(); const accounts = accountsController.listAccounts(); - const { selectedAddress } = preferencesController.store.getState(); + const { selectedAddress } = controller.state; expect(selectedAddress).toBe(selectedAccount.address); accountsController.setSelectedAccount(accounts[1].id); // refresh state after state changed - const { selectedAddress: updatedSelectedAddress } = - preferencesController.store.getState(); + const { selectedAddress: updatedSelectedAddress } = controller.state; const updatedSelectedAccount = accountsController.getSelectedAccount(); @@ -300,173 +295,142 @@ describe('preferences controller', () => { }); describe('setPasswordForgotten', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(false); + expect(controller.state.forgottenPassword).toStrictEqual(false); }); it('should set the forgottenPassword property in state', () => { - preferencesController.setPasswordForgotten(true); - expect( - preferencesController.store.getState().forgottenPassword, - ).toStrictEqual(true); + controller.setPasswordForgotten(true); + expect(controller.state.forgottenPassword).toStrictEqual(true); }); }); describe('setUsePhishDetect', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); }); it('should set the usePhishDetect property in state', () => { - preferencesController.setUsePhishDetect(false); - expect( - preferencesController.store.getState().usePhishDetect, - ).toStrictEqual(false); + controller.setUsePhishDetect(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); }); }); describe('setUseMultiAccountBalanceChecker', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(true); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + true, + ); }); it('should set the setUseMultiAccountBalanceChecker property in state', () => { - preferencesController.setUseMultiAccountBalanceChecker(false); - expect( - preferencesController.store.getState().useMultiAccountBalanceChecker, - ).toStrictEqual(false); + controller.setUseMultiAccountBalanceChecker(false); + expect(controller.state.useMultiAccountBalanceChecker).toStrictEqual( + false, + ); }); }); describe('isRedesignedConfirmationsFeatureEnabled', () => { + const { controller } = setupController({}); it('isRedesignedConfirmationsFeatureEnabled should default to false', () => { expect( - preferencesController.store.getState().preferences - .isRedesignedConfirmationsDeveloperEnabled, + controller.state.preferences.isRedesignedConfirmationsDeveloperEnabled, ).toStrictEqual(false); }); }); describe('setUseSafeChainsListValidation', function () { + const { controller } = setupController({}); it('should default to true', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useSafeChainsListValidation).toStrictEqual(true); }); it('should set the `setUseSafeChainsListValidation` property in state', function () { - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(true); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(true); - preferencesController.setUseSafeChainsListValidation(false); + controller.setUseSafeChainsListValidation(false); - expect( - preferencesController.store.getState().useSafeChainsListValidation, - ).toStrictEqual(false); + expect(controller.state.useSafeChainsListValidation).toStrictEqual(false); }); }); describe('setUseTokenDetection', function () { + const { controller } = setupController({}); it('should default to true for new users', function () { - const state = preferencesController.store.getState(); + const { state } = controller; expect(state.useTokenDetection).toStrictEqual(true); }); it('should set the useTokenDetection property in state', () => { - preferencesController.setUseTokenDetection(true); - expect( - preferencesController.store.getState().useTokenDetection, - ).toStrictEqual(true); + controller.setUseTokenDetection(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); }); it('should keep initial value of useTokenDetection for existing users', function () { - // TODO: Remove unregisterActionHandler and clearEventSubscriptions once the PreferencesController has been refactored to use the withController pattern. - controllerMessenger.unregisterActionHandler( - 'PreferencesController:getState', - ); - controllerMessenger.clearEventSubscriptions( - 'PreferencesController:stateChange', - ); - const preferencesControllerExistingUser = new PreferencesController({ - messenger: controllerMessenger.getRestricted({ - name: 'PreferencesController', - allowedActions: [], - allowedEvents: ['AccountsController:stateChange'], - }), - initLangCode: 'en_US', - initState: { - useTokenDetection: false, + const { controller: preferencesControllerExistingUser } = setupController( + { + state: { + useTokenDetection: false, + }, }, - networkConfigurationsByChainId: NETWORK_CONFIGURATION_DATA, - }); - const state = preferencesControllerExistingUser.store.getState(); + ); + const { state } = preferencesControllerExistingUser; expect(state.useTokenDetection).toStrictEqual(false); }); }); describe('setUseNftDetection', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); it('should set the useNftDetection property in state', () => { - preferencesController.setOpenSeaEnabled(true); - preferencesController.setUseNftDetection(true); - expect( - preferencesController.store.getState().useNftDetection, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + controller.setUseNftDetection(true); + expect(controller.state.useNftDetection).toStrictEqual(true); }); }); describe('setUse4ByteResolution', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(true); + expect(controller.state.use4ByteResolution).toStrictEqual(true); }); it('should set the use4ByteResolution property in state', () => { - preferencesController.setUse4ByteResolution(false); - expect( - preferencesController.store.getState().use4ByteResolution, - ).toStrictEqual(false); + controller.setUse4ByteResolution(false); + expect(controller.state.use4ByteResolution).toStrictEqual(false); }); }); describe('setOpenSeaEnabled', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); it('should set the openSeaEnabled property in state', () => { - preferencesController.setOpenSeaEnabled(true); - expect( - preferencesController.store.getState().openSeaEnabled, - ).toStrictEqual(true); + controller.setOpenSeaEnabled(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); }); }); describe('setAdvancedGasFee', () => { + const { controller } = setupController({}); it('should default to an empty object', () => { - expect( - preferencesController.store.getState().advancedGasFee, - ).toStrictEqual({}); + expect(controller.state.advancedGasFee).toStrictEqual({}); }); it('should set the setAdvancedGasFee property in state', () => { - preferencesController.setAdvancedGasFee({ + controller.setAdvancedGasFee({ chainId: CHAIN_IDS.GOERLI, gasFeePreferences: { maxBaseFee: '1.5', @@ -474,51 +438,44 @@ describe('preferences controller', () => { }, }); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .maxBaseFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].maxBaseFee, ).toStrictEqual('1.5'); expect( - preferencesController.store.getState().advancedGasFee[CHAIN_IDS.GOERLI] - .priorityFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI].priorityFee, ).toStrictEqual('2'); }); }); describe('setTheme', () => { + const { controller } = setupController({}); it('should default to value "OS"', () => { - expect(preferencesController.store.getState().theme).toStrictEqual('os'); + expect(controller.state.theme).toStrictEqual('os'); }); it('should set the setTheme property in state', () => { - preferencesController.setTheme(ThemeType.dark); - expect(preferencesController.store.getState().theme).toStrictEqual( - 'dark', - ); + controller.setTheme(ThemeType.dark); + expect(controller.state.theme).toStrictEqual('dark'); }); }); describe('setUseCurrencyRateCheck', () => { + const { controller } = setupController({}); it('should default to false', () => { - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); }); it('should set the useCurrencyRateCheck property in state', () => { - preferencesController.setUseCurrencyRateCheck(false); - expect( - preferencesController.store.getState().useCurrencyRateCheck, - ).toStrictEqual(false); + controller.setUseCurrencyRateCheck(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); }); }); describe('setIncomingTransactionsPreferences', () => { + const { controller } = setupController({}); const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA); it('should have default value combined', () => { - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: true, @@ -533,13 +490,11 @@ describe('preferences controller', () => { }); it('should update incomingTransactionsPreferences with given value set', () => { - preferencesController.setIncomingTransactionsPreferences( + controller.setIncomingTransactionsPreferences( CHAIN_IDS.LINEA_MAINNET, false, ); - const state: { - incomingTransactionsPreferences: Record; - } = preferencesController.store.getState(); + const { state } = controller; expect(state.incomingTransactionsPreferences).toStrictEqual({ [CHAIN_IDS.MAINNET]: true, [CHAIN_IDS.LINEA_MAINNET]: false, @@ -555,10 +510,11 @@ describe('preferences controller', () => { }); describe('AccountsController:stateChange subscription', () => { + const { controller, messenger, accountsController } = setupController({}); it('sync the identities with the accounts in the accounts controller', () => { const firstAddress = '0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326'; const secondAddress = '0x0affb0a96fbefaa97dce488dfd97512346cf3ab8'; - controllerMessenger.publish( + messenger.publish( 'KeyringController:stateChange', { isUnlocked: true, @@ -574,7 +530,7 @@ describe('preferences controller', () => { const accounts = accountsController.listAccounts(); - const { identities } = preferencesController.store.getState(); + const { identities } = controller.state; expect(accounts.map((account) => account.address)).toStrictEqual( Object.keys(identities), @@ -584,68 +540,313 @@ describe('preferences controller', () => { ///: BEGIN:ONLY_INCLUDE_IF(petnames) describe('setUseExternalNameSources', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the useExternalNameSources property in state', () => { - preferencesController.setUseExternalNameSources(false); - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(false); + controller.setUseExternalNameSources(false); + expect(controller.state.useExternalNameSources).toStrictEqual(false); }); }); ///: END:ONLY_INCLUDE_IF describe('setUseTransactionSimulations', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().useExternalNameSources, - ).toStrictEqual(true); + expect(controller.state.useExternalNameSources).toStrictEqual(true); }); it('should set the setUseTransactionSimulations property in state', () => { - preferencesController.setUseTransactionSimulations(false); - expect( - preferencesController.store.getState().useTransactionSimulations, - ).toStrictEqual(false); + controller.setUseTransactionSimulations(false); + expect(controller.state.useTransactionSimulations).toStrictEqual(false); }); }); describe('setServiceWorkerKeepAlivePreference', () => { + const { controller } = setupController({}); it('should default to true', () => { - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(true); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(true); }); it('should set the setServiceWorkerKeepAlivePreference property in state', () => { - preferencesController.setServiceWorkerKeepAlivePreference(false); - expect( - preferencesController.store.getState().enableMV3TimestampSave, - ).toStrictEqual(false); + controller.setServiceWorkerKeepAlivePreference(false); + expect(controller.state.enableMV3TimestampSave).toStrictEqual(false); }); }); describe('setBitcoinSupportEnabled', () => { + const { controller } = setupController({}); it('has the default value as false', () => { - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); }); it('sets the bitcoinSupportEnabled property in state to true and then false', () => { - preferencesController.setBitcoinSupportEnabled(true); + controller.setBitcoinSupportEnabled(true); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(true); + + controller.setBitcoinSupportEnabled(false); + expect(controller.state.bitcoinSupportEnabled).toStrictEqual(false); + }); + }); + + describe('useNonceField', () => { + it('defaults useNonceField to false', () => { + const { controller } = setupController({}); + expect(controller.state.useNonceField).toStrictEqual(false); + }); + + it('setUseNonceField to true', () => { + const { controller } = setupController({}); + controller.setUseNonceField(true); + expect(controller.state.useNonceField).toStrictEqual(true); + }); + }); + + describe('globalThis.setPreference', () => { + it('setFeatureFlags to true', () => { + const { controller } = setupController({}); + globalThis.setPreference('showFiatInTestnets', true); + expect(controller.state.featureFlags.showFiatInTestnets).toStrictEqual( + true, + ); + }); + }); + + describe('useExternalServices', () => { + it('defaults useExternalServices to true', () => { + const { controller } = setupController({}); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useExternalServices).toStrictEqual(true); + expect(controller.state.useTokenDetection).toStrictEqual(true); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(true); + expect(controller.state.usePhishDetect).toStrictEqual(true); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + expect(controller.state.openSeaEnabled).toStrictEqual(true); + expect(controller.state.useNftDetection).toStrictEqual(true); + }); + + it('useExternalServices to false', () => { + const { controller } = setupController({}); + controller.toggleExternalServices(false); + expect(controller.state.useExternalServices).toStrictEqual(false); + expect(controller.state.useTokenDetection).toStrictEqual(false); + expect(controller.state.useCurrencyRateCheck).toStrictEqual(false); + expect(controller.state.usePhishDetect).toStrictEqual(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + expect(controller.state.openSeaEnabled).toStrictEqual(false); + expect(controller.state.useNftDetection).toStrictEqual(false); + }); + }); + + describe('useRequestQueue', () => { + it('defaults useRequestQueue to true', () => { + const { controller } = setupController({}); + expect(controller.state.useRequestQueue).toStrictEqual(true); + }); + + it('setUseRequestQueue to false', () => { + const { controller } = setupController({}); + controller.setUseRequestQueue(false); + expect(controller.state.useRequestQueue).toStrictEqual(false); + }); + }); + + describe('addSnapAccountEnabled', () => { + it('defaults addSnapAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(false); + }); + + it('setAddSnapAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setAddSnapAccountEnabled(true); + expect(controller.state.addSnapAccountEnabled).toStrictEqual(true); + }); + }); + + describe('watchEthereumAccountEnabled', () => { + it('defaults watchEthereumAccountEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(false); + }); + + it('setWatchEthereumAccountEnabled to true', () => { + const { controller } = setupController({}); + controller.setWatchEthereumAccountEnabled(true); + expect(controller.state.watchEthereumAccountEnabled).toStrictEqual(true); + }); + }); + + describe('bitcoinTestnetSupportEnabled', () => { + it('defaults bitcoinTestnetSupportEnabled to false', () => { + const { controller } = setupController({}); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual( + false, + ); + }); + + it('setBitcoinTestnetSupportEnabled to true', () => { + const { controller } = setupController({}); + controller.setBitcoinTestnetSupportEnabled(true); + expect(controller.state.bitcoinTestnetSupportEnabled).toStrictEqual(true); + }); + }); + + describe('knownMethodData', () => { + it('defaults knownMethodData', () => { + const { controller } = setupController({}); + expect(controller.state.knownMethodData).toStrictEqual({}); + }); + + it('addKnownMethodData', () => { + const { controller } = setupController({}); + controller.addKnownMethodData('0x60806040', 'testMethodName'); + expect(controller.state.knownMethodData).toStrictEqual({ + '0x60806040': 'testMethodName', + }); + }); + }); + + describe('featureFlags', () => { + it('defaults featureFlags', () => { + const { controller } = setupController({}); + expect(controller.state.featureFlags).toStrictEqual({}); + }); + + it('setFeatureFlags', () => { + const { controller } = setupController({}); + controller.setFeatureFlag('showConfirmationAdvancedDetails', true); expect( - preferencesController.store.getState().bitcoinSupportEnabled, + controller.state.featureFlags.showConfirmationAdvancedDetails, ).toStrictEqual(true); + }); + }); - preferencesController.setBitcoinSupportEnabled(false); - expect( - preferencesController.store.getState().bitcoinSupportEnabled, - ).toStrictEqual(false); + describe('preferences', () => { + it('defaults preferences', () => { + const { controller } = setupController({}); + expect(controller.state.preferences).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + + it('setPreference', () => { + const { controller } = setupController({}); + controller.setPreference('showConfirmationAdvancedDetails', true); + expect(controller.getPreferences()).toStrictEqual({ + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + shouldShowAggregatedBalancePopover: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: true, + showMultiRpcModal: false, + showNativeTokenAsMainBalance: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }); + }); + }); + + describe('ipfsGateway', () => { + it('defaults ipfsGate to dweb.link', () => { + const { controller } = setupController({}); + expect(controller.state.ipfsGateway).toStrictEqual('dweb.link'); + }); + + it('setIpfsGateway to test.link', () => { + const { controller } = setupController({}); + controller.setIpfsGateway('test.link'); + expect(controller.getIpfsGateway()).toStrictEqual('test.link'); + }); + }); + + describe('isIpfsGatewayEnabled', () => { + it('defaults isIpfsGatewayEnabled to true', () => { + const { controller } = setupController({}); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(true); + }); + + it('set isIpfsGatewayEnabled to false', () => { + const { controller } = setupController({}); + controller.setIsIpfsGatewayEnabled(false); + expect(controller.state.isIpfsGatewayEnabled).toStrictEqual(false); + }); + }); + + describe('useAddressBarEnsResolution', () => { + it('defaults useAddressBarEnsResolution to true', () => { + const { controller } = setupController({}); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(true); + }); + + it('set useAddressBarEnsResolution to false', () => { + const { controller } = setupController({}); + controller.setUseAddressBarEnsResolution(false); + expect(controller.state.useAddressBarEnsResolution).toStrictEqual(false); + }); + }); + + describe('dismissSeedBackUpReminder', () => { + it('defaults dismissSeedBackUpReminder to false', () => { + const { controller } = setupController({}); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(false); + }); + + it('set dismissSeedBackUpReminder to true', () => { + const { controller } = setupController({}); + controller.setDismissSeedBackUpReminder(true); + expect(controller.state.dismissSeedBackUpReminder).toStrictEqual(true); + }); + }); + + describe('snapsAddSnapAccountModalDismissed', () => { + it('defaults snapsAddSnapAccountModalDismissed to false', () => { + const { controller } = setupController({}); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + false, + ); + }); + + it('set snapsAddSnapAccountModalDismissed to true', () => { + const { controller } = setupController({}); + controller.setSnapsAddSnapAccountModalDismissed(true); + expect(controller.state.snapsAddSnapAccountModalDismissed).toStrictEqual( + true, + ); }); }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index eb126b176a41..a7ede69bb26c 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -1,4 +1,3 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerChangeEvent, AccountsControllerGetAccountByAddressAction, @@ -8,7 +7,18 @@ import { AccountsControllerState, } from '@metamask/accounts-controller'; import { Hex } from '@metamask/utils'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Json } from 'json-rpc-engine'; +import { NetworkControllerGetStateAction } from '@metamask/network-controller'; +import { + ETHERSCAN_SUPPORTED_CHAIN_IDS, + type PreferencesState, +} from '@metamask/preferences-controller'; import { CHAIN_IDS, IPFS_DEFAULT_GATEWAY_URL, @@ -19,7 +29,7 @@ import { ThemeType } from '../../../shared/constants/preferences'; type AccountIdentityEntry = { address: string; name: string; - lastSelected: number | undefined; + lastSelected?: number; }; const mainNetworks = { @@ -38,10 +48,10 @@ const controllerName = 'PreferencesController'; /** * Returns the state of the {@link PreferencesController}. */ -export type PreferencesControllerGetStateAction = { - type: 'PreferencesController:getState'; - handler: () => PreferencesControllerState; -}; +export type PreferencesControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + PreferencesControllerState +>; /** * Actions exposed by the {@link PreferencesController}. @@ -51,10 +61,10 @@ export type PreferencesControllerActions = PreferencesControllerGetStateAction; /** * Event emitted when the state of the {@link PreferencesController} changes. */ -export type PreferencesControllerStateChangeEvent = { - type: 'PreferencesController:stateChange'; - payload: [PreferencesControllerState, []]; -}; +export type PreferencesControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + PreferencesControllerState +>; /** * Events emitted by {@link PreferencesController}. @@ -68,7 +78,8 @@ export type AllowedActions = | AccountsControllerGetAccountByAddressAction | AccountsControllerSetAccountNameAction | AccountsControllerGetSelectedAccountAction - | AccountsControllerSetSelectedAccountAction; + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -84,9 +95,7 @@ export type PreferencesControllerMessenger = RestrictedControllerMessenger< >; type PreferencesControllerOptions = { - networkConfigurationsByChainId?: Record; - initState?: Partial; - initLangCode?: string; + state?: Partial; messenger: PreferencesControllerMessenger; }; @@ -114,176 +123,356 @@ export type Preferences = { shouldShowAggregatedBalancePopover: boolean; }; -export type PreferencesControllerState = { - selectedAddress: string; +// Omitting showTestNetworks and smartTransactionsOptInStatus, as they already exists here in Preferences type +export type PreferencesControllerState = Omit< + PreferencesState, + 'showTestNetworks' | 'smartTransactionsOptInStatus' +> & { useBlockie: boolean; useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; - useTokenDetection: boolean; - useNftDetection: boolean; use4ByteResolution: boolean; useCurrencyRateCheck: boolean; useRequestQueue: boolean; - openSeaEnabled: boolean; - securityAlertsEnabled: boolean; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; - addSnapAccountEnabled: boolean; + addSnapAccountEnabled?: boolean; advancedGasFee: Record>; - featureFlags: Record; incomingTransactionsPreferences: Record; knownMethodData: Record; currentLocale: string; - identities: Record; - lostIdentities: Record; forgottenPassword: boolean; preferences: Preferences; - ipfsGateway: string; - isIpfsGatewayEnabled: boolean; useAddressBarEnsResolution: boolean; ledgerTransportType: LedgerTransportTypes; - snapRegistryList: Record; + // TODO: Replace `Json` with correct type + snapRegistryList: Record; theme: ThemeType; - snapsAddSnapAccountModalDismissed: boolean; + snapsAddSnapAccountModalDismissed?: boolean; useExternalNameSources: boolean; - useTransactionSimulations: boolean; enableMV3TimestampSave: boolean; useExternalServices: boolean; textDirection?: string; }; -export default class PreferencesController { - store: ObservableStore; +/** + * Function to get default state of the {@link PreferencesController}. + */ +export const getDefaultPreferencesControllerState = + (): PreferencesControllerState => ({ + selectedAddress: '', + useBlockie: false, + useNonceField: false, + usePhishDetect: true, + dismissSeedBackUpReminder: false, + useMultiAccountBalanceChecker: true, + useSafeChainsListValidation: true, + // set to true means the dynamic list from the API is being used + // set to false will be using the static list from contract-metadata + useTokenDetection: true, + useNftDetection: true, + use4ByteResolution: true, + useCurrencyRateCheck: true, + useRequestQueue: true, + openSeaEnabled: true, + securityAlertsEnabled: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + addSnapAccountEnabled: false, + ///: END:ONLY_INCLUDE_IF + advancedGasFee: {}, + featureFlags: {}, + incomingTransactionsPreferences: { + ...mainNetworks, + ...testNetworks, + }, + knownMethodData: {}, + currentLocale: '', + identities: {}, + lostIdentities: {}, + forgottenPassword: false, + preferences: { + autoLockTimeLimit: undefined, + showExtensionInFullSizeView: false, + showFiatInTestnets: false, + showTestNetworks: false, + smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, + useNativeCurrencyAsPrimaryCurrency: true, + hideZeroBalanceTokens: false, + petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, + featureNotificationsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, + showMultiRpcModal: false, + shouldShowAggregatedBalancePopover: true, // by default user should see popover; + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + // ENS decentralized website resolution + ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, + isIpfsGatewayEnabled: true, + useAddressBarEnsResolution: true, + // Ledger transport type is deprecated. We currently only support webhid + // on chrome, and u2f on firefox. + ledgerTransportType: window.navigator.hid + ? LedgerTransportTypes.webhid + : LedgerTransportTypes.u2f, + snapRegistryList: {}, + theme: ThemeType.os, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + snapsAddSnapAccountModalDismissed: false, + ///: END:ONLY_INCLUDE_IF + useExternalNameSources: true, + useTransactionSimulations: true, + enableMV3TimestampSave: true, + // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. + // Whenever useExternalServices is false, certain features will be disabled. + // The flag is true by Default, meaning the toggle is ON by default. + useExternalServices: true, + // from core PreferencesController + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + }); - private messagingSystem: PreferencesControllerMessenger; +/** + * {@link PreferencesController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + selectedAddress: { + persist: true, + anonymous: false, + }, + useBlockie: { + persist: true, + anonymous: true, + }, + useNonceField: { + persist: true, + anonymous: true, + }, + usePhishDetect: { + persist: true, + anonymous: true, + }, + dismissSeedBackUpReminder: { + persist: true, + anonymous: true, + }, + useMultiAccountBalanceChecker: { + persist: true, + anonymous: true, + }, + useSafeChainsListValidation: { + persist: true, + anonymous: false, + }, + useTokenDetection: { + persist: true, + anonymous: true, + }, + useNftDetection: { + persist: true, + anonymous: true, + }, + use4ByteResolution: { + persist: true, + anonymous: true, + }, + useCurrencyRateCheck: { + persist: true, + anonymous: true, + }, + useRequestQueue: { + persist: true, + anonymous: true, + }, + openSeaEnabled: { + persist: true, + anonymous: true, + }, + securityAlertsEnabled: { + persist: true, + anonymous: false, + }, + watchEthereumAccountEnabled: { + persist: true, + anonymous: false, + }, + bitcoinSupportEnabled: { + persist: true, + anonymous: false, + }, + bitcoinTestnetSupportEnabled: { + persist: true, + anonymous: false, + }, + addSnapAccountEnabled: { + persist: true, + anonymous: false, + }, + advancedGasFee: { + persist: true, + anonymous: true, + }, + featureFlags: { + persist: true, + anonymous: true, + }, + incomingTransactionsPreferences: { + persist: true, + anonymous: true, + }, + knownMethodData: { + persist: true, + anonymous: false, + }, + currentLocale: { + persist: true, + anonymous: true, + }, + identities: { + persist: true, + anonymous: false, + }, + lostIdentities: { + persist: true, + anonymous: false, + }, + forgottenPassword: { + persist: true, + anonymous: true, + }, + preferences: { + persist: true, + anonymous: true, + }, + ipfsGateway: { + persist: true, + anonymous: false, + }, + isIpfsGatewayEnabled: { + persist: true, + anonymous: false, + }, + useAddressBarEnsResolution: { + persist: true, + anonymous: true, + }, + ledgerTransportType: { + persist: true, + anonymous: true, + }, + snapRegistryList: { + persist: true, + anonymous: false, + }, + theme: { + persist: true, + anonymous: true, + }, + snapsAddSnapAccountModalDismissed: { + persist: true, + anonymous: false, + }, + useExternalNameSources: { + persist: true, + anonymous: false, + }, + useTransactionSimulations: { + persist: true, + anonymous: true, + }, + enableMV3TimestampSave: { + persist: true, + anonymous: true, + }, + useExternalServices: { + persist: true, + anonymous: false, + }, + textDirection: { + persist: true, + anonymous: false, + }, + isMultiAccountBalancesEnabled: { persist: true, anonymous: true }, + showIncomingTransactions: { persist: true, anonymous: true }, +}; +export class PreferencesController extends BaseController< + typeof controllerName, + PreferencesControllerState, + PreferencesControllerMessenger +> { /** + * Constructs a Preferences controller. * - * @param opts - Overrides the defaults for the initial state of this.store - * @property messenger - The controller messenger - * @property initState The stored object containing a users preferences, stored in local storage - * @property initState.useBlockie The users preference for blockie identicons within the UI - * @property initState.useNonceField The users preference for nonce field within the UI - * @property initState.featureFlags A key-boolean map, where keys refer to features and booleans to whether the - * user wishes to see that feature. - * - * Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior. - * @property initState.knownMethodData Contains all data methods known by the user - * @property initState.currentLocale The preferred language locale key - * @property initState.selectedAddress A hex string that matches the currently selected address in the app + * @param options - the controller options + * @param options.messenger - The controller messenger + * @param options.state - The initial controller state */ - constructor(opts: PreferencesControllerOptions) { + constructor({ messenger, state }: PreferencesControllerOptions) { + const { networkConfigurationsByChainId } = messenger.call( + 'NetworkController:getState', + ); + const addedNonMainNetwork: Record = Object.values( - opts.networkConfigurationsByChainId ?? {}, + networkConfigurationsByChainId ?? {}, ).reduce((acc: Record, element) => { acc[element.chainId] = true; return acc; }, {}); - - const initState: PreferencesControllerState = { - selectedAddress: '', - useBlockie: false, - useNonceField: false, - usePhishDetect: true, - dismissSeedBackUpReminder: false, - useMultiAccountBalanceChecker: true, - useSafeChainsListValidation: true, - // set to true means the dynamic list from the API is being used - // set to false will be using the static list from contract-metadata - useTokenDetection: opts?.initState?.useTokenDetection ?? true, - useNftDetection: opts?.initState?.useTokenDetection ?? true, - use4ByteResolution: true, - useCurrencyRateCheck: true, - useRequestQueue: true, - openSeaEnabled: true, - securityAlertsEnabled: true, - watchEthereumAccountEnabled: false, - bitcoinSupportEnabled: false, - bitcoinTestnetSupportEnabled: false, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - addSnapAccountEnabled: false, - ///: END:ONLY_INCLUDE_IF - advancedGasFee: {}, - - // WARNING: Do not use feature flags for security-sensitive things. - // Feature flag toggling is available in the global namespace - // for convenient testing of pre-release features, and should never - // perform sensitive operations. - featureFlags: {}, - incomingTransactionsPreferences: { - ...mainNetworks, - ...addedNonMainNetwork, - ...testNetworks, - }, - knownMethodData: {}, - currentLocale: opts.initLangCode ?? '', - identities: {}, - lostIdentities: {}, - forgottenPassword: false, - preferences: { - autoLockTimeLimit: undefined, - showExtensionInFullSizeView: false, - showFiatInTestnets: false, - showTestNetworks: false, - smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible - showNativeTokenAsMainBalance: false, - useNativeCurrencyAsPrimaryCurrency: true, - hideZeroBalanceTokens: false, - petnamesEnabled: true, - redesignedConfirmationsEnabled: true, - redesignedTransactionsEnabled: true, - featureNotificationsEnabled: false, - showMultiRpcModal: false, - isRedesignedConfirmationsDeveloperEnabled: false, - showConfirmationAdvancedDetails: false, - tokenSortConfig: { - key: 'tokenFiatAmount', - order: 'dsc', - sortCallback: 'stringNumeric', + super({ + messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultPreferencesControllerState(), + incomingTransactionsPreferences: { + ...mainNetworks, + ...addedNonMainNetwork, + ...testNetworks, }, - shouldShowAggregatedBalancePopover: true, // by default user should see popover; + ...state, }, - // ENS decentralized website resolution - ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, - isIpfsGatewayEnabled: true, - useAddressBarEnsResolution: true, - // Ledger transport type is deprecated. We currently only support webhid - // on chrome, and u2f on firefox. - ledgerTransportType: window.navigator.hid - ? LedgerTransportTypes.webhid - : LedgerTransportTypes.u2f, - snapRegistryList: {}, - theme: ThemeType.os, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - snapsAddSnapAccountModalDismissed: false, - ///: END:ONLY_INCLUDE_IF - useExternalNameSources: true, - useTransactionSimulations: true, - enableMV3TimestampSave: true, - // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. - // Whenever useExternalServices is false, certain features will be disabled. - // The flag is true by Default, meaning the toggle is ON by default. - useExternalServices: true, - ...opts.initState, - }; - - this.store = new ObservableStore(initState); - this.store.setMaxListeners(13); - - this.messagingSystem = opts.messenger; - this.messagingSystem.registerActionHandler( - `PreferencesController:getState`, - () => this.store.getState(), - ); - this.messagingSystem.registerInitialEventPayload({ - eventType: `PreferencesController:stateChange`, - getPayload: () => [this.store.getState(), []], }); this.messagingSystem.subscribe( @@ -302,7 +491,9 @@ export default class PreferencesController { * @param forgottenPassword - whether or not the user has forgotten their password */ setPasswordForgotten(forgottenPassword: boolean): void { - this.store.updateState({ forgottenPassword }); + this.update((state) => { + state.forgottenPassword = forgottenPassword; + }); } /** @@ -311,7 +502,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers blockie indicators */ setUseBlockie(val: boolean): void { - this.store.updateState({ useBlockie: val }); + this.update((state) => { + state.useBlockie = val; + }); } /** @@ -320,7 +513,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to set nonce */ setUseNonceField(val: boolean): void { - this.store.updateState({ useNonceField: val }); + this.update((state) => { + state.useNonceField = val; + }); } /** @@ -329,7 +524,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers phishing domain protection */ setUsePhishDetect(val: boolean): void { - this.store.updateState({ usePhishDetect: val }); + this.update((state) => { + state.usePhishDetect = val; + }); } /** @@ -338,7 +535,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on all security settings */ setUseMultiAccountBalanceChecker(val: boolean): void { - this.store.updateState({ useMultiAccountBalanceChecker: val }); + this.update((state) => { + state.useMultiAccountBalanceChecker = val; + }); } /** @@ -347,11 +546,15 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to turn off/on validation for manually adding networks */ setUseSafeChainsListValidation(val: boolean): void { - this.store.updateState({ useSafeChainsListValidation: val }); + this.update((state) => { + state.useSafeChainsListValidation = val; + }); } toggleExternalServices(useExternalServices: boolean): void { - this.store.updateState({ useExternalServices }); + this.update((state) => { + state.useExternalServices = useExternalServices; + }); this.setUseTokenDetection(useExternalServices); this.setUseCurrencyRateCheck(useExternalServices); this.setUsePhishDetect(useExternalServices); @@ -366,7 +569,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use the static token list or dynamic token list from the API */ setUseTokenDetection(val: boolean): void { - this.store.updateState({ useTokenDetection: val }); + this.update((state) => { + state.useTokenDetection = val; + }); } /** @@ -375,7 +580,9 @@ export default class PreferencesController { * @param useNftDetection - Whether or not the user prefers to autodetect NFTs. */ setUseNftDetection(useNftDetection: boolean): void { - this.store.updateState({ useNftDetection }); + this.update((state) => { + state.useNftDetection = useNftDetection; + }); } /** @@ -384,7 +591,9 @@ export default class PreferencesController { * @param use4ByteResolution - (Privacy) Whether or not the user prefers to have smart contract name details resolved with 4byte.directory */ setUse4ByteResolution(use4ByteResolution: boolean): void { - this.store.updateState({ use4ByteResolution }); + this.update((state) => { + state.use4ByteResolution = use4ByteResolution; + }); } /** @@ -393,7 +602,9 @@ export default class PreferencesController { * @param val - Whether or not the user prefers to use currency rate check for ETH and tokens. */ setUseCurrencyRateCheck(val: boolean): void { - this.store.updateState({ useCurrencyRateCheck: val }); + this.update((state) => { + state.useCurrencyRateCheck = val; + }); } /** @@ -402,7 +613,9 @@ export default class PreferencesController { * @param val - Whether or not the user wants to have requests queued if network change is required. */ setUseRequestQueue(val: boolean): void { - this.store.updateState({ useRequestQueue: val }); + this.update((state) => { + state.useRequestQueue = val; + }); } /** @@ -411,8 +624,8 @@ export default class PreferencesController { * @param openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data. */ setOpenSeaEnabled(openSeaEnabled: boolean): void { - this.store.updateState({ - openSeaEnabled, + this.update((state) => { + state.openSeaEnabled = openSeaEnabled; }); } @@ -422,8 +635,8 @@ export default class PreferencesController { * @param securityAlertsEnabled - Whether or not the user prefers to use the security alerts. */ setSecurityAlertsEnabled(securityAlertsEnabled: boolean): void { - this.store.updateState({ - securityAlertsEnabled, + this.update((state) => { + state.securityAlertsEnabled = securityAlertsEnabled; }); } @@ -435,8 +648,8 @@ export default class PreferencesController { * enable the "Add Snap accounts" button. */ setAddSnapAccountEnabled(addSnapAccountEnabled: boolean): void { - this.store.updateState({ - addSnapAccountEnabled, + this.update((state) => { + state.addSnapAccountEnabled = addSnapAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -449,8 +662,8 @@ export default class PreferencesController { * enable the "Watch Ethereum account (Beta)" button. */ setWatchEthereumAccountEnabled(watchEthereumAccountEnabled: boolean): void { - this.store.updateState({ - watchEthereumAccountEnabled, + this.update((state) => { + state.watchEthereumAccountEnabled = watchEthereumAccountEnabled; }); } ///: END:ONLY_INCLUDE_IF @@ -462,8 +675,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Beta)" button. */ setBitcoinSupportEnabled(bitcoinSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinSupportEnabled, + this.update((state) => { + state.bitcoinSupportEnabled = bitcoinSupportEnabled; }); } @@ -474,8 +687,8 @@ export default class PreferencesController { * enable the "Add a new Bitcoin account (Testnet)" button. */ setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled: boolean): void { - this.store.updateState({ - bitcoinTestnetSupportEnabled, + this.update((state) => { + state.bitcoinTestnetSupportEnabled = bitcoinTestnetSupportEnabled; }); } @@ -485,8 +698,8 @@ export default class PreferencesController { * @param useExternalNameSources - Whether or not to use external name providers in the name controller. */ setUseExternalNameSources(useExternalNameSources: boolean): void { - this.store.updateState({ - useExternalNameSources, + this.update((state) => { + state.useExternalNameSources = useExternalNameSources; }); } @@ -496,8 +709,8 @@ export default class PreferencesController { * @param useTransactionSimulations - Whether or not to use simulations in the transaction confirmations. */ setUseTransactionSimulations(useTransactionSimulations: boolean): void { - this.store.updateState({ - useTransactionSimulations, + this.update((state) => { + state.useTransactionSimulations = useTransactionSimulations; }); } @@ -515,12 +728,12 @@ export default class PreferencesController { chainId: string; gasFeePreferences: Record; }): void { - const { advancedGasFee } = this.store.getState(); - this.store.updateState({ - advancedGasFee: { + const { advancedGasFee } = this.state; + this.update((state) => { + state.advancedGasFee = { ...advancedGasFee, [chainId]: gasFeePreferences, - }, + }; }); } @@ -530,7 +743,9 @@ export default class PreferencesController { * @param val - 'default' or 'dark' value based on the mode selected by user. */ setTheme(val: ThemeType): void { - this.store.updateState({ theme: val }); + this.update((state) => { + state.theme = val; + }); } /** @@ -540,12 +755,14 @@ export default class PreferencesController { * @param methodData - Corresponding data method */ addKnownMethodData(fourBytePrefix: string, methodData: string): void { - const { knownMethodData } = this.store.getState(); + const { knownMethodData } = this.state; const updatedKnownMethodData = { ...knownMethodData }; updatedKnownMethodData[fourBytePrefix] = methodData; - this.store.updateState({ knownMethodData: updatedKnownMethodData }); + this.update((state) => { + state.knownMethodData = updatedKnownMethodData; + }); } /** @@ -557,9 +774,9 @@ export default class PreferencesController { const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key) ? 'rtl' : 'auto'; - this.store.updateState({ - currentLocale: key, - textDirection, + this.update((state) => { + state.currentLocale = key; + state.textDirection = textDirection; }); return textDirection; } @@ -605,7 +822,7 @@ export default class PreferencesController { * @returns whether this option is on or off. */ getUseRequestQueue(): boolean { - return this.store.getState().useRequestQueue; + return this.state.useRequestQueue; } /** @@ -648,14 +865,15 @@ export default class PreferencesController { * @returns the updated featureFlags object. */ setFeatureFlag(feature: string, activated: boolean): Record { - const currentFeatureFlags = this.store.getState().featureFlags; + const currentFeatureFlags = this.state.featureFlags; const updatedFeatureFlags = { ...currentFeatureFlags, [feature]: activated, }; - this.store.updateState({ featureFlags: updatedFeatureFlags }); - + this.update((state) => { + state.featureFlags = updatedFeatureFlags; + }); return updatedFeatureFlags; } @@ -677,7 +895,9 @@ export default class PreferencesController { [preference]: value, }; - this.store.updateState({ preferences: updatedPreferences }); + this.update((state) => { + state.preferences = updatedPreferences; + }); return updatedPreferences; } @@ -687,7 +907,7 @@ export default class PreferencesController { * @returns A map of user-selected preferences. */ getPreferences(): Preferences { - return this.store.getState().preferences; + return this.state.preferences; } /** @@ -696,7 +916,7 @@ export default class PreferencesController { * @returns The current IPFS gateway domain */ getIpfsGateway(): string { - return this.store.getState().ipfsGateway; + return this.state.ipfsGateway; } /** @@ -706,7 +926,9 @@ export default class PreferencesController { * @returns the update IPFS gateway domain */ setIpfsGateway(domain: string): string { - this.store.updateState({ ipfsGateway: domain }); + this.update((state) => { + state.ipfsGateway = domain; + }); return domain; } @@ -716,7 +938,9 @@ export default class PreferencesController { * @param enabled - Whether or not IPFS is enabled */ setIsIpfsGatewayEnabled(enabled: boolean): void { - this.store.updateState({ isIpfsGatewayEnabled: enabled }); + this.update((state) => { + state.isIpfsGatewayEnabled = enabled; + }); } /** @@ -725,7 +949,9 @@ export default class PreferencesController { * @param useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains */ setUseAddressBarEnsResolution(useAddressBarEnsResolution: boolean): void { - this.store.updateState({ useAddressBarEnsResolution }); + this.update((state) => { + state.useAddressBarEnsResolution = useAddressBarEnsResolution; + }); } /** @@ -739,7 +965,9 @@ export default class PreferencesController { setLedgerTransportPreference( ledgerTransportType: LedgerTransportTypes, ): string { - this.store.updateState({ ledgerTransportType }); + this.update((state) => { + state.ledgerTransportType = ledgerTransportType; + }); return ledgerTransportType; } @@ -749,8 +977,8 @@ export default class PreferencesController { * @param dismissSeedBackUpReminder - User preference for dismissing the back up reminder. */ setDismissSeedBackUpReminder(dismissSeedBackUpReminder: boolean): void { - this.store.updateState({ - dismissSeedBackUpReminder, + this.update((state) => { + state.dismissSeedBackUpReminder = dismissSeedBackUpReminder; }); } @@ -761,18 +989,24 @@ export default class PreferencesController { * @param value - preference of certain network, true to be enabled */ setIncomingTransactionsPreferences(chainId: Hex, value: boolean): void { - const previousValue = this.store.getState().incomingTransactionsPreferences; + const previousValue = this.state.incomingTransactionsPreferences; const updatedValue = { ...previousValue, [chainId]: value }; - this.store.updateState({ incomingTransactionsPreferences: updatedValue }); + this.update((state) => { + state.incomingTransactionsPreferences = updatedValue; + }); } setServiceWorkerKeepAlivePreference(value: boolean): void { - this.store.updateState({ enableMV3TimestampSave: value }); + this.update((state) => { + state.enableMV3TimestampSave = value; + }); } ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setSnapsAddSnapAccountModalDismissed(value: boolean): void { - this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); + this.update((state) => { + state.snapsAddSnapAccountModalDismissed = value; + }); } ///: END:ONLY_INCLUDE_IF @@ -783,7 +1017,7 @@ export default class PreferencesController { newAccountsControllerState.internalAccounts; const selectedAccount = accounts[selectedAccountId]; - const { identities, lostIdentities } = this.store.getState(); + const { identities, lostIdentities } = this.state; const addresses = Object.values(accounts).map((account) => account.address.toLowerCase(), @@ -812,10 +1046,10 @@ export default class PreferencesController { {}, ); - this.store.updateState({ - identities: updatedIdentities, - lostIdentities: updatedLostIdentities, - selectedAddress: selectedAccount?.address || '', // it will be an empty string during onboarding + this.update((state) => { + state.identities = updatedIdentities; + state.lostIdentities = updatedLostIdentities; + state.selectedAddress = selectedAccount?.address || ''; // it will be an empty string during onboarding }); } } diff --git a/app/scripts/lib/backup.js b/app/scripts/lib/backup.js index 7c550c1581ab..c9da3628a99c 100644 --- a/app/scripts/lib/backup.js +++ b/app/scripts/lib/backup.js @@ -18,7 +18,7 @@ export default class Backup { } async restoreUserData(jsonString) { - const existingPreferences = this.preferencesController.store.getState(); + const existingPreferences = this.preferencesController.state; const { preferences, addressBook, network, internalAccounts } = JSON.parse(jsonString); if (preferences) { @@ -26,7 +26,7 @@ export default class Backup { preferences.lostIdentities = existingPreferences.lostIdentities; preferences.selectedAddress = existingPreferences.selectedAddress; - this.preferencesController.store.updateState(preferences); + this.preferencesController.update(preferences); } if (addressBook) { @@ -51,7 +51,7 @@ export default class Backup { async backupUserData() { const userData = { - preferences: { ...this.preferencesController.store.getState() }, + preferences: { ...this.preferencesController.state }, internalAccounts: { internalAccounts: this.accountsController.state.internalAccounts, }, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 0d9712ba5be5..7a322148c847 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -7,8 +7,7 @@ import { mockNetworkState } from '../../../test/stub/networks'; import Backup from './backup'; function getMockPreferencesController() { - const mcState = { - getSelectedAddress: jest.fn().mockReturnValue('0x01'), + const state = { selectedAddress: '0x01', identities: { '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B': { @@ -24,15 +23,14 @@ function getMockPreferencesController() { name: 'Ledger 1', }, }, - update: (store) => (mcState.store = store), }; + const getSelectedAddress = jest.fn().mockReturnValue('0x01'); - mcState.store = { - getState: jest.fn().mockReturnValue(mcState), - updateState: (store) => (mcState.store = store), + return { + state, + getSelectedAddress, + update: jest.fn(), }; - - return mcState; } function getMockAddressBookController() { @@ -239,30 +237,30 @@ describe('Backup', function () { ).toStrictEqual('network-configuration-id-4'); // make sure identities are not lost after restore expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].lastSelected, ).toStrictEqual(1655380342907); expect( - backup.preferencesController.store.identities[ + backup.preferencesController.state.identities[ '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' ].name, ).toStrictEqual('Account 3'); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].lastSelected, ).toStrictEqual(1655379648197); expect( - backup.preferencesController.store.lostIdentities[ + backup.preferencesController.state.lostIdentities[ '0xfd59bbe569376e3d3e4430297c3c69ea93f77435' ].name, ).toStrictEqual('Ledger 1'); // make sure selected address is not lost after restore - expect(backup.preferencesController.store.selectedAddress).toStrictEqual( + expect(backup.preferencesController.state.selectedAddress).toStrictEqual( '0x01', ); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index f0b66430ee84..b96c708be2d3 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -58,13 +58,11 @@ const metaMetricsController = new MetaMetricsController({ segment: createSegmentMock(2, 10000), getCurrentChainId: () => '0x1338', onNetworkDidChange: jest.fn(), - preferencesStore: { - subscribe: jest.fn(), - getState: jest.fn(() => ({ - currentLocale: 'en_US', - preferences: {}, - })), + preferencesControllerState: { + currentLocale: 'en_US', + preferences: {}, }, + onPreferencesStateChange: jest.fn(), version: '0.0.1', environment: 'test', initState: { diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index d0adbefb264b..8977c00aa3d7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -57,17 +57,17 @@ const createMiddleware = ( const ppomController = {}; const preferenceController = { - store: { - getState: () => ({ - securityAlertsEnabled: securityAlertsEnabled ?? true, - }), + state: { + securityAlertsEnabled: securityAlertsEnabled ?? true, }, }; if (error) { - preferenceController.store.getState = () => { - throw error; - }; + Object.defineProperty(preferenceController, 'state', { + get() { + throw error; + }, + }); } const networkController = { diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 1bad576e3881..3b393897b2e0 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -11,7 +11,7 @@ import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; -import PreferencesController from '../../controllers/preferences-controller'; +import { PreferencesController } from '../../controllers/preferences-controller'; import { AppStateController } from '../../controllers/app-state'; import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; // eslint-disable-next-line import/no-restricted-paths @@ -76,8 +76,7 @@ export function createPPOMMiddleware< next: () => void, ) => { try { - const securityAlertsEnabled = - preferencesController.store.getState()?.securityAlertsEnabled; + const { securityAlertsEnabled } = preferencesController.state; const { chainId } = getProviderConfig({ diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 98af29fe38f1..b19c91a232ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -290,7 +290,7 @@ import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { isStreamWritable, setupMultiplex } from './lib/stream-utils'; -import PreferencesController from './controllers/preferences-controller'; +import { PreferencesController } from './controllers/preferences-controller'; import AppStateController from './controllers/app-state'; import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; @@ -600,19 +600,20 @@ export default class MetamaskController extends EventEmitter { name: 'PreferencesController', allowedActions: [ 'AccountsController:setSelectedAccount', + 'AccountsController:getSelectedAccount', 'AccountsController:getAccountByAddress', 'AccountsController:setAccountName', + 'NetworkController:getState', ], allowedEvents: ['AccountsController:stateChange'], }); this.preferencesController = new PreferencesController({ - initState: initState.PreferencesController, - initLangCode: opts.initLangCode, + state: { + currentLocale: opts.initLangCode ?? '', + ...initState.PreferencesController, + }, messenger: preferencesMessenger, - provider: this.provider, - networkConfigurationsByChainId: - this.networkController.state.networkConfigurationsByChainId, }); const tokenListMessenger = this.controllerMessenger.getRestricted({ @@ -624,7 +625,7 @@ export default class MetamaskController extends EventEmitter { this.tokenListController = new TokenListController({ chainId: getCurrentChainId({ metamask: this.networkController.state }), preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( - this.preferencesController.store.getState(), + this.preferencesController.state, ), messenger: tokenListMessenger, state: initState.TokenListController, @@ -738,16 +739,19 @@ export default class MetamaskController extends EventEmitter { addNft: this.nftController.addNft.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] - disabled: - this.preferencesController.store.getState().useNftDetection === - undefined - ? false // the detection is enabled by default - : !this.preferencesController.store.getState().useNftDetection, + disabled: !this.preferencesController.state.useNftDetection, }); this.metaMetricsController = new MetaMetricsController({ segment, - preferencesStore: this.preferencesController.store, + onPreferencesStateChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', + ), + preferencesControllerState: { + currentLocale: this.preferencesController.state.currentLocale, + selectedAddress: this.preferencesController.state.selectedAddress, + }, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', @@ -834,14 +838,17 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - preferencesStore: this.preferencesController.store, + preferencesController: this.preferencesController, messenger: this.controllerMessenger.getRestricted({ name: 'AppStateController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:acceptRequest`, ], - allowedEvents: [`KeyringController:qrKeyringStateChange`], + allowedEvents: [ + `KeyringController:qrKeyringStateChange`, + 'PreferencesController:stateChange', + ], }), extension: this.extension, }); @@ -860,7 +867,7 @@ export default class MetamaskController extends EventEmitter { this.currencyRateController, ); this.currencyRateController.fetchExchangeRate = (...args) => { - if (this.preferencesController.store.getState().useCurrencyRateCheck) { + if (this.preferencesController.state.useCurrencyRateCheck) { return initialFetchExchangeRate(...args); } return { @@ -898,9 +905,10 @@ export default class MetamaskController extends EventEmitter { state: initState.PPOMController, chainId: getCurrentChainId({ metamask: this.networkController.state }), securityAlertsEnabled: - this.preferencesController.store.getState().securityAlertsEnabled, - onPreferencesChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, + this.preferencesController.state.securityAlertsEnabled, + onPreferencesChange: preferencesMessenger.subscribe.bind( + preferencesMessenger, + 'PreferencesController:stateChange', ), cdnBaseUrl: process.env.BLOCKAID_FILE_CDN, blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY, @@ -986,7 +994,8 @@ export default class MetamaskController extends EventEmitter { tokenPricesService: new CodefiTokenPricesServiceV2(), }); - this.preferencesController.store.subscribe( + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', previousValueComparator((prevState, currState) => { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; @@ -995,7 +1004,7 @@ export default class MetamaskController extends EventEmitter { } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { this.tokenRatesController.stop(); } - }, this.preferencesController.store.getState()), + }, this.preferencesController.state), ); this.ensController = new EnsController({ @@ -1259,9 +1268,13 @@ export default class MetamaskController extends EventEmitter { }), state: initState.SelectedNetworkController, useRequestQueuePreference: - this.preferencesController.store.getState().useRequestQueue, - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), + this.preferencesController.state.useRequestQueue, + onPreferencesStateChange: (listener) => { + preferencesMessenger.subscribe( + 'PreferencesController:stateChange', + listener, + ); + }, domainProxyMap: new WeakRefObjectMap(), }); @@ -1366,8 +1379,7 @@ export default class MetamaskController extends EventEmitter { getFeatureFlags: () => { return { disableSnaps: - this.preferencesController.store.getState().useExternalServices === - false, + this.preferencesController.state.useExternalServices === false, }; }, }); @@ -1686,7 +1698,7 @@ export default class MetamaskController extends EventEmitter { }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, - preferencesController: this.preferencesController, + preferencesControllerState: this.preferencesController.state, }); // start and stop polling for balances based on activeControllerConnections @@ -1769,7 +1781,6 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ initState: initState.AlertController, - preferencesStore: this.preferencesController.store, controllerMessenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], @@ -1852,7 +1863,7 @@ export default class MetamaskController extends EventEmitter { getNetworkState: () => this.networkController.state, getPermittedAccounts: this.getPermittedAccounts.bind(this), getSavedGasFees: () => - this.preferencesController.store.getState().advancedGasFee[ + this.preferencesController.state.advancedGasFee[ getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { @@ -1863,8 +1874,7 @@ export default class MetamaskController extends EventEmitter { includeTokenTransfers: false, isEnabled: () => Boolean( - this.preferencesController.store.getState() - .incomingTransactionsPreferences?.[ + this.preferencesController.state.incomingTransactionsPreferences?.[ getCurrentChainId({ metamask: this.networkController.state }) ] && this.onboardingController.state.completedOnboarding, ), @@ -1873,7 +1883,7 @@ export default class MetamaskController extends EventEmitter { }, isMultichainEnabled: process.env.TRANSACTION_MULTICHAIN, isSimulationEnabled: () => - this.preferencesController.store.getState().useTransactionSimulations, + this.preferencesController.state.useTransactionSimulations, messenger: transactionControllerMessenger, onNetworkStateChange: (listener) => { networkControllerMessenger.subscribe( @@ -2138,7 +2148,7 @@ export default class MetamaskController extends EventEmitter { }); const isExternalNameSourcesEnabled = () => - this.preferencesController.store.getState().useExternalNameSources; + this.preferencesController.state.useExternalNameSources; this.nameController = new NameController({ messenger: this.controllerMessenger.getRestricted({ @@ -2353,7 +2363,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, TransactionController: this.txController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, @@ -2408,7 +2418,7 @@ export default class MetamaskController extends EventEmitter { MultichainBalancesController: this.multichainBalancesController, NetworkController: this.networkController, KeyringController: this.keyringController, - PreferencesController: this.preferencesController.store, + PreferencesController: this.preferencesController, MetaMetricsController: this.metaMetricsController.store, MetaMetricsDataDeletionController: this.metaMetricsDataDeletionController, @@ -2531,7 +2541,7 @@ export default class MetamaskController extends EventEmitter { } postOnboardingInitialization() { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; this.networkController.lookupNetwork(); @@ -2540,8 +2550,7 @@ export default class MetamaskController extends EventEmitter { } // post onboarding emit detectTokens event - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useTokenDetection, useNftDetection } = preferencesControllerState ?? {}; this.metaMetricsController.trackEvent({ @@ -2565,8 +2574,7 @@ export default class MetamaskController extends EventEmitter { this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2584,8 +2592,7 @@ export default class MetamaskController extends EventEmitter { this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); - const preferencesControllerState = - this.preferencesController.store.getState(); + const preferencesControllerState = this.preferencesController.state; const { useCurrencyRateCheck } = preferencesControllerState; @@ -2709,7 +2716,7 @@ export default class MetamaskController extends EventEmitter { * @returns The currently selected locale. */ getLocale() { - const { currentLocale } = this.preferencesController.store.getState(); + const { currentLocale } = this.preferencesController.state; return currentLocale; } @@ -2770,8 +2777,7 @@ export default class MetamaskController extends EventEmitter { 'SnapController:updateSnapState', ), maybeUpdatePhishingList: () => { - const { usePhishDetect } = - this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -2842,11 +2848,23 @@ export default class MetamaskController extends EventEmitter { */ setupControllerEventSubscriptions() { let lastSelectedAddress; + this.controllerMessenger.subscribe( + 'PreferencesController:stateChange', + previousValueComparator(async (prevState, currState) => { + const { currentLocale } = currState; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); - this.preferencesController.store.subscribe( - previousValueComparator((prevState, currState) => { - this.#onPreferencesControllerStateChange(currState, prevState); - }, this.preferencesController.store.getState()), + await updateCurrentLocale(currentLocale); + if (currState.incomingTransactionsPreferences?.[chainId]) { + this.txController.startIncomingTransactionPolling(); + } else { + this.txController.stopIncomingTransactionPolling(); + } + + this.#checkTokenListPolling(currState, prevState); + }, this.preferencesController.state), ); this.controllerMessenger.subscribe( @@ -4772,7 +4790,7 @@ export default class MetamaskController extends EventEmitter { const accounts = this.accountsController.listAccounts(); - const { identities } = this.preferencesController.store.getState(); + const { identities } = this.preferencesController.state; return { unlockedAccount, identities, accounts }; } @@ -4971,7 +4989,7 @@ export default class MetamaskController extends EventEmitter { chainId: getCurrentChainId({ metamask: this.networkController.state }), ppomController: this.ppomController, securityAlertsEnabled: - this.preferencesController.store.getState()?.securityAlertsEnabled, + this.preferencesController.state?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), ...otherParams, }; @@ -5143,7 +5161,7 @@ export default class MetamaskController extends EventEmitter { }) { if (sender.url) { if (this.onboardingController.state.completedOnboarding) { - if (this.preferencesController.store.getState().usePhishDetect) { + if (this.preferencesController.state.usePhishDetect) { const { hostname } = new URL(sender.url); this.phishingController.maybeUpdateState(); // Check if new connection is blocked if phishing detection is on @@ -5242,7 +5260,7 @@ export default class MetamaskController extends EventEmitter { * @param {ReadableStream} options.connectionStream - The Duplex stream to connect to. */ setupPhishingCommunication({ connectionStream }) { - const { usePhishDetect } = this.preferencesController.store.getState(); + const { usePhishDetect } = this.preferencesController.state; if (!usePhishDetect) { return; @@ -5636,7 +5654,7 @@ export default class MetamaskController extends EventEmitter { ); const isConfirmationRedesignEnabled = () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .redesignedConfirmationsEnabled; }; @@ -6450,7 +6468,7 @@ export default class MetamaskController extends EventEmitter { return null; } const { knownMethodData, use4ByteResolution } = - this.preferencesController.store.getState(); + this.preferencesController.state; const prefixedData = addHexPrefix(data); return getMethodDataName( knownMethodData, @@ -6463,11 +6481,11 @@ export default class MetamaskController extends EventEmitter { ); }, getIsRedesignedConfirmationsDeveloperEnabled: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .isRedesignedConfirmationsDeveloperEnabled; }, getIsConfirmationAdvancedDetailsOpen: () => { - return this.preferencesController.store.getState().preferences + return this.preferencesController.state.preferences .showConfirmationAdvancedDetails; }, }; @@ -7063,30 +7081,6 @@ export default class MetamaskController extends EventEmitter { }; } - async #onPreferencesControllerStateChange(currentState, previousState) { - const { currentLocale } = currentState; - const chainId = getCurrentChainId({ - metamask: this.networkController.state, - }); - - await updateCurrentLocale(currentLocale); - - if (currentState.incomingTransactionsPreferences?.[chainId]) { - this.txController.startIncomingTransactionPolling(); - } else { - this.txController.stopIncomingTransactionPolling(); - } - - this.#checkTokenListPolling(currentState, previousState); - - // TODO: Remove once the preferences controller has been replaced with the core monorepo implementation - this.controllerMessenger.publish( - 'PreferencesController:stateChange', - currentState, - [], - ); - } - #checkTokenListPolling(currentState, previousState) { const previousEnabled = this.#isTokenListPollingRequired(previousState); const newEnabled = this.#isTokenListPollingRequired(currentState); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index bab66d9bc515..77b062bcfdc7 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -114,22 +114,6 @@ const rpcMethodMiddlewareMock = { }; jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); -jest.mock( - './controllers/preferences-controller', - () => - function (...args) { - const PreferencesController = jest.requireActual( - './controllers/preferences-controller', - ).default; - const controller = new PreferencesController(...args); - // jest.spyOn gets hoisted to the top of this function before controller is initialized. - // This forces us to replace the function directly with a jest stub instead. - // eslint-disable-next-line jest/prefer-spy-on - controller.store.subscribe = jest.fn(); - return controller; - }, -); - const KNOWN_PUBLIC_KEY = '02065bc80d3d12b3688e4ad5ab1e9eda6adf24aec2518bfc21b87c99d4c5077ab0'; @@ -357,10 +341,10 @@ describe('MetaMaskController', () => { let metamaskController; async function simulatePreferencesChange(preferences) { - metamaskController.preferencesController.store.subscribe.mock.lastCall[0]( + metamaskController.controllerMessenger.publish( + 'PreferencesController:stateChange', preferences, ); - await flushPromises(); } @@ -604,8 +588,7 @@ describe('MetaMaskController', () => { await localMetaMaskController.submitPassword(password); const identities = Object.keys( - localMetaMaskController.preferencesController.store.getState() - .identities, + localMetaMaskController.preferencesController.state.identities, ); const addresses = await localMetaMaskController.keyringController.getAccounts(); @@ -937,8 +920,7 @@ describe('MetaMaskController', () => { expect( Object.keys( - metamaskController.preferencesController.store.getState() - .identities, + metamaskController.preferencesController.state.identities, ), ).not.toContain(hardwareKeyringAccount); expect( diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 7ffbd68472d1..107c1bd7ad14 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -40,8 +40,6 @@ "app/scripts/controllers/permissions/selectors.test.js", "app/scripts/controllers/permissions/specifications.js", "app/scripts/controllers/permissions/specifications.test.js", - "app/scripts/controllers/preferences.js", - "app/scripts/controllers/preferences.test.js", "app/scripts/controllers/swaps.js", "app/scripts/controllers/swaps.test.js", "app/scripts/controllers/transactions/index.js", diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ec02c2756185..d7522783f9fc 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2023,6 +2023,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7eaa06a954b0..3df824f29c78 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2115,6 +2115,12 @@ "ethers>@ethersproject/sha2>hash.js": true } }, + "@metamask/preferences-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true + } + }, "@metamask/profile-sync-controller": { "globals": { "Event": true, diff --git a/package.json b/package.json index 90da21554bc2..4973fa0da559 100644 --- a/package.json +++ b/package.json @@ -477,6 +477,7 @@ "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^4.0.0", + "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", "@metamask/test-dapp": "^8.4.0", "@octokit/core": "^3.6.0", diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index e61d7ed807cd..a57a1eea2109 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -6,7 +6,7 @@ import { SignatureController } from '@metamask/signature-controller'; import { NetworkController } from '@metamask/network-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import PreferencesController from '../../app/scripts/controllers/preferences-controller'; +import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { AppStateController } from '../../app/scripts/controllers/app-state'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2c0dfe9a23cb..5d3883a5e8f5 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -1,3 +1,6 @@ +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { FirstTimeFlowType } = require('../../shared/constants/onboarding'); @@ -232,6 +235,29 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index f1e9a7e5ae1d..4c802e13bfa0 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,6 +4,9 @@ const { } = require('@metamask/snaps-utils'); const { merge, mergeWith } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); +const { + ETHERSCAN_SUPPORTED_CHAIN_IDS, +} = require('@metamask/preferences-controller'); const { mockNetworkStateOld } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); @@ -94,6 +97,31 @@ function onboardingFixture() { useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, useRequestQueue: true, + isMultiAccountBalancesEnabled: true, + showIncomingTransactions: { + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.BSC_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.OPTIMISM_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.POLYGON_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.AVALANCHE_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.FANTOM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_GOERLI]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_SEPOLIA]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.LINEA_MAINNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONBEAM_TESTNET]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.MOONRIVER]: true, + [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 559e8a256d43..4658c175bfd5 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -228,7 +228,9 @@ "useExternalNameSources": "boolean", "useTransactionSimulations": true, "enableMV3TimestampSave": true, - "useExternalServices": "boolean" + "useExternalServices": "boolean", + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 2df9ee4e2f23..924769a3cb91 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -45,6 +45,7 @@ "completedOnboarding": true, "knownMethodData": "object", "use4ByteResolution": true, + "showIncomingTransactions": "object", "participateInMetaMetrics": true, "dataCollectionForMarketing": "boolean", "nextNonce": null, @@ -123,6 +124,7 @@ "forgottenPassword": false, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", + "isMultiAccountBalancesEnabled": "boolean", "useAddressBarEnsResolution": true, "ledgerTransportType": "webhid", "snapRegistryList": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index d22b69967027..e2cb7369d88a 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 2dfd6ac6ef21..34cc62d3c560 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -129,7 +129,9 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true + "useRequestQueue": true, + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, diff --git a/yarn.lock b/yarn.lock index bc7fc36aabbe..733c94112452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6057,6 +6057,18 @@ __metadata: languageName: node linkType: hard +"@metamask/preferences-controller@npm:^13.0.2": + version: 13.0.3 + resolution: "@metamask/preferences-controller@npm:13.0.3" + dependencies: + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + peerDependencies: + "@metamask/keyring-controller": ^17.0.0 + checksum: 10/d922c2e603c7a1ef0301dcfc7d5b6aa0bbdd9c318f0857fbbc9e95606609ae806e69c46231288953ce443322039781404565a46fe42bdfa731c4f0da20448d32 + languageName: node + linkType: hard + "@metamask/preinstalled-example-snap@npm:^0.1.0": version: 0.1.0 resolution: "@metamask/preinstalled-example-snap@npm:0.1.0" @@ -26165,6 +26177,7 @@ __metadata: "@metamask/phishing-warning": "npm:^4.0.0" "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" + "@metamask/preferences-controller": "npm:^13.0.2" "@metamask/preinstalled-example-snap": "npm:^0.1.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" From 59dc0cd3c2ebe71f920260ed49d2611dc9d2ba31 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:13:48 +0400 Subject: [PATCH 13/41] feat: Create a quality gate for typescript coverage (#27717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27717?quickstart=1) This PR introduces a quality gate for typescript coverage. It updates the existing fitness function to disallow the creation of new js and jsx files in the repository. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2399 ## **Manual testing steps** 1. After committing a javascript file, the fitness function should fail. ## **Screenshots/Recordings** Not applicable ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .github/workflows/fitness-functions.yml | 6 +- .../common/constants.test.ts | 86 ++++++++++--------- .../fitness-functions/common/constants.ts | 16 ++-- .../fitness-functions/common/shared.test.ts | 84 ++++++++---------- .../fitness-functions/common/shared.ts | 44 ++++++++-- development/fitness-functions/rules/index.ts | 18 ++-- .../rules/javascript-additions.test.ts | 12 +-- .../rules/javascript-additions.ts | 11 +-- .../rules/sinon-assert-syntax.ts | 4 +- 9 files changed, 154 insertions(+), 127 deletions(-) diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index b4979c8f3e7b..f8e24692e8fe 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -2,12 +2,14 @@ name: Fitness Functions CI on: pull_request: - types: [assigned, opened, synchronize, reopened] + types: + - opened + - reopened + - synchronize jobs: build: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/development/fitness-functions/common/constants.test.ts b/development/fitness-functions/common/constants.test.ts index 21912d5fa194..e0077f086594 100644 --- a/development/fitness-functions/common/constants.test.ts +++ b/development/fitness-functions/common/constants.test.ts @@ -1,8 +1,34 @@ -import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants'; +import { E2E_TESTS_REGEX, JS_REGEX } from './constants'; describe('Regular Expressions used in Fitness Functions', (): void => { - describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => { + describe(`E2E_TESTS_REGEX "${E2E_TESTS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + // JS, TS, JSX, and TSX files inside the + // test/e2e directory + 'test/e2e/file.js', + 'test/e2e/path/file.ts', + 'test/e2e/much/longer/path/file.jsx', + 'test/e2e/much/longer/path/file.tsx', + // development/fitness-functions directory + 'development/fitness-functions/file.js', + 'development/fitness-functions/path/file.ts', + 'development/fitness-functions/much/longer/path/file.jsx', + 'development/fitness-functions/much/longer/path/file.tsx', + // development/webpack directory + 'development/webpack/file.js', + 'development/webpack/path/file.ts', + 'development/webpack/much/longer/path/file.jsx', + 'development/webpack/much/longer/path/file.tsx', + ]; + + const PATHS_IT_SHOULD_NOT_MATCH = [ + // any files without JS, TS, JSX or TSX extension + 'file', + 'file.extension', + 'path/file.extension', + 'much/longer/path/file.extension', + // JS, TS, JSX, and TSX files outside + // the test/e2e, development/fitness-functions, development/webpack directories 'file.js', 'path/file.js', 'much/longer/path/file.js', @@ -12,39 +38,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.jsx', 'path/file.jsx', 'much/longer/path/file.jsx', - ]; - - const PATHS_IT_SHOULD_NOT_MATCH = [ - // any without JS, TS, JSX or TSX extension - 'file', - 'file.extension', - 'path/file.extension', - 'much/longer/path/file.extension', - // any in the test/e2e directory - 'test/e2e/file', - 'test/e2e/file.extension', - 'test/e2e/path/file.extension', - 'test/e2e/much/longer/path/file.extension', - 'test/e2e/file.js', - 'test/e2e/path/file.ts', - 'test/e2e/much/longer/path/file.jsx', - 'test/e2e/much/longer/path/file.tsx', - // any in the development/fitness-functions directory - 'development/fitness-functions/file', - 'development/fitness-functions/file.extension', - 'development/fitness-functions/path/file.extension', - 'development/fitness-functions/much/longer/path/file.extension', - 'development/fitness-functions/file.js', - 'development/fitness-functions/path/file.ts', - 'development/fitness-functions/much/longer/path/file.jsx', - 'development/fitness-functions/much/longer/path/file.tsx', + 'file.tsx', + 'path/file.tsx', + 'much/longer/path/file.tsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -53,22 +55,23 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - + const result = E2E_TESTS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); }); }); - describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => { + describe(`JS_REGEX "${JS_REGEX}"`, (): void => { const PATHS_IT_SHOULD_MATCH = [ + 'app/much/longer/path/file.js', + 'app/much/longer/path/file.jsx', + 'offscreen/path/file.js', + 'offscreen/path/file.jsx', 'shared/file.js', - 'shared/path/file.js', - 'shared/much/longer/path/file.js', 'shared/file.jsx', - 'shared/path/file.jsx', - 'shared/much/longer/path/file.jsx', + 'ui/much/longer/path/file.js', + 'ui/much/longer/path/file.jsx', ]; const PATHS_IT_SHOULD_NOT_MATCH = [ @@ -80,13 +83,15 @@ describe('Regular Expressions used in Fitness Functions', (): void => { 'file.ts', 'path/file.ts', 'much/longer/path/file.tsx', + // any JS or JSX files outside the app, offscreen, shared, and ui directories + 'test/longer/path/file.js', + 'random/longer/path/file.jsx', ]; describe('included paths', (): void => { PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { it(`should match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(true); }); }); @@ -95,8 +100,7 @@ describe('Regular Expressions used in Fitness Functions', (): void => { describe('excluded paths', (): void => { PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { it(`should not match "${path}"`, (): void => { - const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); - + const result = JS_REGEX.test(path); expect(result).toStrictEqual(false); }); }); diff --git a/development/fitness-functions/common/constants.ts b/development/fitness-functions/common/constants.ts index 5758d4e2a6e1..f3996a294a5a 100644 --- a/development/fitness-functions/common/constants.ts +++ b/development/fitness-functions/common/constants.ts @@ -1,10 +1,12 @@ -// include JS, TS, JSX, TSX files only excluding files in the e2e tests and -// fitness functions directories -const EXCLUDE_E2E_TESTS_REGEX = - '^(?!test/e2e)(?!development/fitness|development/webpack).*.(js|ts|jsx|tsx)$'; +// include JS, TS, JSX, TSX files only in the +// test/e2e +// development/fitness-functions +// development/webpack directories +const E2E_TESTS_REGEX = + /^(test\/e2e|development\/fitness-functions|development\/webpack).*\.(js|ts|jsx|tsx)$/u; -// include JS and JSX files in the shared directory only -const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$'; +// include JS and JSX files only in the app, offscreen, shared, and ui directories +const JS_REGEX = /^(app|offscreen|shared|ui)\/.*\.(js|jsx)$/u; enum AUTOMATION_TYPE { CI = 'ci', @@ -12,4 +14,4 @@ enum AUTOMATION_TYPE { PRE_PUSH_HOOK = 'pre-push-hook', } -export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE }; +export { E2E_TESTS_REGEX, JS_REGEX, AUTOMATION_TYPE }; diff --git a/development/fitness-functions/common/shared.test.ts b/development/fitness-functions/common/shared.test.ts index 92306b9d4751..66af337fbfc8 100644 --- a/development/fitness-functions/common/shared.test.ts +++ b/development/fitness-functions/common/shared.test.ts @@ -30,13 +30,13 @@ describe('filterDiffFileCreations()', (): void => { const actualResult = filterDiffFileCreations(testFileDiff); expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - new file mode 100644 - index 000000000..30d74d258 - --- /dev/null - +++ b/old-file.js - @@ -0,0 +1 @@ - +ping" + "diff --git a/old-file.js b/old-file.js + new file mode 100644 + index 000000000..30d74d258 + --- /dev/null + +++ b/old-file.js + @@ -0,0 +1 @@ + +ping" `); }); }); @@ -44,9 +44,9 @@ describe('filterDiffFileCreations()', (): void => { describe('hasNumberOfCodeBlocksIncreased()', (): void => { it('should show which code blocks have increased', (): void => { const testDiffFragment = ` - +foo - +bar - +baz`; + +foo + +bar + +baz`; const testCodeBlocks = ['code block 1', 'foo', 'baz']; const actualResult = hasNumberOfCodeBlocksIncreased( @@ -69,7 +69,7 @@ describe('filterDiffByFilePath()', (): void => { it('should return the right diff for a generic matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '.*/.*.(js|ts)$|.*.(js|ts)$', + /^(.*\/)?.*\.(jsx)$/u, // Exclude jsx files ); expect(actualResult).toMatchInlineSnapshot(` @@ -93,35 +93,17 @@ describe('filterDiffByFilePath()', (): void => { }); it('should return the right diff for a specific file in any dir matcher', (): void => { - const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$'); + const actualResult = filterDiffByFilePath(testFileDiff, /.*old-file\.js$/u); // Exclude old-file.js expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js - index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js - @@ -1,3 +1,8 @@ - +ping - @@ -34,33 +39,4 @@ - -pong" - `); - }); - - it('should return the right diff for a multiple file extension (OR) matcher', (): void => { - const actualResult = filterDiffByFilePath( - testFileDiff, - '^(./)*old-file.(js|ts|jsx)$', - ); - - expect(actualResult).toMatchInlineSnapshot(` - "diff --git a/old-file.js b/old-file.js + "diff --git a/new-file.ts b/new-file.ts index 57d5de75c..808d8ba37 100644 - --- a/old-file.js - +++ b/old-file.js + --- a/new-file.ts + +++ b/new-file.ts @@ -1,3 +1,8 @@ - +ping + +foo @@ -34,33 +39,4 @@ - -pong + -bar diff --git a/old-file.jsx b/old-file.jsx index 57d5de75c..808d8ba37 100644 --- a/old-file.jsx @@ -133,10 +115,10 @@ describe('filterDiffByFilePath()', (): void => { `); }); - it('should return the right diff for a file name negation matcher', (): void => { + it('should return the right diff for a multiple file extension (OR) matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, - '^(?!.*old-file.js$).*.[a-zA-Z]+$', + /^(\.\/)*old-file\.(js|ts|jsx)$/u, // Exclude files named old-file that have js, ts, or jsx extensions ); expect(actualResult).toMatchInlineSnapshot(` @@ -147,15 +129,25 @@ describe('filterDiffByFilePath()', (): void => { @@ -1,3 +1,8 @@ +foo @@ -34,33 +39,4 @@ - -bar - diff --git a/old-file.jsx b/old-file.jsx - index 57d5de75c..808d8ba37 100644 - --- a/old-file.jsx - +++ b/old-file.jsx - @@ -1,3 +1,8 @@ - +yin - @@ -34,33 +39,4 @@ - -yang" + -bar" `); }); + + it('should return the right diff for a file name negation matcher', (): void => { + const actualResult = filterDiffByFilePath( + testFileDiff, + /^(?!.*old-file\.js$).*\.[a-zA-Z]+$/u, // Exclude files that do not end with old-file.js but include all other file extensions + ); + + expect(actualResult).toMatchInlineSnapshot(` + "diff --git a/old-file.js b/old-file.js + index 57d5de75c..808d8ba37 100644 + --- a/old-file.js + +++ b/old-file.js + @@ -1,3 +1,8 @@ + +ping + @@ -34,33 +39,4 @@ + -pong" + `); + }); }); diff --git a/development/fitness-functions/common/shared.ts b/development/fitness-functions/common/shared.ts index f7f22101378d..e96073ab5b27 100644 --- a/development/fitness-functions/common/shared.ts +++ b/development/fitness-functions/common/shared.ts @@ -1,11 +1,11 @@ -function filterDiffByFilePath(diff: string, regex: string): string { +function filterDiffByFilePath(diff: string, regex: RegExp): string { // split by `diff --git` and remove the first element which is empty const diffBlocks = diff.split(`diff --git`).slice(1); const filteredDiff = diffBlocks .map((block) => block.trim()) .filter((block) => { - let didAPathInBlockMatchRegEx = false; + let shouldCheckBlock = false; block // get the first line of the block which has the paths @@ -18,12 +18,13 @@ function filterDiffByFilePath(diff: string, regex: string): string { // if at least one of the two paths matches the regex, filter the // corresponding diff block in .forEach((path) => { - if (new RegExp(regex, 'u').test(path)) { - didAPathInBlockMatchRegEx = true; + if (!regex.test(path)) { + // Not excluded, include in check + shouldCheckBlock = true; } }); - return didAPathInBlockMatchRegEx; + return shouldCheckBlock; }) // prepend `git --diff` to each block .map((block) => `diff --git ${block}`) @@ -32,6 +33,34 @@ function filterDiffByFilePath(diff: string, regex: string): string { return filteredDiff; } +function restrictedFilePresent(diff: string, regex: RegExp): boolean { + // split by `diff --git` and remove the first element which is empty + const diffBlocks = diff.split(`diff --git`).slice(1); + let jsOrJsxFilePresent = false; + diffBlocks + .map((block) => block.trim()) + .filter((block) => { + block + // get the first line of the block which has the paths + .split('\n')[0] + .trim() + // split the two paths + .split(' ') + // remove `a/` and `b/` from the paths + .map((path) => path.substring(2)) + // if at least one of the two paths matches the regex, filter the + // corresponding diff block in + .forEach((path) => { + if (regex.test(path)) { + // Not excluded, include in check + jsOrJsxFilePresent = true; + } + }); + return jsOrJsxFilePresent; + }); + return jsOrJsxFilePresent; +} + // This function returns all lines that are additions to files that are being // modified but that previously already existed. Example: // diff --git a/test.js b/test.js @@ -44,7 +73,9 @@ function filterDiffLineAdditions(diff: string): string { const diffLines = diff.split('\n'); const diffAdditionLines = diffLines.filter((line) => { - const isAdditionLine = line.startsWith('+') && !line.startsWith('+++'); + const trimmedLine = line.trim(); + const isAdditionLine = + trimmedLine.startsWith('+') && !trimmedLine.startsWith('+++'); return isAdditionLine; }); @@ -108,6 +139,7 @@ function hasNumberOfCodeBlocksIncreased( export { filterDiffByFilePath, + restrictedFilePresent, filterDiffFileCreations, filterDiffLineAdditions, hasNumberOfCodeBlocksIncreased, diff --git a/development/fitness-functions/rules/index.ts b/development/fitness-functions/rules/index.ts index cd74d286093d..6ba0f1198684 100644 --- a/development/fitness-functions/rules/index.ts +++ b/development/fitness-functions/rules/index.ts @@ -5,23 +5,25 @@ const RULES: IRule[] = [ { name: "Don't use `sinon` or `assert` in unit tests", fn: preventSinonAssertSyntax, - docURL: - 'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', + errorMessage: + '`sinon` or `assert` was detected in the diff. Please use Jest instead. For more info: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', }, { - name: "Don't add JS or JSX files to the `shared` directory", + name: "Don't add JS or JSX files", fn: preventJavaScriptFileAdditions, + errorMessage: + 'The diff includes a newly created JS or JSX file. Please use TS or TSX instead.', }, ]; type IRule = { name: string; fn: (diff: string) => boolean; - docURL?: string; + errorMessage: string; }; function runFitnessFunctionRule(rule: IRule, diff: string): void { - const { name, fn, docURL } = rule; + const { name, fn, errorMessage } = rule; console.log(`Checking rule "${name}"...`); const hasRulePassed: boolean = fn(diff) as boolean; @@ -29,11 +31,7 @@ function runFitnessFunctionRule(rule: IRule, diff: string): void { console.log(`...OK`); } else { console.log(`...FAILED. Changes not accepted by the fitness function.`); - - if (docURL) { - console.log(`For more info: ${docURL}.`); - } - + console.log(errorMessage); process.exit(1); } } diff --git a/development/fitness-functions/rules/javascript-additions.test.ts b/development/fitness-functions/rules/javascript-additions.test.ts index db1803c1d9af..f1ae6e378e37 100644 --- a/development/fitness-functions/rules/javascript-additions.test.ts +++ b/development/fitness-functions/rules/javascript-additions.test.ts @@ -13,11 +13,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should pass when receiving a diff with a new TS file on the shared folder', (): void => { + it('should pass when receiving a diff with a new TS file folder', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'), + generateCreateFileDiff('app/test.ts', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -25,11 +25,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(true); }); - it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JS file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.js', 'yada yada yada yada'), + generateCreateFileDiff('app/test.js', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); @@ -37,11 +37,11 @@ describe('preventJavaScriptFileAdditions()', (): void => { expect(hasRulePassed).toBe(false); }); - it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => { + it('should not pass when receiving a diff with a new JSX file', (): void => { const testDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', undefined, 'pong'), - generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'), + generateCreateFileDiff('app/test.jsx', 'yada yada yada yada'), ].join(''); const hasRulePassed = preventJavaScriptFileAdditions(testDiff); diff --git a/development/fitness-functions/rules/javascript-additions.ts b/development/fitness-functions/rules/javascript-additions.ts index 3e3705ea30f0..0d7c39b07110 100644 --- a/development/fitness-functions/rules/javascript-additions.ts +++ b/development/fitness-functions/rules/javascript-additions.ts @@ -1,15 +1,12 @@ -import { SHARED_FOLDER_JS_REGEX } from '../common/constants'; +import { JS_REGEX } from '../common/constants'; import { - filterDiffByFilePath, filterDiffFileCreations, + restrictedFilePresent, } from '../common/shared'; function preventJavaScriptFileAdditions(diff: string): boolean { - const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX); - const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff); - - const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== ''; - if (hasCreatedAtLeastOneJSFileInShared) { + const diffAdditions = filterDiffFileCreations(diff); + if (restrictedFilePresent(diffAdditions, JS_REGEX)) { return false; } return true; diff --git a/development/fitness-functions/rules/sinon-assert-syntax.ts b/development/fitness-functions/rules/sinon-assert-syntax.ts index 2cc56ec37762..a40c0768ad06 100644 --- a/development/fitness-functions/rules/sinon-assert-syntax.ts +++ b/development/fitness-functions/rules/sinon-assert-syntax.ts @@ -1,4 +1,4 @@ -import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants'; +import { E2E_TESTS_REGEX } from '../common/constants'; import { filterDiffByFilePath, filterDiffFileCreations, @@ -15,7 +15,7 @@ const codeBlocks = [ ]; function preventSinonAssertSyntax(diff: string): boolean { - const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX); + const diffByFilePath = filterDiffByFilePath(diff, E2E_TESTS_REGEX); const diffAdditions = filterDiffFileCreations(diffByFilePath); const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks); From f523617a9faaa70bbf96537067f06e6f1ec6d4b2 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 15 Oct 2024 12:50:29 +0200 Subject: [PATCH 14/41] chore: Add react-beautiful-dnd to deprecated packages list (#27856) ## **Description** Adds `react-beautiful-dnd` to list of deprecated packages that are ignored when using `yarn audit`. This unblocks `develop`. The package is currently in use for the network selection drag and drop functionality and cannot be removed. This PR also removes some packages from the list that were previously ignored, but are no longer in the dependency tree. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27856?quickstart=1) --- .yarnrc.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index 252333917781..f4d8fc7fa471 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -114,15 +114,9 @@ npmAuditIgnoreAdvisories: # upon old versions of ethereumjs-utils. - 'ethereum-cryptography (deprecation)' - # Currently only dependent on deprecated @metamask/types as it is brought in - # by @metamask/keyring-api. Updating the dependency in keyring-api will - # remove this. - - '@metamask/types (deprecation)' - - # @metamask/keyring-api also depends on @metamask/snaps-ui which is - # deprecated. Replacing that dependency with @metamask/snaps-sdk will remove - # this. - - '@metamask/snaps-ui (deprecation)' + # Currently in use for the network list drag and drop functionality. + # Maintenance has stopped and the project will be archived in 2025. + - 'react-beautiful-dnd (deprecation)' npmRegistries: 'https://npm.pkg.github.com': From 670d9cd18c0d23b5704e1009c2f0c1f1ec66da72 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 15 Oct 2024 13:40:39 +0200 Subject: [PATCH 15/41] fix(multichain): fix eth send flow (from dapp) when a btc account is selected (#27566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The extension displays an error (with a stacktrace) whenever a user tries to start a "Send ETH" flow from a dapp while having a Bitcoin account being selected in the wallet. Some UI components rely on the currently selected account to display currencies/network logos, and since Eth is using hex-format when formatting amounts (while Bitcoin is using standard decimal numbers), then the "Send ETH" amount's could not be properly displayed since we were expecting a "Bitcoin number format" but the dapp is sending an hex-formatted number. To avoid having similar issues elsewhere, the `UserPreferencedCurrencyDisplay` component will now fallback to the original EVM behavior if the `account` property is omitted. Meaning that, in a multichain context, you will HAVE TO pass the `account` property to be able to display the correct currency for that account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27566?quickstart=1) ## **Related issues** Fixes: - https://github.com/MetaMask/accounts-planning/issues/616 ## **Manual testing steps** 1. `yarn start:flask` 2. Settings > Experimental > Enable bitcoin support 3. Create a bitcoin account 4. Make sure to have the Bitcoin account being selected in your wallet 5. Go to: https://metamask.github.io/test-dapp/ 6. Connect 1 EVM account 7. Then use "Send" button from the "Send Eth" section 8. You should be able to display a Eth send confirmation on MM ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-01 at 11 08 24](https://github.com/user-attachments/assets/a71aa68b-f0b7-4151-b1eb-0b83fe599032) ### **After** ![Screenshot 2024-10-02 at 15 29 52](https://github.com/user-attachments/assets/70c0eb70-6423-43e6-a06e-0e1b6afb841f) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Daniel Rocha <68558152+danroc@users.noreply.github.com> --- test/jest/mocks.ts | 3 + ...referenced-currency-display.component.d.ts | 2 + ...-preferenced-currency-display.component.js | 13 +- .../app/wallet-overview/coin-overview.tsx | 2 + ui/helpers/utils/util.js | 19 +++ ui/helpers/utils/util.test.js | 49 ++++++++ ui/selectors/multichain.ts | 7 +- ui/selectors/selectors.js | 18 +++ ui/selectors/selectors.test.js | 116 +++++++++++++++++- 9 files changed, 225 insertions(+), 4 deletions(-) diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index ed89b487e3ab..be1120429290 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -180,12 +180,14 @@ export function createMockInternalAccount({ address = MOCK_DEFAULT_ADDRESS, type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, + lastSelected = 0, snapOptions = undefined, }: { name?: string; address?: string; type?: string; keyringType?: string; + lastSelected?: number; snapOptions?: { enabled: boolean; name: string; @@ -228,6 +230,7 @@ export function createMockInternalAccount({ type: keyringType, }, snap: snapOptions, + lastSelected, }, options: {}, methods, diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 3bf65d98d19c..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -1,3 +1,4 @@ +import { InternalAccount } from '@metamask/keyring-api'; import type { CurrencyDisplayProps } from '../../ui/currency-display/currency-display.component'; import type { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; @@ -5,6 +6,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< CurrencyDisplayProps, { type?: PRIMARY | SECONDARY; + account?: InternalAccount; currency?: string; showEthLogo?: boolean; ethNumberOfDecimals?: string | number; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 4b5492091288..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { EtherDenomination } from '../../../../shared/constants/common'; import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'; import CurrencyDisplay from '../../ui/currency-display'; @@ -10,13 +11,14 @@ import { getMultichainCurrentNetwork, } from '../../../selectors/multichain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { getSelectedEvmInternalAccount } from '../../../selectors'; /* eslint-disable jsdoc/require-param-name */ // eslint-disable-next-line jsdoc/require-param /** @param {PropTypes.InferProps>} */ export default function UserPreferencedCurrencyDisplay({ 'data-testid': dataTestId, - account, + account: multichainAccount, ethNumberOfDecimals, fiatNumberOfDecimals, numberOfDecimals: propsNumberOfDecimals, @@ -28,6 +30,15 @@ export default function UserPreferencedCurrencyDisplay({ shouldCheckShowNativeToken, ...restProps }) { + // NOTE: When displaying currencies, we need the actual account to detect whether we're in a + // multichain world or EVM-only world. + // To preserve the original behavior of this component, we default to the lastly selected + // EVM accounts (when used in an EVM-only context). + // The caller has to pass the account in a multichain context to properly display the currency + // here (e.g for Bitcoin). + const evmAccount = useSelector(getSelectedEvmInternalAccount); + const account = multichainAccount ?? evmAccount; + const currentNetwork = useMultichainSelector( getMultichainCurrentNetwork, account, diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index c369ef0e89fd..2de787ef23c0 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -117,6 +117,7 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF + const account = useSelector(getSelectedAccount); const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute( t, t('general'), @@ -254,6 +255,7 @@ export const CoinOverview = ({ {balanceToDisplay ? ( address === targetAddress); } +/** + * Sort the given list of account their selecting order (descending). Meaning the + * first account of the sorted list will be the last selected account. + * + * @param {import('@metamask/keyring-api').InternalAccount[]} accounts - The internal accounts list. + * @returns {import('@metamask/keyring-api').InternalAccount[]} The sorted internal account list. + */ +export function sortSelectedInternalAccounts(accounts) { + // This logic comes from the `AccountsController`: + // TODO: Expose a free function from this controller and use it here + return accounts.sort((accountA, accountB) => { + // Sort by `.lastSelected` in descending order + return ( + (accountB.metadata.lastSelected ?? 0) - + (accountA.metadata.lastSelected ?? 0) + ); + }); +} + /** * Strips the following schemes from URL strings: * - http diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index dd2282efa531..d12a57675343 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -4,6 +4,7 @@ import { CHAIN_IDS } from '../../../shared/constants/network'; import { addHexPrefixToObjectValues } from '../../../shared/lib/swaps-utils'; import { toPrecisionWithoutTrailingZeros } from '../../../shared/lib/transactions-controller-utils'; import { MinPermissionAbstractionDisplayCount } from '../../../shared/constants/permissions'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; import * as util from './util'; describe('util', () => { @@ -1259,4 +1260,52 @@ describe('util', () => { expect(result).toBe(0); }); }); + + describe('sortSelectedInternalAccounts', () => { + const account1 = createMockInternalAccount({ lastSelected: 1 }); + const account2 = createMockInternalAccount({ lastSelected: 2 }); + const account3 = createMockInternalAccount({ lastSelected: 3 }); + // We use a big "gap" here to make sure we're not only sorting with sequential indexes + const accountWithBigSelectedIndexGap = createMockInternalAccount({ + lastSelected: 108912379837, + }); + // We wanna make sure that negative indexes are also being considered properly + const accountWithNegativeSelectedIndex = createMockInternalAccount({ + lastSelected: -1, + }); + + const orderedAccounts = [account3, account2, account1]; + + it.each([ + { accounts: [account1, account2, account3] }, + { accounts: [account2, account3, account1] }, + { accounts: [account3, account2, account1] }, + ])('sorts accounts by descending order: $accounts', ({ accounts }) => { + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount).toStrictEqual(orderedAccounts); + }); + + it('sorts accounts with bigger gap', () => { + const accounts = [account1, accountWithBigSelectedIndexGap, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[0]).toStrictEqual(accountWithBigSelectedIndexGap); + }); + + it('sorts accounts with negative `lastSelected` index', () => { + const accounts = [account1, accountWithNegativeSelectedIndex, account3]; + const sortedAccount = util.sortSelectedInternalAccounts(accounts); + expect(sortedAccount.length).toBeGreaterThan(0); // Required since we using `length - 1` + expect(sortedAccount).toHaveLength(accounts.length); + expect(sortedAccount[sortedAccount.length - 1]).toStrictEqual( + accountWithNegativeSelectedIndex, + ); + }); + + it('succeed with no accounts', () => { + const sortedAccount = util.sortSelectedInternalAccounts([]); + expect(sortedAccount).toStrictEqual([]); + }); + }); }); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1148e8d86468..b676da209046 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -231,8 +231,11 @@ export function getMultichainProviderConfig( return getMultichainNetwork(state, account).network; } -export function getMultichainCurrentNetwork(state: MultichainState) { - return getMultichainProviderConfig(state); +export function getMultichainCurrentNetwork( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainProviderConfig(state, account); } export function getMultichainNativeCurrency( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 2059c3a4678d..09c062012731 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -67,6 +67,7 @@ import { shortenAddress, getAccountByAddress, getURLHostName, + sortSelectedInternalAccounts, } from '../helpers/utils/util'; import { @@ -388,6 +389,23 @@ export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } +export const getEvmInternalAccounts = createSelector( + getInternalAccounts, + (accounts) => { + return accounts.filter((account) => isEvmAccountType(account.type)); + }, +); + +export const getSelectedEvmInternalAccount = createSelector( + getEvmInternalAccounts, + (accounts) => { + // We should always have 1 EVM account (if not, it would be `undefined`, same + // as `getSelectedInternalAccount` selector. + const [evmAccountSelected] = sortSelectedInternalAccounts(accounts); + return evmAccountSelected; + }, +); + /** * Returns an array of internal accounts sorted by keyring. * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 24b2a2afe125..8d71048e0924 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1,6 +1,10 @@ import { deepClone } from '@metamask/snaps-utils'; import { ApprovalType } from '@metamask/controller-utils'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + EthMethod, +} from '@metamask/keyring-api'; import { TransactionStatus } from '@metamask/transaction-controller'; import mockState from '../../test/data/mock-state.json'; import { KeyringType } from '../../shared/constants/keyring'; @@ -36,6 +40,21 @@ const modifyStateWithHWKeyring = (keyring) => { return modifiedState; }; +const mockAccountsState = (accounts) => { + const accountsMap = accounts.reduce((map, account) => { + map[account.id] = account; + return map; + }, {}); + + return { + metamask: { + internalAccounts: { + accounts: accountsMap, + }, + }, + }; +}; + describe('Selectors', () => { describe('#getSelectedAddress', () => { it('returns undefined if selectedAddress is undefined', () => { @@ -2080,4 +2099,99 @@ describe('#getConnectedSitesList', () => { ).toStrictEqual('INITIALIZED'); }); }); + + describe('getEvmInternalAccounts', () => { + const account1 = createMockInternalAccount({ + keyringType: KeyringType.hd, + }); + const account2 = createMockInternalAccount({ + type: EthAccountType.Erc4337, + keyringType: KeyringType.snap, + }); + const account3 = createMockInternalAccount({ + keyringType: KeyringType.imported, + }); + const account4 = createMockInternalAccount({ + keyringType: KeyringType.ledger, + }); + const account5 = createMockInternalAccount({ + keyringType: KeyringType.trezor, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + }); + + const evmAccounts = [account1, account2, account3, account4, account5]; + + it('returns all EVM accounts when only EVM accounts are present', () => { + const state = mockAccountsState(evmAccounts); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('only returns EVM accounts when there are non-EVM accounts', () => { + const state = mockAccountsState([ + ...evmAccounts, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual( + evmAccounts, + ); + }); + + it('returns an empty array when there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getEvmInternalAccounts(state)).toStrictEqual([]); + }); + }); + + describe('getSelectedEvmInternalAccount', () => { + const account1 = createMockInternalAccount({ + lastSelected: 1, + }); + const account2 = createMockInternalAccount({ + lastSelected: 2, + }); + const account3 = createMockInternalAccount({ + lastSelected: 3, + }); + const nonEvmAccount1 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 4, + }); + const nonEvmAccount2 = createMockInternalAccount({ + type: BtcAccountType.P2wpkh, + keyringType: KeyringType.snap, + lastSelected: 5, + }); + + it('returns the last selected EVM account', () => { + const state = mockAccountsState([account1, account2, account3]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns the last selected EVM account when there are non-EVM accounts', () => { + const state = mockAccountsState([ + account1, + account2, + account3, + nonEvmAccount1, + nonEvmAccount2, + ]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(account3); + }); + + it('returns `undefined` if there are no EVM accounts', () => { + const state = mockAccountsState([nonEvmAccount1, nonEvmAccount2]); + expect(selectors.getSelectedEvmInternalAccount(state)).toBe(undefined); + }); + }); }); From 0793e750c5ce571bc4549df5efd2701d47e1cf6c Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Tue, 15 Oct 2024 15:30:56 +0200 Subject: [PATCH 16/41] fix: dismiss addToken modal for mmi (#27855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to fix dismissing modal to add suggested tokens when building mmi. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27855?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27854 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- ui/store/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 91453590791c..9c5ab7ebb45e 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4020,7 +4020,7 @@ export function resolvePendingApproval( // Before closing the current window, check if any additional confirmations // are added as a result of this confirmation being accepted - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) const { pendingApprovals } = await forceUpdateMetamaskState(_dispatch); if (Object.values(pendingApprovals).length === 0) { _dispatch(closeCurrentNotificationWindow()); From f880da8114209a65c6466616fc89fe45587f1362 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 15 Oct 2024 19:02:22 +0530 Subject: [PATCH 17/41] fix: Reset nonce as network is switched (#27789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with nonce being not reset when switching network. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27788 ## **Manual testing steps** 1. Go to test dapp 2. Switch network between submitting transactions 3. Ensure that nonce is correct each time ## **Screenshots/Recordings** TODO ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../multichain/network-list-menu/network-list-menu.test.js | 6 ++++++ .../multichain/network-list-menu/network-list-menu.tsx | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index c6be491a1aaa..c140189cbf81 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -19,11 +19,15 @@ const mockSetShowTestNetworks = jest.fn(); const mockToggleNetworkMenu = jest.fn(); const mockSetNetworkClientIdForDomain = jest.fn(); const mockSetActiveNetwork = jest.fn(); +const mockUpdateCustomNonce = jest.fn(); +const mockSetNextNonce = jest.fn(); jest.mock('../../../store/actions.ts', () => ({ setShowTestNetworks: () => mockSetShowTestNetworks, setActiveNetwork: () => mockSetActiveNetwork, toggleNetworkMenu: () => mockToggleNetworkMenu, + updateCustomNonce: () => mockUpdateCustomNonce, + setNextNonce: () => mockSetNextNonce, setNetworkClientIdForDomain: (network, id) => mockSetNetworkClientIdForDomain(network, id), })); @@ -206,6 +210,8 @@ describe('NetworkListMenu', () => { fireEvent.click(getByText(MAINNET_DISPLAY_NAME)); expect(mockToggleNetworkMenu).toHaveBeenCalled(); expect(mockSetActiveNetwork).toHaveBeenCalled(); + expect(mockUpdateCustomNonce).toHaveBeenCalled(); + expect(mockSetNextNonce).toHaveBeenCalled(); }); it('shows the correct selected network when networks share the same chain ID', () => { diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 6dc4457cceb5..5376dc17859e 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -26,6 +26,8 @@ import { setEditedNetwork, grantPermittedChain, showPermittedNetworkToast, + updateCustomNonce, + setNextNonce, } from '../../../store/actions'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, @@ -277,6 +279,8 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { network.rpcEndpoints[network.defaultRpcEndpointIndex]; dispatch(setActiveNetwork(networkClientId)); dispatch(toggleNetworkMenu()); + dispatch(updateCustomNonce('')); + dispatch(setNextNonce('')); if (permittedAccountAddresses.length > 0) { grantPermittedChain(selectedTabOrigin, network.chainId); From 85bf4c361cc243713b3aab87a637fb2769501e05 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 15 Oct 2024 14:56:38 +0100 Subject: [PATCH 18/41] perf: include custom traces in benchmark results (#27701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Extend the `benchmark:chrome` and `benchmark:firefox` scripts to also record the duration of the startup custom traces. This provides a mechanism for developers to more easily test the impact of performance changes with reduced variance due to the ability to gather multiple samples. Sample from this pull request: ``` { "home": { ... "average": { "firstPaint": 1834.9400000002236, "domContentLoaded": 1802.5400000001305, "load": 1835.6650000001305, "domInteractive": 46.270000000298026, "backgroundConnect": 38.23001708984375, "firstReactRender": 86.8, "getState": 10.7, "initialActions": 0.15, "loadScripts": 1354.3799926757813, "setupStore": 23.3, "uiStartup": 2023.185009765625 }, ... } ``` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27701?quickstart=1) ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/lib/trace.ts | 28 +++++++++++++++++++++++----- test/e2e/benchmark.js | 17 ++++++++++++++++- test/e2e/webdriver/driver.js | 5 ++++- types/global.d.ts | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 5ca256371502..ab1deefd1cc5 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -32,6 +32,11 @@ const ID_DEFAULT = 'default'; const OP_DEFAULT = 'custom'; const tracesByKey: Map = new Map(); +const durationsByName: { [name: string]: number } = {}; + +if (process.env.IN_TEST && globalThis.stateHooks) { + globalThis.stateHooks.getCustomTraces = () => durationsByName; +} type PendingTrace = { end: (timestamp?: number) => void; @@ -155,9 +160,8 @@ export function endTrace(request: EndTraceRequest) { const { request: pendingRequest, startTime } = pendingTrace; const endTime = timestamp ?? getPerformanceTimestamp(); - const duration = endTime - startTime; - log('Finished trace', name, id, duration, { request: pendingRequest }); + logTrace(pendingRequest, startTime, endTime); } function traceCallback(request: TraceRequest, fn: TraceCallback): T { @@ -181,9 +185,7 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T { }, () => { const end = Date.now(); - const duration = end - start; - - log('Finished trace', name, duration, { error, request }); + logTrace(request, start, end, error); }, ) as T; }; @@ -242,6 +244,22 @@ function startSpan( }); } +function logTrace( + request: TraceRequest, + startTime: number, + endTime: number, + error?: unknown, +) { + const duration = endTime - startTime; + const { name } = request; + + if (process.env.IN_TEST) { + durationsByName[name] = duration; + } + + log('Finished trace', name, duration, { request, error }); +} + function getTraceId(request: TraceRequest) { return request.id ?? ID_DEFAULT; } diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index 738d766f8555..1f24a960d9eb 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -17,6 +17,16 @@ const FixtureBuilder = require('./fixture-builder'); const DEFAULT_NUM_SAMPLES = 20; const ALL_PAGES = Object.values(PAGES); +const CUSTOM_TRACES = { + backgroundConnect: 'Background Connect', + firstReactRender: 'First Render', + getState: 'Get State', + initialActions: 'Initial Actions', + loadScripts: 'Load Scripts', + setupStore: 'Setup Store', + uiStartup: 'UI Startup', +}; + async function measurePage(pageName) { let metrics; await withFixtures( @@ -32,6 +42,7 @@ async function measurePage(pageName) { await driver.findElement('[data-testid="account-menu-icon"]'); await driver.navigate(pageName); await driver.delay(1000); + metrics = await driver.collectMetrics(); }, ); @@ -79,7 +90,7 @@ async function profilePageLoad(pages, numSamples, retries) { runResults.push(result); } - if (runResults.some((result) => result.navigation.lenth > 1)) { + if (runResults.some((result) => result.navigation.length > 1)) { throw new Error(`Multiple navigations not supported`); } else if ( runResults.some((result) => result.navigation[0].type !== 'navigate') @@ -107,6 +118,10 @@ async function profilePageLoad(pages, numSamples, retries) { ), }; + for (const [key, name] of Object.entries(CUSTOM_TRACES)) { + result[key] = runResults.map((metrics) => metrics[name]); + } + results[pageName] = { min: minResult(result), max: maxResult(result), diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index a03a0d1cbd04..fb8aed3d28a6 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -1313,7 +1313,10 @@ function collectMetrics() { }); }); - return results; + return { + ...results, + ...window.stateHooks.getCustomTraces(), + }; } module.exports = { Driver, PAGES }; diff --git a/types/global.d.ts b/types/global.d.ts index 95fb6c98547a..8078a3998bde 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -239,6 +239,7 @@ type HttpProvider = { }; type StateHooks = { + getCustomTraces?: () => { [name: string]: number }; getCleanAppState?: () => Promise; getLogs?: () => any[]; getMostRecentPersistedState?: () => any; From 3452eb915c8eb9fd92047701059bdbd50fda07ae Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:57:33 +0200 Subject: [PATCH 19/41] fix: flaky test `MultiRpc: should select rpc from settings @no-mmi` (#27858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27858?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27851 ## **Manual testing steps** 1. Check ci 2. Run test locally ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/tests/network/multi-rpc.spec.ts | 27 +++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index c9fa95f986e9..af2ef47e93fb 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -396,12 +396,10 @@ describe('MultiRpc:', function (this: Suite) { await driver.delay(regularDelayMs); // go to advanced settigns - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Manage default settings', }); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'General', }); @@ -420,23 +418,18 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Save', tag: 'button', }); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="category-back-button"]'); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.waitForSelector( - '[data-testid="privacy-settings-back-button"]', - ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -446,7 +439,7 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -461,7 +454,17 @@ describe('MultiRpc:', function (this: Suite) { '“Arbitrum One” was successfully edited!', ); // Ensures popover backround doesn't kill test - await driver.delay(regularDelayMs); + await driver.assertElementNotPresent('.popover-bg'); + + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); + + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); + await driver.clickElement('[data-testid="network-display"]'); const arbitrumRpcUsed = await driver.findElement({ From 58142243e4a9626113a7a45ccdf48ff3b03797bc Mon Sep 17 00:00:00 2001 From: martahj Date: Tue, 15 Oct 2024 10:16:18 -0500 Subject: [PATCH 20/41] fix: hackily wait longer for linea swap approval tx to increase chance of success (#27810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Linea seems to be taking longer than other chains to process the approve transaction after it is submitted, so the trade transaction is erroring. This PR adds a hacky workaround where we artificially delay if we're on Linea to give the trade transaction more time. In the future, we'd want to avoid this hack, but for now it should increase the swap success rate on Linea. With the delay, the token symbol also wasn't immediately populating on the awaiting swap page, so this PR also updates how it's retrieved. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27810?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27804 ## **Manual testing steps** 1. Start a swap on Linea with a token that you have not granted approval for 2. Observe that the swap does not fail ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/57cdc5e5-cea7-48ad-ba13-38820ecc9155 ### **After** https://github.com/user-attachments/assets/91bbfbf4-8392-41ea-bfe8-d54813758f5c ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/ducks/swaps/swaps.js | 16 ++++++++++++++ ui/pages/swaps/awaiting-swap/awaiting-swap.js | 21 +++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index efbd781f943f..cf8348243238 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -6,6 +6,7 @@ import { captureMessage } from '@sentry/browser'; import { TransactionType } from '@metamask/transaction-controller'; import { createProjectLogger } from '@metamask/utils'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { addToken, addTransactionAndWaitForPublish, @@ -1284,6 +1285,21 @@ export const signAndSendTransactions = ( }, }, ); + if ( + [ + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.LINEA_GOERLI, + CHAIN_IDS.LINEA_SEPOLIA, + ].includes(chainId) + ) { + debugLog( + 'Delaying submitting trade tx to make Linea confirmation more likely', + ); + const waitPromise = new Promise((resolve) => + setTimeout(resolve, 5000), + ); + await waitPromise; + } } catch (e) { debugLog('Approve transaction failed', e); await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR)); diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index e7f47bc3f006..660f7ef4fcae 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -97,6 +97,9 @@ export default function AwaitingSwap({ const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState(false); + const destinationTokenSymbol = + usedQuote?.destinationTokenInfo?.symbol || swapMetaData?.token_to; + let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { @@ -107,7 +110,7 @@ export default function AwaitingSwap({ currentCurrency, conversionRate: usdConversionRate, tradeValue: usedQuote?.trade?.value, - sourceSymbol: swapMetaData?.token_from, + sourceSymbol: usedQuote?.sourceTokenInfo?.symbol, sourceAmount: usedQuote.sourceAmount, chainId, }); @@ -123,13 +126,14 @@ export default function AwaitingSwap({ const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); + const swapSlippage = swapMetaData?.slippage || usedQuote?.slippage; const sensitiveProperties = { - token_from: swapMetaData?.token_from, + token_from: swapMetaData?.token_from || usedQuote?.sourceTokenInfo?.symbol, token_from_amount: swapMetaData?.token_from_amount, - token_to: swapMetaData?.token_to, + token_to: destinationTokenSymbol, request_type: fetchParams?.balanceError ? 'Quote' : 'Order', - slippage: swapMetaData?.slippage, - custom_slippage: swapMetaData?.slippage === 2, + slippage: swapSlippage, + custom_slippage: swapSlippage === 2, gas_fees: feeinUnformattedFiat, is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, @@ -137,7 +141,6 @@ export default function AwaitingSwap({ current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }; - const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? @@ -234,7 +237,7 @@ export default function AwaitingSwap({ className="awaiting-swap__amount-and-symbol" data-testid="awaiting-swap-amount-and-symbol" > - {swapMetaData?.token_to} + {destinationTokenSymbol} , ]); content = blockExplorerUrl && ( @@ -252,7 +255,7 @@ export default function AwaitingSwap({ key="swapTokenAvailable-2" className="awaiting-swap__amount-and-symbol" > - {`${tokensReceived || ''} ${swapMetaData?.token_to}`} + {`${tokensReceived || ''} ${destinationTokenSymbol}`} , ]); content = blockExplorerUrl && ( @@ -317,7 +320,7 @@ export default function AwaitingSwap({ } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); } else if ( - isSwapsDefaultTokenSymbol(swapMetaData?.token_to, chainId) || + isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) || swapComplete ) { history.push(DEFAULT_ROUTE); From 2b45577566518deff4cbacd557660b7eef0657b5 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:27:17 +0200 Subject: [PATCH 21/41] fix: flaky test `Add account should not affect public address when using secret recovery phrase to recover account with non-zero balance @no-mmi` (#27834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** There is a race condition that after clicking Add Account button, for adding an account, the dialog remains open and the account is not added. Then the test fails when trying to click an element which is below the dialog: `ElementClickInterceptedError: element click intercepted:` ![Screenshot from 2024-10-14 17-40-45](https://github.com/user-attachments/assets/8cb59dfd-d662-460e-a228-a1e033bdecba) ![image](https://github.com/user-attachments/assets/ee45e300-f13b-4daa-a667-b4ea8257483a) Unfortunately there is no UI condition we can wait for, to know when the form is ready after adding our input, given that the Add Account button is enabled from start, so the click will never fail, despite the component not being ready/needing to update. You can see an illustration of this in the video below, despite not being able to reproduce it locally. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27834?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27837 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** See how there is no UI way to tell we can click the button, as it is already enabled. Then, when we update the input and quickly click Add Account, unexpected things can happen: ie in this case the last updated value is not applied https://github.com/user-attachments/assets/982b2d33-1bd0-42e0-99a3-0f13fa571620 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/tests/account/add-account.spec.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js index c1a6136cc47d..04980cf20c3e 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -42,6 +42,9 @@ describe('Add account', function () { ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', @@ -86,6 +89,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Check address of 2nd account @@ -109,16 +115,11 @@ describe('Add account', function () { '[data-testid="account-options-menu-button"]', ); - await driver.delay(regularDelayMs); - await driver.waitForSelector('[data-testid="global-menu-lock"]'); await driver.clickElement('[data-testid="global-menu-lock"]'); await driver.waitForSelector('[data-testid="unlock-page"]'); // Recover via SRP in "forget password" option - const restoreSeedLink = await driver.findClickableElement( - '.unlock-page__link', - ); - await restoreSeedLink.click(); + await driver.clickElement('.unlock-page__link'); await driver.pasteIntoField( '[data-testid="import-srp__srp-word-0"]', TEST_SEED_PHRASE, @@ -126,7 +127,6 @@ describe('Add account', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.fill('#confirm-password', 'correct horse battery staple'); - await driver.delay(regularDelayMs); await driver.clickElement( '[data-testid="create-new-vault-submit-button"]', ); @@ -171,6 +171,9 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Add account', tag: 'button' }); // Wait for 2nd account to be created From 581b7fb9cf08552096f4eb08375447b44d057f87 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Tue, 15 Oct 2024 16:34:37 +0100 Subject: [PATCH 22/41] fix: updated permissions flow copy changes (#27658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is to update the copy changes in Permissions Screen ## **Related issues** Fixes: [https://github.com/MetaMask/MetaMask-planning/issues/3391](https://github.com/MetaMask/MetaMask-planning/issues/3391) ## **Manual testing steps** 1. Run extension with yarn start 2. For all permissions page, there should be no extra space between "[x] accounts" and the "•" in the list item description 3. Click on connected permission, On Permissions Page check 4px spacing between the Permission label and the secondary description 4. Click Disconnect button and the Modal copy should be "If you disconnect from this site, you’ll need to reconnect your accounts and networks to use this site again." 5. When no account is connected, copy should be updated as well For tooltips, I will file a separate issue and we are not adding any badge for avatars on permissions page ## **Screenshots/Recordings** ### **Before** ### **After** ![Screenshot 2024-10-07 at 1 35 23 PM](https://github.com/user-attachments/assets/d6c4c14a-d514-449e-bc3d-25925335d144) ![Screenshot 2024-10-07 at 1 36 35 PM](https://github.com/user-attachments/assets/75fd34e9-a17a-470f-9788-982a2248c638) ![Screenshot 2024-10-07 at 1 36 47 PM](https://github.com/user-attachments/assets/d94c8768-7892-4759-81e3-3e585e763595) ![Screenshot 2024-10-07 at 1 37 12 PM](https://github.com/user-attachments/assets/08abec2c-3abd-4020-86e9-53815305ac9d) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 3 --- app/_locales/el/messages.json | 3 --- app/_locales/en/messages.json | 11 +++++----- app/_locales/es/messages.json | 3 --- app/_locales/fr/messages.json | 3 --- app/_locales/hi/messages.json | 3 --- app/_locales/id/messages.json | 3 --- app/_locales/ja/messages.json | 3 --- app/_locales/ko/messages.json | 3 --- app/_locales/pt/messages.json | 3 --- app/_locales/ru/messages.json | 3 --- app/_locales/tl/messages.json | 3 --- app/_locales/tr/messages.json | 3 --- app/_locales/vi/messages.json | 3 --- app/_locales/zh_CN/messages.json | 3 --- .../disconnect-all-modal.tsx | 3 +-- .../no-connections.test.tsx.snap | 2 +- .../connections/components/no-connection.tsx | 2 +- .../permissions-page.test.js.snap | 2 +- .../permissions-page/connection-list-item.js | 3 +-- .../review-permissions-page.test.tsx.snap | 12 ++--------- .../review-permissions-page.tsx | 20 +++++++++++-------- ...ite-cell-connection-list-item.test.js.snap | 2 +- .../site-cell-connection-list-item.js | 1 + .../__snapshots__/connect-page.test.tsx.snap | 4 ++-- 25 files changed, 28 insertions(+), 76 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9931e17a83a7..296f1c716297 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Keine Konten für die angegebene Suchanfrage gefunden" }, - "noConnectedAccountDescription": { - "message": "Wählen Sie ein Konto, das Sie auf dieser Website verwenden möchten, um fortzufahren." - }, "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 95e1e43cf51f..6adad0a49176 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Δεν βρέθηκαν λογαριασμοί για το συγκεκριμένο αίτημα αναζήτησης" }, - "noConnectedAccountDescription": { - "message": "Επιλέξτε έναν λογαριασμό που θέλετε να χρησιμοποιήσετε σε αυτόν τον ιστότοπο για να συνεχίσετε." - }, "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 862b761abd8f..6970fbb4473c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1633,9 +1633,8 @@ "disconnectAllAccountsText": { "message": "accounts" }, - "disconnectAllDescription": { - "message": "If you disconnect from $1, you’ll need to reconnect your accounts and networks to use this site again.", - "description": "$1 represents the website hostname" + "disconnectAllDescriptionText": { + "message": "If you disconnect from this site, you’ll need to reconnect your accounts and networks to use this site again." }, "disconnectAllSnapsText": { "message": "Snaps" @@ -3313,12 +3312,12 @@ "noAccountsFound": { "message": "No accounts found for the given search query" }, - "noConnectedAccountDescription": { - "message": "Select an account you want to use on this site to continue." - }, "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, + "noConnectionDescription": { + "message": "To connect to a site, find and select the \"connect\" button. Remember MetaMask can only connect to sites on web3" + }, "noConversionRateAvailable": { "message": "No conversion rate available" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 9fd0f3d20941..03fa1e519ef7 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -3028,9 +3028,6 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConnectedAccountDescription": { - "message": "Seleccione una cuenta que desee utilizar en este sitio para continuar." - }, "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 05c67e49462f..fccc617dee93 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Aucun compte trouvé pour la demande de recherche effectuée" }, - "noConnectedAccountDescription": { - "message": "Sélectionnez un compte que vous souhaitez utiliser sur ce site pour continuer." - }, "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index e23b10a874f0..7333626d1e30 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "दी गई खोज क्वेरी के लिए कोई अकाउंट नहीं मिला" }, - "noConnectedAccountDescription": { - "message": "जारी रखने के लिए जिस अकाउंट को आप इस साइट पर उपयोग करना चाहते हैं वह अकाउंट चुनें।" - }, "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 12ab926cf9ce..f2d8828e9226 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Tidak ditemukan akun untuk kueri pencarian yang diberikan" }, - "noConnectedAccountDescription": { - "message": "Pilih akun yang ingin Anda gunakan di situs ini untuk melanjutkan." - }, "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 0c3887643691..cadc1ab1e302 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "指定された検索クエリでアカウントが見つかりませんでした" }, - "noConnectedAccountDescription": { - "message": "続行するには、このサイトで使用するアカウントを選択してください。" - }, "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6e4dad181512..760ad7df43dc 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "검색어에 해당하는 계정이 없습니다." }, - "noConnectedAccountDescription": { - "message": "이 사이트에서 계속 사용하고자 하는 계정을 선택하세요." - }, "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 47a53a6ed328..06c9fbe38adf 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a pesquisa efetuada" }, - "noConnectedAccountDescription": { - "message": "Selecione uma conta que você deseja usar neste site para continuar." - }, "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0ecd4f0eb8d6..2308eb10721e 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "По данному поисковому запросу счетов не найдено" }, - "noConnectedAccountDescription": { - "message": "Для продолжения выберите счет, который вы хотите использовать на этом сайте." - }, "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c6614483aa5b..8e3d8fd7fdd0 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Walang nakitang account para sa ibinigay na query sa paghahanap" }, - "noConnectedAccountDescription": { - "message": "Pumili ng account na gusto mong gamitin sa site na ito para magpatuloy." - }, "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 361b92cdd87e..06d2f1de953f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Belirtilen arama sorgusu için hesap bulunamadı" }, - "noConnectedAccountDescription": { - "message": "Devam etmek için bu sitede kullanmak istediğiniz bir hesap seçin." - }, "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3be725af9351..89772c1d4eec 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "Không tìm thấy tài khoản nào cho cụm từ tìm kiếm đã đưa ra" }, - "noConnectedAccountDescription": { - "message": "Chọn tài khoản mà bạn muốn sử dụng trên trang web này để tiếp tục." - }, "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 58298abdf542..b4816b165545 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -3031,9 +3031,6 @@ "noAccountsFound": { "message": "未找到符合给定查询条件的账户" }, - "noConnectedAccountDescription": { - "message": "选择要在此站点上使用的账户以继续。" - }, "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, diff --git a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx index 62ca0ed8093a..0170abc79fc7 100644 --- a/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx +++ b/ui/components/multichain/disconnect-all-modal/disconnect-all-modal.tsx @@ -19,7 +19,6 @@ export enum DisconnectType { } export const DisconnectAllModal = ({ - hostname, onClick, onClose, }: { @@ -36,7 +35,7 @@ export const DisconnectAllModal = ({ {t('disconnect')} - {{t('disconnectAllDescription', [hostname])}} + {{t('disconnectAllDescriptionText')}} - + /> diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx index 022b508984cd..b12fea776c65 100644 --- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx @@ -265,14 +265,18 @@ export const ReviewPermissions = () => { ) : ( - - {t('connectAccounts')} - + <> + {connectedAccountAddresses.length > 0 ? ( + + {t('connectAccounts')} + + ) : null} + )} diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap index ae198ab79882..fba510aba170 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/__snapshots__/site-cell-connection-list-item.test.js.snap @@ -15,7 +15,7 @@ exports[`SiteCellConnectionListItem renders correctly with required props 1`] = />

{title} diff --git a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap index 6353df3e96cc..e416011c1b08 100644 --- a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -51,7 +51,7 @@ exports[`ConnectPage should render correctly 1`] = ` />

Date: Tue, 15 Oct 2024 17:07:03 +0100 Subject: [PATCH 23/41] feat: update copy for 'Default settings' (#27821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates the copy for the onboarding message screen and the onboarding settings screen. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27821?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3496 ## **Manual testing steps** 1. For the following scenarios: a. Wallet created w backup b. Wallet created w/o backup c. Wallet imported 3. The copy of the success screens and the settings screen should be updated as shown in the Screenshots: ## **Screenshots/Recordings** Screenshot 2024-10-14 at 12 56 40 Screenshot 2024-10-14 at 12 57 23 ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 4 ++-- test/e2e/helpers.js | 5 ++++- test/e2e/tests/network/multi-rpc.spec.ts | 2 +- test/e2e/tests/onboarding/onboarding.spec.js | 4 ++-- test/e2e/tests/privacy/basic-functionality.spec.js | 4 ++-- .../creation-successful/creation-successful.test.js | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6970fbb4473c..57dd9152752e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1548,7 +1548,7 @@ "message": "MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy." }, "defaultSettingsTitle": { - "message": "Default settings" + "message": "Default privacy settings" }, "delete": { "message": "Delete" @@ -2846,7 +2846,7 @@ "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, "manageDefaultSettings": { - "message": "Manage default settings" + "message": "Manage default privacy settings" }, "marketCap": { "message": "Market cap" diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 926b152e899b..65a405f5325d 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -566,7 +566,10 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API on general section - await driver.clickElement({ text: 'Manage default settings', tag: 'button' }); + await driver.clickElement({ + text: 'Manage default privacy settings', + tag: 'button', + }); await driver.clickElement({ text: 'General', tag: 'p' }); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index af2ef47e93fb..6fc7025f5dbc 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -397,7 +397,7 @@ describe('MultiRpc:', function (this: Suite) { // go to advanced settigns await driver.clickElementAndWaitToDisappear({ - text: 'Manage default settings', + text: 'Manage default privacy settings', }); await driver.clickElement({ diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 1b15dba5ddd7..8d6b00de07ed 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -280,7 +280,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); @@ -402,7 +402,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index 062a0345a39a..b4fc0e138104 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -60,7 +60,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); @@ -130,7 +130,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); await driver.clickElement({ - text: 'Manage default settings', + text: 'Manage default privacy settings', tag: 'button', }); await driver.clickElement('[data-testid="category-item-General"]'); diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js index 5349a9f23f9e..9438f3859ff1 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js @@ -116,9 +116,9 @@ describe('Creation Successful Onboarding View', () => { ).toBeInTheDocument(); }); - it('should redirect to privacy-settings view when "Manage default settings" button is clicked', () => { + it('should redirect to privacy-settings view when "Manage default privacy settings" button is clicked', () => { const { getByText } = renderWithProvider(, store); - const privacySettingsButton = getByText('Manage default settings'); + const privacySettingsButton = getByText('Manage default privacy settings'); fireEvent.click(privacySettingsButton); expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PRIVACY_SETTINGS_ROUTE, From bd018b20e77266de009355a125956a3c7f0c3216 Mon Sep 17 00:00:00 2001 From: Harika <153644847+hjetpoluru@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:18:34 -0400 Subject: [PATCH 24/41] fix: "Update Network: should update added rpc url for exis..." flaky tests (#27437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses a flaky test issue caused by a popover dialog that appears. The presence of this popover dialog prevents the test from interacting with the intended element leading to test failures. To resolve this, I have included the clickSafeElement method to safely click the "Got it" button on the popover dialog. ![image](https://github.com/user-attachments/assets/5eb874d5-65bd-423a-aeab-6bf2cb628081) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27437?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27422 ## **Manual testing steps** Run the test using below commands locally or in codespaces: yarn yarn build:test:webpack ENABLE_MV3=false yarn test:e2e:single test/e2e/tests/network/update-network.spec.ts --browser=chrome ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- test/e2e/tests/network/update-network.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/e2e/tests/network/update-network.spec.ts b/test/e2e/tests/network/update-network.spec.ts index 08b1bc570c83..3f0b9882688f 100644 --- a/test/e2e/tests/network/update-network.spec.ts +++ b/test/e2e/tests/network/update-network.spec.ts @@ -240,7 +240,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network @@ -360,6 +366,13 @@ describe('Update Network:', function (this: Suite) { // Re-open the network menu await driver.delay(regularDelayMs); + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await driver.clickElementSafe({ text: 'Got it', tag: 'h6' }); + await driver.assertElementNotPresent({ + tag: 'h6', + text: 'Got it', + }); await driver.clickElement('[data-testid="network-display"]'); // Go back to edit the network From 988156b60bae62a2e2e81f04a3f8a8d5ad8c6f2d Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:33:03 +0200 Subject: [PATCH 25/41] feat: use messenger in AccountTracker to get Preferences state (#27711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates Account Tracker to retrieve the state from PreferencesController via the messenger, replacing the use of the state callback. All the unit tests were incorrectly passing before, but this issue has now been fixed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27711?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 1 + .../account-tracker-controller.test.ts | 111 +++++++++++------- .../controllers/account-tracker-controller.ts | 28 ++--- app/scripts/metamask-controller.js | 2 +- 4 files changed, 87 insertions(+), 55 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a53619b179ca..258556239ac3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -317,6 +317,7 @@ module.exports = { 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/controllers/preferences-controller.test.ts', + 'app/scripts/controllers/account-tracker-controller.test.ts', 'app/scripts/lib/**/*.test.js', 'app/scripts/metamask-controller.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts index ad33541fb5b6..7456244fc5a4 100644 --- a/app/scripts/controllers/account-tracker-controller.test.ts +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -32,7 +32,7 @@ const GAS_LIMIT_HOOK = '0x222222'; // The below three values were generated by running MetaMask in the browser // The response to eth_call, which is called via `ethContract.balances` -// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly +// in `#updateAccountsViaBalanceChecker` of account-tracker-controller.ts, needs to be properly // formatted or else ethers will throw an error. const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; @@ -85,9 +85,9 @@ type WithControllerArgs = | [WithControllerCallback] | [WithControllerOptions, WithControllerCallback]; -function withController( +async function withController( ...args: WithControllerArgs -): ReturnValue { +): Promise { const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; const { completedOnboarding = false, @@ -127,8 +127,8 @@ function withController( eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, }, - networkId: '0x1', - chainId: '0x1', + networkId: 'selectedNetworkId', + chainId: currentChainId, }); const getNetworkStateStub = jest.fn().mockReturnValue({ @@ -160,14 +160,19 @@ function withController( getOnboardingControllerState, ); + const getPreferencesControllerState = jest.fn().mockReturnValue({ + useMultiAccountBalanceChecker, + }); + controllerMessenger.registerActionHandler( + 'PreferencesController:getState', + getPreferencesControllerState, + ); + const controller = new AccountTrackerController({ state: getDefaultAccountTrackerControllerState(), provider: provider as Provider, blockTracker: blockTrackerStub as unknown as BlockTracker, getNetworkIdentifier: jest.fn(), - preferencesControllerState: { - useMultiAccountBalanceChecker, - }, messenger: controllerMessenger.getRestricted({ name: 'AccountTrackerController', allowedActions: [ @@ -175,6 +180,7 @@ function withController( 'NetworkController:getState', 'NetworkController:getNetworkClientById', 'OnboardingController:getState', + 'PreferencesController:getState', ], allowedEvents: [ 'AccountsController:selectedEvmAccountChange', @@ -185,7 +191,7 @@ function withController( ...accountTrackerOptions, }); - return fn({ + return await fn({ controller, blockTrackerFromHookStub, blockTrackerStub, @@ -198,7 +204,7 @@ function withController( describe('AccountTrackerController', () => { describe('start', () => { it('restarts the subscription to the block tracker and update accounts', async () => { - withController(({ controller, blockTrackerStub }) => { + await withController(({ controller, blockTrackerStub }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -238,7 +244,7 @@ describe('AccountTrackerController', () => { describe('stop', () => { it('ends the subscription to the block tracker', async () => { - withController(({ controller, blockTrackerStub }) => { + await withController(({ controller, blockTrackerStub }) => { controller.stop(); expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( @@ -252,7 +258,7 @@ describe('AccountTrackerController', () => { describe('startPollingByNetworkClientId', () => { it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -278,7 +284,7 @@ describe('AccountTrackerController', () => { const blockTrackerFromHookStub1 = buildMockBlockTracker(); const blockTrackerFromHookStub2 = buildMockBlockTracker(); const blockTrackerFromHookStub3 = buildMockBlockTracker(); - withController( + await withController( { getNetworkClientById: jest .fn() @@ -347,7 +353,7 @@ describe('AccountTrackerController', () => { describe('stopPollingByPollingToken', () => { it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); const pollingToken = @@ -363,7 +369,7 @@ describe('AccountTrackerController', () => { }); it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { - withController(({ controller, blockTrackerFromHookStub }) => { + await withController(({ controller, blockTrackerFromHookStub }) => { jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); const pollingToken1 = @@ -378,16 +384,16 @@ describe('AccountTrackerController', () => { }); }); - it('should error if no pollingToken is passed', () => { - withController(({ controller }) => { + it('should error if no pollingToken is passed', async () => { + await withController(({ controller }) => { expect(() => { controller.stopPollingByPollingToken(undefined); }).toThrow('pollingToken required'); }); }); - it('should error if no matching pollingToken is found', () => { - withController(({ controller }) => { + it('should error if no matching pollingToken is found', async () => { + await withController(({ controller }) => { expect(() => { controller.stopPollingByPollingToken('potato'); }).toThrow('pollingToken not found'); @@ -421,7 +427,7 @@ describe('AccountTrackerController', () => { throw new Error('unexpected networkClientId'); } }); - withController( + await withController( { getNetworkClientById: getNetworkClientByIdStub, }, @@ -456,7 +462,7 @@ describe('AccountTrackerController', () => { const blockTrackerStub = buildMockBlockTracker({ shouldStubListeners: false, }); - withController( + await withController( { blockTracker: blockTrackerStub as unknown as BlockTracker, }, @@ -499,14 +505,29 @@ describe('AccountTrackerController', () => { networkId: '0x1', chainId: '0x1', }).provider; - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - withController( + const getNetworkClientByIdStub = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }; + case 'selectedNetworkClientId': + return { + configuration: { + chainId: currentChainId, + }, + }; + default: + throw new Error('unexpected networkClientId'); + } + }); + await withController( { getNetworkClientById: getNetworkClientByIdStub, }, @@ -540,7 +561,7 @@ describe('AccountTrackerController', () => { describe('updateAccountsAllActiveNetworks', () => { it('updates accounts for the globally selected network and all currently polling networks', async () => { - withController(async ({ controller }) => { + await withController(async ({ controller }) => { const updateAccountsSpy = jest .spyOn(controller, 'updateAccounts') .mockResolvedValue(); @@ -572,7 +593,7 @@ describe('AccountTrackerController', () => { describe('updateAccounts', () => { it('does not update accounts if completedOnBoarding is false', async () => { - withController( + await withController( { completedOnboarding: false, }, @@ -606,11 +627,16 @@ describe('AccountTrackerController', () => { describe('when useMultiAccountBalanceChecker is true', () => { it('updates all accounts directly', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: true, state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), }, async ({ controller }) => { await controller.updateAccounts(); @@ -645,11 +671,16 @@ describe('AccountTrackerController', () => { describe('when useMultiAccountBalanceChecker is false', () => { it('updates only the selectedAddress directly, setting other balances to null', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: false, state: mockInitialState, + getNetworkClientById: jest.fn().mockReturnValue({ + configuration: { + chainId: '0x999', + }, + }), }, async ({ controller }) => { await controller.updateAccounts(); @@ -683,7 +714,7 @@ describe('AccountTrackerController', () => { describe('chain does have single call balance address and network is not localhost', () => { describe('when useMultiAccountBalanceChecker is true', () => { it('updates all accounts via balance checker', async () => { - withController( + await withController( { completedOnboarding: true, useMultiAccountBalanceChecker: true, @@ -697,7 +728,7 @@ describe('AccountTrackerController', () => { state: { accounts: { ...mockAccounts }, accountsByChainId: { - '0x1': { ...mockAccounts }, + [currentChainId]: { ...mockAccounts }, }, }, }, @@ -718,7 +749,7 @@ describe('AccountTrackerController', () => { expect(controller.state).toStrictEqual({ accounts, accountsByChainId: { - '0x1': accounts, + [currentChainId]: accounts, }, currentBlockGasLimit: '', currentBlockGasLimitByChainId: {}, @@ -731,8 +762,8 @@ describe('AccountTrackerController', () => { }); describe('onAccountRemoved', () => { - it('should remove an account from state', () => { - withController( + it('should remove an account from state', async () => { + await withController( { state: { accounts: { ...mockAccounts }, @@ -772,8 +803,8 @@ describe('AccountTrackerController', () => { }); describe('clearAccounts', () => { - it('should reset state', () => { - withController( + it('should reset state', async () => { + await withController( { state: { accounts: { ...mockAccounts }, diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts index ec4789189a0c..5f509a1901bf 100644 --- a/app/scripts/controllers/account-tracker-controller.ts +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -45,7 +45,7 @@ import type { OnboardingControllerGetStateAction, OnboardingControllerStateChangeEvent, } from './onboarding'; -import { PreferencesControllerState } from './preferences-controller'; +import { PreferencesControllerGetStateAction } from './preferences-controller'; // Unique name for the controller const controllerName = 'AccountTrackerController'; @@ -143,7 +143,8 @@ export type AllowedActions = | OnboardingControllerGetStateAction | AccountsControllerGetSelectedAccountAction | NetworkControllerGetStateAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | PreferencesControllerGetStateAction; /** * Events that this controller is allowed to subscribe. @@ -170,7 +171,6 @@ export type AccountTrackerControllerOptions = { provider: Provider; blockTracker: BlockTracker; getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; - preferencesControllerState: Partial; }; /** @@ -198,8 +198,6 @@ export default class AccountTrackerController extends BaseController< #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; - #preferencesControllerState: AccountTrackerControllerOptions['preferencesControllerState']; - #selectedAccount: InternalAccount; /** @@ -226,7 +224,6 @@ export default class AccountTrackerController extends BaseController< this.#blockTracker = options.blockTracker; this.#getNetworkIdentifier = options.getNetworkIdentifier; - this.#preferencesControllerState = options.preferencesControllerState; // subscribe to account removal this.messagingSystem.subscribe( @@ -256,8 +253,9 @@ export default class AccountTrackerController extends BaseController< this.messagingSystem.subscribe( 'AccountsController:selectedEvmAccountChange', (newAccount) => { - const { useMultiAccountBalanceChecker } = - this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); if ( this.#selectedAccount.id !== newAccount.id && @@ -433,10 +431,8 @@ export default class AccountTrackerController extends BaseController< return; } const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); - const updateForBlock = this.#updateForBlockByNetworkClientId.bind( - this, - networkClientId, - ); + const updateForBlock = (blockNumber: string) => + this.#updateForBlockByNetworkClientId(networkClientId, blockNumber); blockTracker.addListener('latest', updateForBlock); this.#listeners[networkClientId] = updateForBlock; @@ -672,7 +668,9 @@ export default class AccountTrackerController extends BaseController< const { chainId, provider, identifier } = this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); let addresses = []; if (useMultiAccountBalanceChecker) { @@ -723,7 +721,9 @@ export default class AccountTrackerController extends BaseController< provider: Provider, chainId: Hex, ): Promise { - const { useMultiAccountBalanceChecker } = this.#preferencesControllerState; + const { useMultiAccountBalanceChecker } = this.messagingSystem.call( + 'PreferencesController:getState', + ); let balance = '0x0'; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b19c91a232ab..5b3693960113 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1681,6 +1681,7 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:getState', 'NetworkController:getNetworkClientById', 'OnboardingController:getState', + 'PreferencesController:getState', ], allowedEvents: [ 'AccountsController:selectedEvmAccountChange', @@ -1698,7 +1699,6 @@ export default class MetamaskController extends EventEmitter { }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, - preferencesControllerState: this.preferencesController.state, }); // start and stop polling for balances based on activeControllerConnections From cd820efca8bf59505f4b4c7fb8ceabb9be7dbfaa Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:40:57 -0400 Subject: [PATCH 26/41] fix: phishing test to not check c2 domains (#27846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR modifies how phishing detection and blocklist checks are handled within the phishingController. Specifically, it ensures that only the isBlockedRequest function checks the blocklist against network requests. The test function will no longer handle for network requests but more specifically only be ran on `main_fame` and `sub_frame` requests. This allowed SEAL to submit C2 domains, however going forward they will need to submit C2 domains directly to us to be ingested. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27846?quickstart=1) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/background.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 7d9d0f5684a6..b6fe63b9aff1 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -266,13 +266,17 @@ function maybeDetectPhishing(theController) { } theController.phishingController.maybeUpdateState(); - const phishingTestResponse = theController.phishingController.test( - details.url, - ); const blockedRequestResponse = theController.phishingController.isBlockedRequest(details.url); + let phishingTestResponse; + if (details.type === 'main_frame' || details.type === 'sub_frame') { + phishingTestResponse = theController.phishingController.test( + details.url, + ); + } + // if the request is not blocked, and the phishing test is not blocked, return and don't show the phishing screen if (!phishingTestResponse?.result && !blockedRequestResponse.result) { return {}; From cc47ff95a7fbac72a3788f213897680318932209 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 15 Oct 2024 23:25:12 +0530 Subject: [PATCH 27/41] fix: nonce value when there are multiple transactions in parallel (#27874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix issue with nonce not updating when there are multiple transaction created in parallel and once transaction is submitted. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27617 ## **Manual testing steps** 1. Go to testdapp 2. create 2 transactions and submit first one 3. Nonce for second transaction should update ## **Screenshots/Recordings** TODO ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../confirm-transaction-base.component.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index 96fd5315e317..b4d2d6a8def5 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -215,6 +215,8 @@ export default class ConfirmTransactionBase extends Component { useMaxValue, hasPriorityApprovalRequest, mostRecentOverviewPage, + txData, + getNextNonce, } = this.props; const { @@ -225,6 +227,7 @@ export default class ConfirmTransactionBase extends Component { isEthGasPriceFetched: prevIsEthGasPriceFetched, hexMaximumTransactionFee: prevHexMaximumTransactionFee, hasPriorityApprovalRequest: prevHasPriorityApprovalRequest, + txData: prevTxData, } = prevProps; const statusUpdated = transactionStatus !== prevTxStatus; @@ -232,6 +235,10 @@ export default class ConfirmTransactionBase extends Component { transactionStatus === TransactionStatus.dropped || transactionStatus === TransactionStatus.confirmed; + if (txData.id !== prevTxData.id) { + getNextNonce(); + } + if ( nextNonce !== prevNextNonce || customNonceValue !== prevCustomNonceValue From dc0dc67dd2ef36692c2329ef0109c8d6a9cb32a7 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Tue, 15 Oct 2024 20:21:15 +0200 Subject: [PATCH 28/41] feat: upgrade assets-controllers to v38.3.0 (#27755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to upgrade assets-controllers to v38.3.0; [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27755?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. Co-authored-by: Howard Braham --- ...ts-controllers-npm-38.3.0-57b3d695bb.patch} | 0 package.json | 2 +- yarn.lock | 18 +++++++++--------- 3 files changed, 10 insertions(+), 10 deletions(-) rename .yarn/patches/{@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch => @metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch} (100%) diff --git a/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch b/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch rename to .yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch diff --git a/package.json b/package.json index 4973fa0da559..76dfb15c1ba7 100644 --- a/package.json +++ b/package.json @@ -301,7 +301,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", diff --git a/yarn.lock b/yarn.lock index 733c94112452..94ecd006df3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,9 +4861,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:38.2.0": - version: 38.2.0 - resolution: "@metamask/assets-controllers@npm:38.2.0" +"@metamask/assets-controllers@npm:38.3.0": + version: 38.3.0 + resolution: "@metamask/assets-controllers@npm:38.3.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4895,13 +4895,13 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/96ae724a002289e4df97bab568e0bba4d28ef18320298b12d828fc3b58c58ebc54b9f9d659c5e6402aad82088b699e52469d897dd4356e827e35b8f8cebb4483 + checksum: 10/b6e69c9925c50f351b9de1e31cc5d9a4c0ab7cf1abf116c0669611ecb58b3890dd0de53d36bcaaea4f8c45d6ddc2c53eef80c42f93f8f303f1ee9d8df088872b languageName: node linkType: hard -"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch": - version: 38.2.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch::version=38.2.0&hash=e14ff8" +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch": + version: 38.3.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch::version=38.3.0&hash=e14ff8" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4933,7 +4933,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/0ba3673bf9c87988d6c569a14512b8c9bb97db3516debfedf24cbcf38110e99afec8d9fc50cb0b627bfbc1d1a62069298e4e27278587197f67812cb38ee2c778 + checksum: 10/1f57289a3a2a88f1f16e00a138b30b9a8e4ac894086732a463e6b47d5e984e0a7e05ef2ec345f0e1cd69857669253260d53d4c37b2b3d9b970999602fc01a21c languageName: node linkType: hard @@ -26126,7 +26126,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" From 82e5a457df241e34b87c0bbd9d12286e3699525f Mon Sep 17 00:00:00 2001 From: Derek Brans Date: Tue, 15 Oct 2024 15:22:27 -0400 Subject: [PATCH 29/41] test(TXL-308): initial e2e for stx using swaps (#27215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** * Initial e2e test for stx [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27215?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- privacy-snapshot.json | 1 + test/e2e/default-fixture.js | 2 + test/e2e/fixture-builder.js | 8 + test/e2e/mock-e2e.js | 58 ++- ...rs-after-init-opt-in-background-state.json | 2 + .../errors-after-init-opt-in-ui-state.json | 2 + ...s-before-init-opt-in-background-state.json | 19 + .../errors-before-init-opt-in-ui-state.json | 19 + .../mock-requests-for-swap-test.ts | 346 ++++++++++++++++++ .../smart-transactions.spec.ts | 97 +++++ test/e2e/tests/swaps/{shared.js => shared.ts} | 98 +++-- .../{swap-eth.spec.js => swap-eth.spec.ts} | 28 +- ...ns.spec.js => swaps-notifications.spec.ts} | 23 +- test/e2e/webdriver/driver.js | 6 +- .../smart-transaction-status.js | 2 + 15 files changed, 617 insertions(+), 94 deletions(-) create mode 100644 test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts create mode 100644 test/e2e/tests/smart-transactions/smart-transactions.spec.ts rename test/e2e/tests/swaps/{shared.js => shared.ts} (71%) rename test/e2e/tests/swaps/{swap-eth.spec.js => swap-eth.spec.ts} (80%) rename test/e2e/tests/swaps/{swaps-notifications.spec.js => swaps-notifications.spec.ts} (92%) diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 2516654f1803..37a05025382d 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -56,6 +56,7 @@ "test.metamask-phishing.io", "token.api.cx.metamask.io", "tokens.api.cx.metamask.io", + "transaction.api.cx.metamask.io", "tx-sentinel-ethereum-mainnet.api.cx.metamask.io", "unresponsive-rpc.test", "unresponsive-rpc.url", diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 5d3883a5e8f5..95f35bf1694c 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -268,7 +268,9 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { SmartTransactionsController: { smartTransactionsState: { fees: {}, + feesByChainId: {}, liveness: true, + livenessByChainId: {}, smartTransactions: { [CHAIN_IDS.MAINNET]: [], }, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 4c802e13bfa0..d73b959946c2 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -654,6 +654,14 @@ class FixtureBuilder { }); } + withPreferencesControllerSmartTransactionsOptedIn() { + return this.withPreferencesController({ + preferences: { + smartTransactionsOptInStatus: true, + }, + }); + } + withPreferencesControllerAndFeatureFlag(flags) { merge(this.fixture.data.PreferencesController, flags); return this; diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index abc536ef6059..209777f32bd7 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -272,35 +272,33 @@ async function setupMocking( .thenCallback(() => { return { statusCode: 200, - json: [ - { - ethereum: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - bsc: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - polygon: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - avalanche: { - fallbackToV1: false, - mobileActive: true, - extensionActive: true, - }, - smartTransactions: { - mobileActive: false, - extensionActive: false, - }, - updated_at: '2022-03-17T15:54:00.360Z', + json: { + ethereum: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, }, - ], + bsc: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + polygon: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + avalanche: { + fallbackToV1: false, + mobileActive: true, + extensionActive: true, + }, + smartTransactions: { + mobileActive: false, + extensionActive: true, + }, + updated_at: '2022-03-17T15:54:00.360Z', + }, }; }); @@ -470,7 +468,7 @@ async function setupMocking( decimals: 18, name: 'Dai Stablecoin', iconUrl: - 'https://crypto.com/price/coin-data/icon/DAI/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png', type: 'erc20', aggregators: [ 'aave', @@ -497,7 +495,7 @@ async function setupMocking( decimals: 6, name: 'USD Coin', iconUrl: - 'https://crypto.com/price/coin-data/icon/USDC/color_icon.png', + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png', type: 'erc20', aggregators: [ 'aave', diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 4658c175bfd5..78988fc87cc8 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -243,7 +243,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 924769a3cb91..f40d36f85aad 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -173,7 +173,9 @@ "allDetectedTokens": {}, "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" }, "allNftContracts": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index e2cb7369d88a..89b1b29100bb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -138,7 +155,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 34cc62d3c560..f13d3e078c64 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -50,6 +50,23 @@ }, "snapsInstallPrivacyWarningShown": true }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CurrencyController": { "currentCurrency": "usd", "currencyRates": { @@ -138,7 +155,9 @@ "SmartTransactionsController": { "smartTransactionsState": { "fees": {}, + "feesByChainId": "object", "liveness": true, + "livenessByChainId": "object", "smartTransactions": "object" } }, diff --git a/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts new file mode 100644 index 000000000000..457d1ea6c0a1 --- /dev/null +++ b/test/e2e/tests/smart-transactions/mock-requests-for-swap-test.ts @@ -0,0 +1,346 @@ +import { MockttpServer } from 'mockttp'; +import { mockEthDaiTrade } from '../swaps/shared'; + +const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; + +const GET_FEES_REQUEST_INCLUDES = { + txs: [ + { + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + value: '0x1bc16d674ec80000', + gas: '0xf4240', + nonce: '0x0', + }, + ], +}; + +const GET_FEES_RESPONSE = { + blockNumber: 20728974, + id: '19d4eea3-8a49-463e-9e9c-099f9d9571ca', + txs: [ + { + cancelFees: [], + return: '0x', + status: 1, + gasUsed: 190780, + gasLimit: 239420, + fees: [ + { + maxFeePerGas: 4667609171, + maxPriorityFeePerGas: 1000000004, + gas: 239420, + balanceNeeded: 1217518987960240, + currentBalance: 751982303082919400, + error: '', + }, + ], + feeEstimate: 627603309182220, + baseFeePerGas: 2289670348, + maxFeeEstimate: 1117518987720820, + }, + ], +}; + +const SUBMIT_TRANSACTIONS_REQUEST_EXACTLY = { + rawTxs: [ + '0x02f91a3b0180843b9aca048501163610538303a73c94881d40237659c251811cec9c364ef91dc08d300c881bc16d674ec80000b919c65f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136b796265725377617046656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000018e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000003e2c284391c000000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017a4e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000132000000000000000000000000000000000000000000000000000000000000015200000000000000000000000000000000000000000000000000000000000001260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000000000066e029cc00000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008200000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000e80000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000400e00deaa000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000c697051d1c6296c24ae3bcef39aca743861d9a81000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae900000000000000000000000000000000000000000000000006e0d04fc2cd90000000000000000000000000000000000000000000000000000000000000000040003c5f890000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ead050515e10fdb3540ccd6f8236c46790508a7600000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000000000000000000e00000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000000000000000000000000000c4000000000000000000000000000000000000000000000000000000000000000000000000000003a4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000028cacd5e26a719f139e2105ca1efc3d9dc892826000000000000000000000000ff8ba4d1fc3762f6154cc942ccf30049a2a0cec6000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000074b3f935bb79d45400000000000000000000000000000000000000000000000074b3f935bb79d4540000000000000000000000000000000000000000000000000000000045aff3b30000000000000000000000000000000000000000000000000000000066e025760000000000000000000000000000000000000000000000000016649acb241b017da53b79cbf14cc2a737cd6469098549000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae80464000000000000000000000000ae4fdcc420f1409c8b9b2af04db150dd986f66a5000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000444b27250000000000000000000000000000000000000000000000000000000000000041031bc9026b766621ebb870691407a8f5b5d222977566d0bb38bbd633459fc9671e24b5c970373555d66f0a46e830ee1605152bd519fed1a9684a097364f8b41f1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413e4923699eff11cb0252c3f8b42793eeac8793bea92843fa4028b80ff3391bbf1df4ddef51732ceeb6f65a8c9dc2651e4b952568d350b4029d4b8b5cae5c1f991c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000045921fcd000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005354532a0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000840f9f95029e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040301a40330000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000005b6a0771c752e35b2ca2aff4f22a66b1598a2bc50000000000000000000000000000000000000000000000000000000053516d7f000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c879c58f70905f734641735bc61e45c19dd9ad60bc0000000000000000000004e7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000005351dd8d000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004063407a490000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd6700000000000000000000000011b815efb8f581194ae79006d24e0d814b7697f6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000004207cfca814f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000408cc7a56b0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c806df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000029a7a0aa0000000000000000000000000000000000000000000000000000000000000020000000000000000000108fd5cc11eaa000000000000000fcb6c0091c62637b42000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000001b83413f0b3640000000000000000000000000000000000000000000000000f7a8daea356aa92bfe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002337b22536f75726365223a226d6574616d61736b222c22416d6f756e74496e555344223a22343635382e373832393833313733303632222c22416d6f756e744f7574555344223a22343637362e393836303733303034303236222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2234363631373438303431393032373532373538353934222c2254696d657374616d70223a313732353936353539362c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a22546363534e7837537235376b367242794f5a74344b714472344d544637356b7651527658644230724266386e395864513869634a3830385963355155595a34675a52527645337777433237352f59586a722f34625065662b4a58514b4969556b6334356a4e73556c366e6141387141774d5a48324f4a3234657932647253386c52625551444f67784b4d6979334d413164467472575241306f6d6e664873365044624b6d6f4e494c58674b45416e497a6b6d687a675043346e396d39715043337a457459737875457042772b386356426b684e7761684f56625850635854646977334870437356365555635375522f4a495342386d6a737a494b6d664b46595a716333516c5a714e6e507a50576a3648366e73587050512b6145725338334c3544554b5868364e6a70584855764748314d7a557074584169615634737354795849582f435645685a396e76564845746b2f776b6a42673d3d227d7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000ac001a0e067d3acdb151721e7fdb3834cf2563d667aad9b4c18a5afb81390d6288ac2fe9fa0e74a9e40e017bcd926f3c5da4355e0926ae7e45b3b9e1bc474507220cb43', + ], + rawCancelTxs: [], +}; + +const GET_BATCH_STATUS_RESPONSE_PENDING = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: false, + minedTx: 'not_mined', + wouldRevertMessage: null, + minedHash: '', + duplicated: false, + timedOut: false, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_BATCH_STATUS_RESPONSE_SUCCESS = { + '0d506aaa-5e38-4cab-ad09-2039cb7a0f33': { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: true, + minedTx: 'success', + wouldRevertMessage: null, + minedHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + duplicated: true, + timedOut: true, + proxied: false, + type: 'sentinel', + }, +}; + +const GET_TRANSACTION_RECEIPT_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + contractAddress: null, + cumulativeGasUsed: '0xc138b1', + effectiveGasPrice: '0x1053fcd93', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gasUsed: '0x2e93c', + logs: [ + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005af3107a4000', + logIndex: '0xde', + removed: false, + topics: [ + '0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xdf', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe0', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x00000000000000000000000000000000000000000000000000006a3845cef618', + logIndex: '0xe1', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x000000000000000000000000ad30f7eebd9bd5150a256f47da41d4403033cdf0', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xd82fa167727a4dc6d6f55830a2c47abbb4b3a0f8', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a3000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000005a275669d200', + logIndex: '0xe2', + removed: false, + topics: [ + '0xb651f2787ff61b5ab14f3936f2daebdad3d84aeb74438e82870cc3b7aee71e90', + '0x00000000000000000000000000000000000000000000000000000191e0cc96ac', + '0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe3', + removed: false, + topics: [ + '0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0xf326e4de8f66a0bdc0970b79e0924e33c79f1915', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000000000cbba106e00', + logIndex: '0xe4', + removed: false, + topics: [ + '0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x000000000000000000000000000000000000000000000000033dd7a160e2a300', + logIndex: '0xe5', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + { + address: '0x881d40237659c251811cec9c364ef91dc08d300c', + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x13c80ce', + data: '0x', + logIndex: '0xe6', + removed: false, + topics: [ + '0xbeee1e6e7fe307ddcf84b0a16137a4430ad5e2480fc4f4a8e250ab56ccd7630d', + '0x015123c6e2552626efe611b6c48de60d080a6650860a38f237bc2b6f651f79d1', + '0x0000000000000000000000005cfe73b6021e818b776b421b1c4db2474086a7e1', + ], + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + }, + ], + logsBloom: + '0x00000000000000001000000000000000000000000000000000000001000000000000010000000000000010000000000002000000080008000000040000000000a00000000000000000020008000000000000000000540000000004008020000010000000000000000000000000000801000000000000040000000010004010000000021000000000000000000000000000020041000100004020000000000000000000000200000000000040000000000000000000000000000000000000000000000002000400000000000000000000001002000400000000000002000000000020200000000400000000800000000000000000020200400000000000001000', + status: '0x1', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionHash: + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + transactionIndex: '0x2f', + type: '0x2', + }, +}; + +const GET_TRANSACTION_BY_HASH_RESPONSE = { + id: 2901696354742565, + jsonrpc: '2.0', + result: { + accessList: [], + blockHash: + '0xe90b92d004a9c22c32c50c628bbd93f22e3468ec4ffc62422d68cf6370f59f1d', + blockNumber: '0x2', + chainId: '0x539', + from: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', + gas: '0x3a73c', + gasPrice: '0x1053fcd93', + hash: '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + input: + '0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001c616972737761704c696768743446656544796e616d696346697865640000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000191e0cc96ac0000000000000000000000000000000000000000000000000000000066e44f2c00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000033dd7a160e2a300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005a275669d200000000000000000000000000000000000000000000000000000000000000001bc1acb8a206598705baeb494a479a8af9dc3a9f9b7bd1ce9818360fd6f603cf0766e7bdc77f9f72e90dcd9157e007291adc6d3947e9b6d89ff412c5b54f9a17f1000000000000000000000000000000000000000000000000000000cbba106e00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000d7', + maxFeePerGas: '0x14bdcd619', + maxPriorityFeePerGas: '0x3b9aca04', + nonce: '0x127', + r: '0x5a5463bfe8e587ee1211be74580c74fa759f8292f37f970033df4b782f5e097d', + s: '0x50e403a70000b106e9f598b1b3f55b6ea9d2ec21d9fc67de63eb1d07df2767dd', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + transactionIndex: '0x2f', + type: '0x2', + v: '0x0', + value: '0x5af3107a4000', + yParity: '0x0', + }, +}; + +export const mockSwapRequests = async (mockServer: MockttpServer) => { + await mockEthDaiTrade(mockServer); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getBalance', + params: ['0x5cfe73b6021e818b776b421b1c4db2474086a7e1'], + }) + .thenJson(200, { + id: 3806592044086814, + jsonrpc: '2.0', + result: '0x1bc16d674ec80000', // 2 ETH + }); + + await mockServer + .forPost('https://transaction.api.cx.metamask.io/networks/1/getFees') + .withJsonBodyIncluding(GET_FEES_REQUEST_INCLUDES) + .thenJson(200, GET_FEES_RESPONSE); + + await mockServer + .forPost( + 'https://transaction.api.cx.metamask.io/networks/1/submitTransactions', + ) + .once() + .withJsonBody(SUBMIT_TRANSACTIONS_REQUEST_EXACTLY) + .thenJson(200, { uuid: STX_UUID }); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_PENDING); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, GET_BATCH_STATUS_RESPONSE_SUCCESS); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionReceipt', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_RECEIPT_RESPONSE); + + await mockServer + .forJsonRpcRequest({ + method: 'eth_getTransactionByHash', + params: [ + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5', + ], + }) + .thenJson(200, GET_TRANSACTION_BY_HASH_RESPONSE); +}; diff --git a/test/e2e/tests/smart-transactions/smart-transactions.spec.ts b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts new file mode 100644 index 000000000000..210d5abdb034 --- /dev/null +++ b/test/e2e/tests/smart-transactions/smart-transactions.spec.ts @@ -0,0 +1,97 @@ +import { MockttpServer } from 'mockttp'; +import { + buildQuote, + reviewQuote, + checkActivityTransaction, +} from '../swaps/shared'; +import FixtureBuilder from '../../fixture-builder'; +import { unlockWallet, withFixtures } from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { mockSwapRequests } from './mock-requests-for-swap-test'; + +export async function withFixturesForSmartTransactions( + { + title, + testSpecificMock, + }: { + title?: string; + testSpecificMock: (mockServer: MockttpServer) => Promise; + }, + test: (args: { driver: Driver }) => Promise, +) { + const inputChainId = CHAIN_IDS.MAINNET; + await withFixtures( + { + fixtures: new FixtureBuilder({ inputChainId }) + .withPermissionControllerConnectedToTestDapp() + .withPreferencesControllerSmartTransactionsOptedIn() + .withNetworkControllerOnMainnet() + .build(), + title, + testSpecificMock, + dapp: true, + }, + async ({ driver }) => { + await unlockWallet(driver); + await test({ driver }); + }, + ); +} + +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { + await driver.waitForSelector({ + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Privately submitting your Swap', + }); + + await driver.waitForSelector( + { + css: '[data-testid="swap-smart-transaction-status-header"]', + text: 'Swap complete!', + }, + { timeout: 30000 }, + ); + + await driver.findElement({ + css: '[data-testid="swap-smart-transaction-status-description"]', + text: `${options.tokenName}`, + }); + + await driver.clickElement({ text: 'Close', tag: 'button' }); + await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); +}; + +describe('smart transactions @no-mmi', function () { + it('Completes a Swap', async function () { + await withFixturesForSmartTransactions( + { + title: this.test?.fullTitle(), + testSpecificMock: mockSwapRequests, + }, + async ({ driver }) => { + await buildQuote(driver, { + amount: 2, + swapTo: 'DAI', + }); + await reviewQuote(driver, { + amount: 2, + swapFrom: 'ETH', + swapTo: 'DAI', + }); + + await driver.clickElement({ text: 'Swap', tag: 'button' }); + await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); + await checkActivityTransaction(driver, { + index: 0, + amount: '2', + swapFrom: 'ETH', + swapTo: 'DAI', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/swaps/shared.js b/test/e2e/tests/swaps/shared.ts similarity index 71% rename from test/e2e/tests/swaps/shared.js rename to test/e2e/tests/swaps/shared.ts index 3bfdefcf71d7..3f3aff4447e5 100644 --- a/test/e2e/tests/swaps/shared.js +++ b/test/e2e/tests/swaps/shared.ts @@ -1,27 +1,54 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { regularDelayMs, veryLargeDelayMs } = require('../../helpers'); - -const ganacheOptions = { - accounts: [ - { - secretKey: - '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', - balance: 25000000000000000000, +import { strict as assert } from 'assert'; +import { ServerOptions } from 'ganache'; +import { MockttpServer } from 'mockttp'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { regularDelayMs, veryLargeDelayMs } from '../../helpers'; +import { SWAP_TEST_ETH_DAI_TRADES_MOCK } from '../../../data/mock-data'; + +export async function mockEthDaiTrade(mockServer: MockttpServer) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => { + return { + statusCode: 200, + json: SWAP_TEST_ETH_DAI_TRADES_MOCK, + }; + }), + ]; +} + +export const ganacheOptions: ServerOptions & { miner: { blockTime?: number } } = + { + wallet: { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000n, + }, + ], }, - ], -}; + miner: {}, + }; -const withFixturesOptions = { +export const withFixturesOptions = { fixtures: new FixtureBuilder().build(), ganacheOptions, }; -const buildQuote = async (driver, options) => { +type SwapOptions = { + amount: number; + swapTo?: string; + swapToContractAddress?: string; +}; + +export const buildQuote = async (driver: Driver, options: SwapOptions) => { await driver.clickElement('[data-testid="token-overview-button-swap"]'); await driver.fill( 'input[data-testid="prepare-swap-page-from-token-amount"]', - options.amount, + options.amount.toString(), ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. await driver.clickElement('[data-testid="prepare-swap-page-swap-to"]'); @@ -29,7 +56,7 @@ const buildQuote = async (driver, options) => { await driver.fill( 'input[id="list-with-search__text-search"]', - options.swapTo || options.swapToContractAddress, + options.swapTo || options.swapToContractAddress || '', ); await driver.delay(veryLargeDelayMs); // Need an extra delay after typing an amount. @@ -55,7 +82,15 @@ const buildQuote = async (driver, options) => { ); }; -const reviewQuote = async (driver, options) => { +export const reviewQuote = async ( + driver: Driver, + options: { + swapFrom: string; + swapTo: string; + amount: number; + skipCounter?: boolean; + }, +) => { const summary = await driver.waitForSelector( '[data-testid="exchange-rate-display-quote-rate"]', ); @@ -68,7 +103,7 @@ const reviewQuote = async (driver, options) => { '[data-testid="prepare-swap-page-receive-amount"]', ); const swapToAmount = await elementSwapToAmount.getText(); - const expectedAmount = parseFloat(quote[3]) * options.amount; + const expectedAmount = Number(quote[3]) * options.amount; const dotIndex = swapToAmount.indexOf('.'); const decimals = dotIndex === -1 ? 0 : swapToAmount.length - dotIndex - 1; assert.equal( @@ -91,7 +126,10 @@ const reviewQuote = async (driver, options) => { } }; -const waitForTransactionToComplete = async (driver, options) => { +export const waitForTransactionToComplete = async ( + driver: Driver, + options: { tokenName: string }, +) => { await driver.waitForSelector({ css: '[data-testid="awaiting-swap-header"]', text: 'Processing', @@ -114,7 +152,10 @@ const waitForTransactionToComplete = async (driver, options) => { await driver.waitForSelector('[data-testid="account-overview__asset-tab"]'); }; -const checkActivityTransaction = async (driver, options) => { +export const checkActivityTransaction = async ( + driver: Driver, + options: { index: number; swapFrom: string; swapTo: string; amount: string }, +) => { await driver.clickElement('[data-testid="account-overview__activity-tab"]'); await driver.waitForSelector('.activity-list-item'); @@ -149,7 +190,10 @@ const checkActivityTransaction = async (driver, options) => { await driver.clickElement('[data-testid="popover-close"]'); }; -const checkNotification = async (driver, options) => { +export const checkNotification = async ( + driver: Driver, + options: { title: string; text: string }, +) => { const isExpectedBoxTitlePresentAndVisible = await driver.isElementPresentAndVisible({ css: '[data-testid="swaps-banner-title"]', @@ -171,7 +215,7 @@ const checkNotification = async (driver, options) => { ); }; -const changeExchangeRate = async (driver) => { +export const changeExchangeRate = async (driver: Driver) => { await driver.clickElement('[data-testid="review-quote-view-all-quotes"]'); await driver.waitForSelector({ text: 'Quote details', tag: 'h2' }); @@ -182,13 +226,3 @@ const changeExchangeRate = async (driver) => { await networkFees[random].click(); await driver.clickElement({ text: 'Select', tag: 'button' }); }; - -module.exports = { - withFixturesOptions, - buildQuote, - reviewQuote, - waitForTransactionToComplete, - checkActivityTransaction, - checkNotification, - changeExchangeRate, -}; diff --git a/test/e2e/tests/swaps/swap-eth.spec.js b/test/e2e/tests/swaps/swap-eth.spec.ts similarity index 80% rename from test/e2e/tests/swaps/swap-eth.spec.js rename to test/e2e/tests/swaps/swap-eth.spec.ts index 35847b0ae33a..18d049e5de16 100644 --- a/test/e2e/tests/swaps/swap-eth.spec.js +++ b/test/e2e/tests/swaps/swap-eth.spec.ts @@ -1,35 +1,22 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_DAI_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { unlockWallet, withFixtures } from '../../helpers'; +import { withFixturesOptions, buildQuote, reviewQuote, waitForTransactionToComplete, checkActivityTransaction, changeExchangeRate, -} = require('./shared'); - -async function mockEthDaiTrade(mockServer) { - return [ - await mockServer - .forGet('https://swap.api.cx.metamask.io/networks/1/trades') - .thenCallback(() => { - return { - statusCode: 200, - json: SWAP_TEST_ETH_DAI_TRADES_MOCK, - }; - }), - ]; -} + mockEthDaiTrade, +} from './shared'; describe('Swap Eth for another Token @no-mmi', function () { it('Completes second Swaps while first swap is processing', async function () { - withFixturesOptions.ganacheOptions.blockTime = 10; + withFixturesOptions.ganacheOptions.miner.blockTime = 10; await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -70,12 +57,13 @@ describe('Swap Eth for another Token @no-mmi', function () { }, ); }); + it('Completes a Swap between ETH and DAI after changing initial rate', async function () { await withFixtures( { ...withFixturesOptions, testSpecificMock: mockEthDaiTrade, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); diff --git a/test/e2e/tests/swaps/swaps-notifications.spec.js b/test/e2e/tests/swaps/swaps-notifications.spec.ts similarity index 92% rename from test/e2e/tests/swaps/swaps-notifications.spec.js rename to test/e2e/tests/swaps/swaps-notifications.spec.ts index c6dbbd469959..134741d3683c 100644 --- a/test/e2e/tests/swaps/swaps-notifications.spec.js +++ b/test/e2e/tests/swaps/swaps-notifications.spec.ts @@ -1,13 +1,14 @@ -const { withFixtures, unlockWallet } = require('../../helpers'); -const { SWAP_TEST_ETH_USDC_TRADES_MOCK } = require('../../../data/mock-data'); -const { +import { Mockttp } from 'mockttp'; +import { withFixtures, unlockWallet } from '../../helpers'; +import { SWAP_TEST_ETH_USDC_TRADES_MOCK } from '../../../data/mock-data'; +import { withFixturesOptions, buildQuote, reviewQuote, checkNotification, -} = require('./shared'); +} from './shared'; -async function mockSwapsTransactionQuote(mockServer) { +async function mockSwapsTransactionQuote(mockServer: Mockttp) { return [ await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') @@ -19,7 +20,7 @@ async function mockSwapsTransactionQuote(mockServer) { } describe('Swaps - notifications @no-mmi', function () { - async function mockTradesApiPriceSlippageError(mockServer) { + async function mockTradesApiPriceSlippageError(mockServer: Mockttp) { await mockServer .forGet('https://swap.api.cx.metamask.io/networks/1/trades') .thenCallback(() => { @@ -71,7 +72,7 @@ describe('Swaps - notifications @no-mmi', function () { { ...withFixturesOptions, testSpecificMock: mockTradesApiPriceSlippageError, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -122,7 +123,7 @@ describe('Swaps - notifications @no-mmi', function () { ...withFixturesOptions, ganacheOptions: lowBalanceGanacheOptions, testSpecificMock: mockSwapsTransactionQuote, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -152,7 +153,7 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); @@ -174,12 +175,12 @@ describe('Swaps - notifications @no-mmi', function () { await withFixtures( { ...withFixturesOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); await buildQuote(driver, { - amount: '.0001', + amount: 0.0001, swapTo: 'DAI', }); await driver.clickElement('[title="Transaction settings"]'); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index fb8aed3d28a6..b0648f122fb9 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -319,10 +319,14 @@ class Driver { * Waits for an element that matches the given locator to reach the specified state within the timeout period. * * @param {string | object} rawLocator - Element locator - * @param {number} timeout - optional parameter that specifies the maximum amount of time (in milliseconds) + * @param {object} [options] - parameter object + * @param {number} [options.timeout] - specifies the maximum amount of time (in milliseconds) * to wait for the condition to be met and desired state of the element to wait for. * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. * The other supported state is 'detached', which means waiting until the element is removed from the DOM. + * @param {string} [options.state] - specifies the state of the element to wait for. + * It defaults to 'visible', indicating that the method will wait until the element is visible on the page. + * The other supported state is 'detached', which means waiting until the element is removed from the DOM. * @returns {Promise} promise resolving when the element meets the state or timeout occurs. * @throws {Error} Will throw an error if the element does not reach the specified state within the timeout period. */ diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index b103ead2097c..e6a77f9474fb 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -401,6 +401,7 @@ export default function SmartTransactionStatusPage() { )} Date: Tue, 15 Oct 2024 15:01:05 -0500 Subject: [PATCH 30/41] chore: remove unused swaps code (#27679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removed code which is no longer needed because we are moving forward with the swaps redesign. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27679?quickstart=1) ## **Manual testing steps** Test Swaps flows and confirm no regresions. ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/de/messages.json | 65 - app/_locales/el/messages.json | 65 - app/_locales/en/messages.json | 65 - app/_locales/en_GB/messages.json | 65 - app/_locales/es/messages.json | 65 - app/_locales/es_419/messages.json | 62 - app/_locales/fr/messages.json | 65 - app/_locales/hi/messages.json | 65 - app/_locales/id/messages.json | 65 - app/_locales/it/messages.json | 46 - app/_locales/ja/messages.json | 65 - app/_locales/ko/messages.json | 65 - app/_locales/ph/messages.json | 62 - app/_locales/pt/messages.json | 65 - app/_locales/pt_BR/messages.json | 62 - app/_locales/ru/messages.json | 65 - app/_locales/tl/messages.json | 65 - app/_locales/tr/messages.json | 65 - app/_locales/vi/messages.json | 65 - app/_locales/zh_CN/messages.json | 65 - app/_locales/zh_TW/messages.json | 7 - .../files-to-convert.json | 27 - .../data/integration-init-state.json | 4 - test/jest/mock-store.js | 4 - .../app/wallet-overview/coin-buttons.tsx | 6 +- .../multichain/app-header/app-header.js | 7 +- ui/ducks/swaps/swaps.js | 15 +- ui/ducks/swaps/swaps.test.js | 19 - ui/helpers/constants/routes.ts | 6 - ui/pages/asset/components/token-buttons.tsx | 6 +- ui/pages/bridge/index.test.tsx | 2 - ui/pages/home/home.component.js | 9 +- ui/pages/routes/routes.component.js | 8 - .../swaps/__snapshots__/index.test.js.snap | 20 +- .../awaiting-signatures.js | 4 +- ui/pages/swaps/awaiting-swap/awaiting-swap.js | 8 +- .../__snapshots__/build-quote.test.js.snap | 33 - ui/pages/swaps/build-quote/build-quote.js | 800 ------------ .../swaps/build-quote/build-quote.stories.js | 35 - .../swaps/build-quote/build-quote.test.js | 223 ---- ui/pages/swaps/build-quote/index.js | 1 - ui/pages/swaps/build-quote/index.scss | 223 ---- .../swaps/create-new-swap/create-new-swap.js | 4 +- .../create-new-swap/create-new-swap.test.js | 12 +- ui/pages/swaps/dropdown-input-pair/README.mdx | 15 - .../dropdown-input-pair.test.js.snap | 20 - .../dropdown-input-pair.js | 177 --- .../dropdown-input-pair.stories.js | 173 --- .../dropdown-input-pair.test.js | 44 - ui/pages/swaps/dropdown-input-pair/index.js | 1 - ui/pages/swaps/dropdown-input-pair/index.scss | 78 -- .../dropdown-search-list.test.js.snap | 46 - .../dropdown-search-list.js | 334 ----- .../dropdown-search-list.stories.js | 147 --- .../dropdown-search-list.test.js | 50 - ui/pages/swaps/dropdown-search-list/index.js | 1 - .../swaps/dropdown-search-list/index.scss | 167 --- ui/pages/swaps/index.js | 220 +--- ui/pages/swaps/index.scss | 35 +- ui/pages/swaps/index.test.js | 4 +- .../loading-swaps-quotes.js | 4 +- ui/pages/swaps/main-quote-summary/README.mdx | 14 - .../main-quote-summary.test.js.snap | 116 -- .../__snapshots__/quote-backdrop.test.js.snap | 74 -- ui/pages/swaps/main-quote-summary/index.js | 1 - ui/pages/swaps/main-quote-summary/index.scss | 125 -- .../main-quote-summary/main-quote-summary.js | 182 --- .../main-quote-summary.stories.js | 67 - .../main-quote-summary.test.js | 39 - .../main-quote-summary/quote-backdrop.js | 89 -- .../main-quote-summary/quote-backdrop.test.js | 23 - .../popover-custom-background/index.scss | 6 - .../popover-custom-background.js | 14 - .../prepare-swap-page.test.js.snap | 3 - ui/pages/swaps/prepare-swap-page/index.scss | 108 +- .../prepare-swap-page.test.js | 3 - .../swaps/prepare-swap-page/review-quote.js | 2 +- .../quote-details/index.scss | 12 - .../__snapshots__/selected-token.test.js.snap | 82 +- ui/pages/swaps/selected-token/index.scss | 142 +++ .../swaps/selected-token/selected-token.js | 86 +- .../selected-token/selected-token.test.js | 2 +- .../slippage-buttons.test.js.snap | 48 - ui/pages/swaps/slippage-buttons/index.js | 1 - ui/pages/swaps/slippage-buttons/index.scss | 111 -- .../slippage-buttons/slippage-buttons.js | 224 ---- .../slippage-buttons.stories.js | 15 - .../slippage-buttons/slippage-buttons.test.js | 99 -- .../swaps/smart-transaction-status/index.scss | 12 + .../smart-transaction-status.js | 12 +- .../view-quote-price-difference.test.js.snap | 233 ---- .../__snapshots__/view-quote.test.js.snap | 145 --- ui/pages/swaps/view-quote/index.js | 1 - ui/pages/swaps/view-quote/index.scss | 179 --- .../view-quote/view-quote-price-difference.js | 111 -- .../view-quote-price-difference.test.js | 132 -- ui/pages/swaps/view-quote/view-quote.js | 1089 ----------------- ui/pages/swaps/view-quote/view-quote.test.js | 100 -- 98 files changed, 361 insertions(+), 7612 deletions(-) delete mode 100644 ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap delete mode 100644 ui/pages/swaps/build-quote/build-quote.js delete mode 100644 ui/pages/swaps/build-quote/build-quote.stories.js delete mode 100644 ui/pages/swaps/build-quote/build-quote.test.js delete mode 100644 ui/pages/swaps/build-quote/index.js delete mode 100644 ui/pages/swaps/build-quote/index.scss delete mode 100644 ui/pages/swaps/dropdown-input-pair/README.mdx delete mode 100644 ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/index.js delete mode 100644 ui/pages/swaps/dropdown-input-pair/index.scss delete mode 100644 ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.js delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js delete mode 100644 ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js delete mode 100644 ui/pages/swaps/dropdown-search-list/index.js delete mode 100644 ui/pages/swaps/dropdown-search-list/index.scss delete mode 100644 ui/pages/swaps/main-quote-summary/README.mdx delete mode 100644 ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap delete mode 100644 ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap delete mode 100644 ui/pages/swaps/main-quote-summary/index.js delete mode 100644 ui/pages/swaps/main-quote-summary/index.scss delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.js delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js delete mode 100644 ui/pages/swaps/main-quote-summary/main-quote-summary.test.js delete mode 100644 ui/pages/swaps/main-quote-summary/quote-backdrop.js delete mode 100644 ui/pages/swaps/main-quote-summary/quote-backdrop.test.js delete mode 100644 ui/pages/swaps/popover-custom-background/index.scss delete mode 100644 ui/pages/swaps/popover-custom-background/popover-custom-background.js delete mode 100644 ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap create mode 100644 ui/pages/swaps/selected-token/index.scss delete mode 100644 ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap delete mode 100644 ui/pages/swaps/slippage-buttons/index.js delete mode 100644 ui/pages/swaps/slippage-buttons/index.scss delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.js delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js delete mode 100644 ui/pages/swaps/slippage-buttons/slippage-buttons.test.js delete mode 100644 ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap delete mode 100644 ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap delete mode 100644 ui/pages/swaps/view-quote/index.js delete mode 100644 ui/pages/swaps/view-quote/index.scss delete mode 100644 ui/pages/swaps/view-quote/view-quote-price-difference.js delete mode 100644 ui/pages/swaps/view-quote/view-quote-price-difference.test.js delete mode 100644 ui/pages/swaps/view-quote/view-quote.js delete mode 100644 ui/pages/swaps/view-quote/view-quote.test.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 296f1c716297..fe0c84afcfac 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Wir sind bereit, Ihnen die neuesten Angebote zu zeigen, wenn Sie fortfahren möchten." }, - "swapBuildQuotePlaceHolderText": { - "message": "Keine Tokens verfügbar, die mit $1 übereinstimmen.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Mit Ihrer Hardware-Wallet bestätigen" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Fehler beim Abrufen der Preisangaben" }, - "swapFetchingTokens": { - "message": "Token abrufen..." - }, "swapFromTo": { "message": "Swap von $1 auf $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Hohe Slippage" }, - "swapHighSlippageWarning": { - "message": "Der Slippage-Betrag ist sehr hoch." - }, "swapIncludesMMFee": { "message": "Enthält eine MetaMask-Gebühr von $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Niedrige Slippage" }, - "swapLowSlippageError": { - "message": "Transaktion kann fehlschlagen, maximale Slippage zu niedrig." - }, "swapMaxSlippage": { "message": "Max. Slippage" }, @@ -5344,9 +5331,6 @@ "message": "Preisdifferenz von ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Der Preiseinfluss ist die Differenz zwischen dem aktuellen Marktpreis und dem bei der Ausführung der Transaktion erhaltenen Betrag. Die Preisauswirkung ist eine Funktion der Größe Ihres Geschäfts im Verhältnis zur Größe des Liquiditätspools." - }, "swapPriceUnavailableDescription": { "message": "Die Auswirkungen auf den Preis konnten aufgrund fehlender Marktpreisdaten nicht ermittelt werden. Bitte bestätigen Sie vor dem Tausch, dass Sie mit der Menge der Tokens, die Sie erhalten werden, einverstanden sind." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Angebotsanfrage" }, - "swapReviewSwap": { - "message": "Swap überprüfen" - }, - "swapSearchNameOrAddress": { - "message": "Namen suchen oder Adresse einfügen" - }, "swapSelect": { "message": "Auswählen" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Niedriger Slippage" }, - "swapSlippageNegative": { - "message": "Slippage muss größer oder gleich Null sein" - }, "swapSlippageNegativeDescription": { "message": "Slippage muss größer oder gleich Null sein" }, @@ -5502,20 +5477,6 @@ "message": "$1 mit $2 tauschen", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Dieses Token wurde manuell hinzugefügt." - }, - "swapTokenVerificationMessage": { - "message": "Bestätigen Sie immer die Token-Adresse auf $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Nur an 1 Quelle verifiziert." - }, - "swapTokenVerificationSources": { - "message": "Auf $1 Quellen überprüft.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 wurde nur auf 1 Quelle bestätigt. Ziehen Sie in Betracht, es vor dem Fortfahren auf $2 zu bestätigen.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Unbekannt" }, - "swapVerifyTokenExplanation": { - "message": "Mehrere Token können denselben Namen und dasselbe Symbol verwenden. Überprüfen Sie $1, um sicherzugehen, dass dies der Token ist, den Sie suchen.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 zum Swap verfügbar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % Slippage" }, - "swapsAdvancedOptions": { - "message": "Erweiterte Optionen" - }, - "swapsExcessiveSlippageWarning": { - "message": "Der Slippage-Betrag ist zu hoch und wird zu einem schlechten Kurs führen. Bitte reduzieren Sie die Slippage-Toleranz auf einen Wert unter 15 %." - }, "swapsMaxSlippage": { "message": "Slippage-Toleranz" }, - "swapsNotEnoughForTx": { - "message": "Nicht genug $1, um diese Transaktion abzuschließen.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Nicht genügend $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Drittanbieter-Details überprüfen" }, - "verifyThisTokenOn": { - "message": "Diesen Token auf $1 verifizieren", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Überprüfen Sie diesen Token auf $1 und stellen Sie sicher, dass dies der Token ist, den Sie handeln möchten.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 6adad0a49176..5c42a8d829b4 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Είμαστε έτοιμοι να σας δείξουμε τις τελευταίες προσφορές, όποτε θέλετε να συνεχίσετε" }, - "swapBuildQuotePlaceHolderText": { - "message": "Δεν υπάρχουν διαθέσιμα tokens που να αντιστοιχούν σε $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Επιβεβαιώστε με το πορτοφόλι υλικού σας" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Σφάλμα κατά τη λήψη προσφορών" }, - "swapFetchingTokens": { - "message": "Λήψη tokens..." - }, "swapFromTo": { "message": "Η ανταλλαγή από $1 έως $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Υψηλή ολίσθηση" }, - "swapHighSlippageWarning": { - "message": "Το ποσό ολίσθησης είναι πολύ υψηλό." - }, "swapIncludesMMFee": { "message": "Περιλαμβάνει μια χρέωση $1% στο MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Χαμηλή ολίσθηση" }, - "swapLowSlippageError": { - "message": "Η συναλλαγή ενδέχεται να αποτύχει, η μέγιστη ολίσθηση είναι πολύ χαμηλή." - }, "swapMaxSlippage": { "message": "Μέγιστη ολίσθηση" }, @@ -5344,9 +5331,6 @@ "message": "Διαφορά τιμής ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Η επίπτωση στις τιμές είναι η διαφορά μεταξύ της τρέχουσας τιμής αγοράς και του ποσού που εισπράττεται κατά την εκτέλεση της συναλλαγής. Η επίπτωση στις τιμές είναι συνάρτηση του μεγέθους της συναλλαγής σας σε σχέση με το μέγεθος του αποθέματος ρευστότητας." - }, "swapPriceUnavailableDescription": { "message": "Η επίπτωση στις τιμές δεν ήταν δυνατόν να προσδιοριστεί λόγω έλλειψης στοιχείων για τις τιμές της αγοράς. Παρακαλούμε επιβεβαιώστε ότι είστε ικανοποιημένοι με το ποσό των tokens που πρόκειται να λάβετε πριν από την ανταλλαγή." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Αίτημα για προσφορά" }, - "swapReviewSwap": { - "message": "Έλεγχος της ανταλλαγής" - }, - "swapSearchNameOrAddress": { - "message": "Αναζήτηση ονόματος ή επικόλληση διεύθυνσης" - }, "swapSelect": { "message": "Επιλογή" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Χαμηλή απόκλιση" }, - "swapSlippageNegative": { - "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με το μηδέν" - }, "swapSlippageNegativeDescription": { "message": "Η απόκλιση πρέπει να είναι μεγαλύτερη ή ίση με μηδέν" }, @@ -5502,20 +5477,6 @@ "message": "Ανταλλαγή $1 έως $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Αυτό το token έχει προστεθεί χειροκίνητα." - }, - "swapTokenVerificationMessage": { - "message": "Πάντα να επιβεβαιώνετε τη διεύθυνση του token στο $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Επαληθεύτηκε μόνο σε 1 πηγή." - }, - "swapTokenVerificationSources": { - "message": "Επαληθεύτηκε μόνο σε $1 πηγές.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Το $1 επαληθεύτηκε μόνο από 1 πηγή. Εξετάστε το ενδεχόμενο επαλήθευσης σε $2 πριν προχωρήσετε.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Άγνωστο" }, - "swapVerifyTokenExplanation": { - "message": "Πολλαπλά tokens μπορούν να χρησιμοποιούν το ίδιο όνομα και σύμβολο. Ελέγξτε το $1 για να επιβεβαιώσετε ότι αυτό είναι το token που ψάχνετε.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 διαθέσιμα για ανταλλαγή", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Απόκλιση" }, - "swapsAdvancedOptions": { - "message": "Προηγμένες επιλογές" - }, - "swapsExcessiveSlippageWarning": { - "message": "Το ποσοστό απόκλισης είναι πολύ υψηλό και θα οδηγήσει σε χαμηλή τιμή. Παρακαλούμε μειώστε την ανοχή απόκλισης σε τιμή κάτω του 15%." - }, "swapsMaxSlippage": { "message": "Ανοχή απόκλισης" }, - "swapsNotEnoughForTx": { - "message": "Δεν υπάρχουν αρκετά $1 για να ολοκληρωθεί αυτή η συναλλαγή", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Δεν υπάρχουν αρκετά $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Επαλήθευση στοιχείων τρίτων" }, - "verifyThisTokenOn": { - "message": "Επαλήθευση αυτού του token στο $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Επαληθεύστε αυτό το token στο $1 και βεβαιωθείτε ότι αυτό είναι το token που θέλετε να κάνετε συναλλαγές.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Έκδοση" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 57dd9152752e..49d48b9c71ac 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5592,10 +5592,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5660,9 +5656,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5697,9 +5690,6 @@ "swapHighSlippage": { "message": "High slippage" }, - "swapHighSlippageWarning": { - "message": "Slippage amount is very high." - }, "swapIncludesGasAndMetaMaskFee": { "message": "Includes gas and a $1% MetaMask fee", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5725,9 +5715,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5762,9 +5749,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5811,12 +5795,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5849,9 +5827,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5920,20 +5895,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5954,30 +5915,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6492,14 +6435,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 25cb6cd3df29..fc635e33a708 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -5341,10 +5341,6 @@ "swapAreYouStillThereDescription": { "message": "We’re ready to show you the latest quotes when you want to continue" }, - "swapBuildQuotePlaceHolderText": { - "message": "No tokens available matching $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -5409,9 +5405,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error fetching quotes" }, - "swapFetchingTokens": { - "message": "Fetching tokens..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5432,9 +5425,6 @@ "swapHighSlippage": { "message": "High slippage" }, - "swapHighSlippageWarning": { - "message": "Slippage amount is very high." - }, "swapIncludesMMFee": { "message": "Includes a $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5456,9 +5446,6 @@ "swapLowSlippage": { "message": "Low slippage" }, - "swapLowSlippageError": { - "message": "Transaction may fail, max slippage too low." - }, "swapMaxSlippage": { "message": "Max slippage" }, @@ -5493,9 +5480,6 @@ "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, @@ -5542,12 +5526,6 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, - "swapReviewSwap": { - "message": "Review swap" - }, - "swapSearchNameOrAddress": { - "message": "Search name or paste address" - }, "swapSelect": { "message": "Select" }, @@ -5580,9 +5558,6 @@ "swapSlippageLowTitle": { "message": "Low slippage" }, - "swapSlippageNegative": { - "message": "Slippage must be greater or equal to zero" - }, "swapSlippageNegativeDescription": { "message": "Slippage must be greater or equal to zero" }, @@ -5651,20 +5626,6 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "This token has been added manually." - }, - "swapTokenVerificationMessage": { - "message": "Always confirm the token address on $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Only verified on 1 source." - }, - "swapTokenVerificationSources": { - "message": "Verified on $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 is only verified on 1 source. Consider verifying it on $2 before proceeding.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5685,30 +5646,12 @@ "swapUnknown": { "message": "Unknown" }, - "swapVerifyTokenExplanation": { - "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 available to swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Advanced options" - }, - "swapsExcessiveSlippageWarning": { - "message": "Slippage amount is too high and will result in a bad rate. Please reduce your slippage tolerance to a value below 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Not enough $1 to complete this transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Not enough $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6255,14 +6198,6 @@ "verifyContractDetails": { "message": "Verify third-party details" }, - "verifyThisTokenOn": { - "message": "Verify this token on $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verify this token on $1 and make sure this is the token you want to trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 03fa1e519ef7..97d6f4be9854 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -5189,10 +5189,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos listos para mostrarle las últimas cotizaciones cuando desee continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con su monedero físico" }, @@ -5257,9 +5253,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens…" - }, "swapFromTo": { "message": "El intercambio de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5280,9 +5273,6 @@ "swapHighSlippage": { "message": "Deslizamiento alto" }, - "swapHighSlippageWarning": { - "message": "El monto del deslizamiento es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5304,9 +5294,6 @@ "swapLowSlippage": { "message": "Deslizamiento bajo" }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el deslizamiento máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -5341,9 +5328,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el intercambio, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -5390,12 +5374,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar intercambio" - }, - "swapSearchNameOrAddress": { - "message": "Buscar nombre o pegar dirección" - }, "swapSelect": { "message": "Seleccionar" }, @@ -5428,9 +5406,6 @@ "swapSlippageLowTitle": { "message": "Deslizamiento bajo" }, - "swapSlippageNegative": { - "message": "El deslizamiento debe ser mayor o igual que cero" - }, "swapSlippageNegativeDescription": { "message": "El deslizamiento debe ser mayor o igual que cero" }, @@ -5499,20 +5474,6 @@ "message": "Intercambiar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 solo se verifica en 1 fuente. Considere verificarlo en $2 antes de continuar.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5533,30 +5494,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles para intercambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de deslizamiento" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del deslizamiento es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de deslizamiento a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de deslizamiento" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para completar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "No hay suficiente $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6052,14 +5995,6 @@ "verifyContractDetails": { "message": "Verificar detalles de terceros" }, - "verifyThisTokenOn": { - "message": "Comprobar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versión" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index cd980aaa99c2..672823c370ba 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1894,10 +1894,6 @@ "message": "Necesita $1 más $2 para realizar este canje", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "No hay tokens disponibles que coincidan con $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1949,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Error al capturar cotizaciones" }, - "swapFetchingTokens": { - "message": "Capturando tokens..." - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1969,16 +1962,10 @@ "message": "Las tarifas de gas se pagan a los mineros de criptomonedas que procesan transacciones en la red $1. MetaMask no se beneficia de las tarifas de gas.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "El monto del desfase es muy alto." - }, "swapIncludesMMFee": { "message": "Incluye una tasa de MetaMask del $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "Es posible que la transacción tenga errores, el desfase máximo es demasiado bajo." - }, "swapMaxSlippage": { "message": "Desfase máximo" }, @@ -2009,9 +1996,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "El impacto sobre el precio es la diferencia entre el precio actual del mercado y el monto recibido durante la ejecución de la transacción. El impacto sobre el precio es una función del tamaño de su transacción respecto de la dimensión del fondo de liquidez." - }, "swapPriceUnavailableDescription": { "message": "No se pudo determinar el impacto sobre el precio debido a la falta de datos de los precios del mercado. Antes de realizar el canje, confirme que está de acuerdo con la cantidad de tokens que está a punto de recibir." }, @@ -2051,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitud de cotización" }, - "swapReviewSwap": { - "message": "Revisar canje" - }, "swapSelect": { "message": "Seleccionar" }, @@ -2066,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "A continuación se muestran todas las cotizaciones recopiladas de diversas fuentes de liquidez." }, - "swapSlippageNegative": { - "message": "El desfase debe ser mayor o igual que cero" - }, "swapSource": { "message": "Fuente de liquidez" }, @@ -2102,20 +2080,6 @@ "message": "Canjear $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token se añadió de forma manual." - }, - "swapTokenVerificationMessage": { - "message": "Siempre confirme la dirección del token en $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Solo se verificó en una fuente." - }, - "swapTokenVerificationSources": { - "message": "Verificar en $1 fuentes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite hasta $2 decimales", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2129,30 +2093,12 @@ "swapUnknown": { "message": "Desconocido" }, - "swapVerifyTokenExplanation": { - "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponible para canje", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de desfase" }, - "swapsAdvancedOptions": { - "message": "Opciones avanzadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "El monto del desfase es muy alto, por lo que recibirá una tasa de conversión desfavorable. Disminuya su tolerancia de desfase a un valor menor al 15 %." - }, "swapsMaxSlippage": { "message": "Tolerancia de desfase" }, - "swapsNotEnoughForTx": { - "message": "No hay $1 suficientes para finalizar esta transacción", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver en actividad" }, @@ -2389,14 +2335,6 @@ "userName": { "message": "Nombre de usuario" }, - "verifyThisTokenOn": { - "message": "Verificar este token en $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique este token en $1 y asegúrese de que sea el token con el que quiere realizar la transacción.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos los detalles" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index fccc617dee93..dbaffd44cf38 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Si vous le souhaitez, nous sommes prêts à vous présenter les dernières cotations" }, - "swapBuildQuotePlaceHolderText": { - "message": "Aucun jeton disponible correspondant à $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirmez avec votre portefeuille matériel" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erreur lors de la récupération des cotations" }, - "swapFetchingTokens": { - "message": "Récupération des jetons…" - }, "swapFromTo": { "message": "Le swap de $1 vers $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Important effet de glissement" }, - "swapHighSlippageWarning": { - "message": "Le montant du glissement est très élevé." - }, "swapIncludesMMFee": { "message": "Comprend des frais MetaMask à hauteur de $1 %.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Faible effet de glissement" }, - "swapLowSlippageError": { - "message": "La transaction peut échouer, car le glissement maximal est trop faible." - }, "swapMaxSlippage": { "message": "Glissement maximal" }, @@ -5344,9 +5331,6 @@ "message": "Différence de prix de ~$1", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "L’incidence sur les prix correspond à la différence entre le prix actuel du marché et le montant reçu lors de l’exécution de la transaction. Cette répercussion dépend du volume de votre transaction par rapport au volume de la réserve de liquidités." - }, "swapPriceUnavailableDescription": { "message": "L’incidence sur les prix n’a pas pu être déterminée faute de données suffisantes sur les prix du marché. Veuillez confirmer que vous êtes satisfait·e du nombre de jetons que vous êtes sur le point de recevoir avant de procéder au swap." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Demande de cotation" }, - "swapReviewSwap": { - "message": "Vérifier le swap" - }, - "swapSearchNameOrAddress": { - "message": "Rechercher le nom ou coller l’adresse" - }, "swapSelect": { "message": "Sélectionner" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Faible effet de glissement" }, - "swapSlippageNegative": { - "message": "Le glissement doit être supérieur ou égal à zéro" - }, "swapSlippageNegativeDescription": { "message": "Le slippage doit être supérieur ou égal à zéro" }, @@ -5502,20 +5477,6 @@ "message": "Swap de $1 vers $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Ce jeton a été ajouté manuellement." - }, - "swapTokenVerificationMessage": { - "message": "Confirmez toujours l’adresse du jeton sur $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Vérification effectuée uniquement sur 1 source." - }, - "swapTokenVerificationSources": { - "message": "Vérification effectuée sur $1 sources.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 n’a été vérifié que par 1 source. Envisagez de le vérifier auprès de $2 sources avant de continuer.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Inconnu" }, - "swapVerifyTokenExplanation": { - "message": "Attention, plusieurs jetons peuvent utiliser le même nom et le même symbole. Vérifiez $1 pour vous assurer qu’il s’agit bien du jeton que vous recherchez.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibles pour un swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0 % de glissement" }, - "swapsAdvancedOptions": { - "message": "Options avancées" - }, - "swapsExcessiveSlippageWarning": { - "message": "Le montant du glissement est trop élevé et donnera lieu à un mauvais taux. Veuillez réduire votre tolérance de glissement à une valeur inférieure à 15 %." - }, "swapsMaxSlippage": { "message": "Tolérance au slippage" }, - "swapsNotEnoughForTx": { - "message": "Pas assez de $1 pour effectuer cette transaction", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Pas assez de $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Vérifier les informations relatives aux tiers" }, - "verifyThisTokenOn": { - "message": "Vérifier ce jeton sur $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Vérifiez ce jeton sur $1 et qu’il s’agit bien de celui que vous souhaitez échanger.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Version" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7333626d1e30..0e624b4ba807 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "जब आप जारी रखना चाहते हैं तो हम आपको लेटेस्ट उद्धरण दिखाने के लिए तैयार हैं" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 के मिलान वाले कोई भी टोकन उपलब्ध नहीं हैं", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "अपने hardware wallet से कन्फर्म करें" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "उद्धरण प्राप्त करने में गड़बड़ी" }, - "swapFetchingTokens": { - "message": "टोकन प्राप्त किए जा रहे हैं..." - }, "swapFromTo": { "message": "$1 से $2 का स्वैप", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "अधिक स्लिपेज" }, - "swapHighSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है।" - }, "swapIncludesMMFee": { "message": "$1% MetaMask फ़ीस शामिल है।", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "कम स्लिपेज" }, - "swapLowSlippageError": { - "message": "ट्रांसेक्शन विफल हो सकता है, अधिकतम स्लिपेज बहुत कम है।" - }, "swapMaxSlippage": { "message": "अधिकतम स्लिपेज" }, @@ -5344,9 +5331,6 @@ "message": "~$1% का मूल्य अंतर", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "मूल्य प्रभाव, वर्तमान बाजार मूल्य और ट्रांसेक्शन निष्पादन के दौरान प्राप्त अमाउंट के बीच का अंतर है। मूल्य प्रभाव चलनिधि पूल के आकार के सापेक्ष आपके व्यापार के आकार का एक कार्य है।" - }, "swapPriceUnavailableDescription": { "message": "बाजार मूल्य डेटा की कमी के कारण मूल्य प्रभाव को निर्धारित नहीं किया जा सका। कृपया कन्फर्म करें कि आप स्वैप करने से पहले प्राप्त होने वाले टोकन की अमाउंट को लेकर सहज हैं।" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "उद्धरण के लिए अनुरोध" }, - "swapReviewSwap": { - "message": "स्वैप की समीक्षा करें" - }, - "swapSearchNameOrAddress": { - "message": "नाम खोजें या ऐड्रेस पेस्ट करें" - }, "swapSelect": { "message": "चयन करें" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "कम स्लिपेज" }, - "swapSlippageNegative": { - "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" - }, "swapSlippageNegativeDescription": { "message": "स्लिपेज शून्य से अधिक या बराबर होना चाहिए" }, @@ -5502,20 +5477,6 @@ "message": "$1 से $2 में स्वैप करें", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "इस टोकन को मैन्युअल रूप से जोड़ा गया है।" - }, - "swapTokenVerificationMessage": { - "message": "हमेशा $1 पर टोकन एड्रेस की कन्फर्म करें।", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "केवल 1 स्रोत पर वेरीफ़ाई।" - }, - "swapTokenVerificationSources": { - "message": "$1 स्रोतों पर वेरीफ़ाई।", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 केवल 1 स्रोत पर वेरीफ़ाई है। आगे बढ़ने से पहले इसे $2 पर वेरीफ़ाई करने पर विचार करें।", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "अज्ञात" }, - "swapVerifyTokenExplanation": { - "message": "एकाधिक टोकन एक ही नाम और प्रतीक का इस्तेमाल कर सकते हैं। यह वेरीफ़ाई करने के लिए $1 देखें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 स्वैप के लिए उपलब्ध है", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% स्लिपेज" }, - "swapsAdvancedOptions": { - "message": "एडवांस्ड विकल्प" - }, - "swapsExcessiveSlippageWarning": { - "message": "स्लिपेज अमाउंट बहुत अधिक है और इस वजह से खराब दर होगी। कृपया अपने स्लिपेज टॉलरेंस को 15% से नीचे के वैल्यू तक कम करें।" - }, "swapsMaxSlippage": { "message": "स्लिपेज टॉलरेंस" }, - "swapsNotEnoughForTx": { - "message": "इस ट्रांसेक्शन को पूरा करने के लिए $1 कम है", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 कम", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "थर्ड-पार्टी विवरण वेरीफ़ाई करें" }, - "verifyThisTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "इस टोकन को $1 पर वेरीफ़ाई करें और पक्का करें कि यह वही टोकन है जिससे आप ट्रेड करना चाहते हैं।", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "वर्शन" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index f2d8828e9226..68b52556c3f6 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Kami siap menampilkan kuotasi terbaru jika Anda ingin melanjutkan" }, - "swapBuildQuotePlaceHolderText": { - "message": "Tidak ada token yang cocok yang tersedia $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Konfirmasikan dengan dompet perangkat keras Anda" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Terjadi kesalahan saat mengambil kuota" }, - "swapFetchingTokens": { - "message": "Mengambil token..." - }, "swapFromTo": { "message": "Pertukaran dari $1 ke $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Selip tinggi" }, - "swapHighSlippageWarning": { - "message": "Jumlah slippage sangat tinggi." - }, "swapIncludesMMFee": { "message": "Termasuk $1% biaya MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Selip rendah" }, - "swapLowSlippageError": { - "message": "Transaksi berpotensi gagal, selip maks terlalu rendah." - }, "swapMaxSlippage": { "message": "Selipi maks" }, @@ -5344,9 +5331,6 @@ "message": "Perbedaan harga ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Dampak harga adalah selisih antara harga pasar saat ini dan jumlah yang diterima selama terjadinya transaksi. Dampak harga adalah fungsi ukuran dagang relatif terhadap ukuran pul likuiditas." - }, "swapPriceUnavailableDescription": { "message": "Dampak harga tidak dapat ditentukan karena kurangnya data harga pasar. Harap konfirmasi bahwa Anda setuju dengan jumlah token yang akan Anda terima sebelum penukaran." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Minta kuotasi" }, - "swapReviewSwap": { - "message": "Tinjau pertukaran" - }, - "swapSearchNameOrAddress": { - "message": "Cari nama atau tempel alamat" - }, "swapSelect": { "message": "Pilih" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Selip rendah" }, - "swapSlippageNegative": { - "message": "Selip harus lebih besar atau sama dengan nol" - }, "swapSlippageNegativeDescription": { "message": "Selip harus lebih besar atau sama dengan nol" }, @@ -5502,20 +5477,6 @@ "message": "Tukar $1 ke $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token ini telah ditambahkan secara manual." - }, - "swapTokenVerificationMessage": { - "message": "Selalu konfirmasikan alamat token di $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Hanya diverifikasi di 1 sumber." - }, - "swapTokenVerificationSources": { - "message": "Diverifikasi di $1 sumber.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 hanya diverifikasi di 1 sumber. Pertimbangkan untuk memverifikasinya di $2 sebelum melanjutkan.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Tidak diketahui" }, - "swapVerifyTokenExplanation": { - "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 tersedia untuk ditukar", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Selip 0%" }, - "swapsAdvancedOptions": { - "message": "Opsi lanjutan" - }, - "swapsExcessiveSlippageWarning": { - "message": "Jumlah selip terlalu tinggi dan akan mengakibatkan tarif yang buruk. Kurangi toleransi selip Anda ke nilai di bawah 15%." - }, "swapsMaxSlippage": { "message": "Toleransi selip" }, - "swapsNotEnoughForTx": { - "message": "$1 tidak cukup untuk menyelesaikan transaksi ini", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 tidak cukup", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Verifikasikan detail pihak ketiga" }, - "verifyThisTokenOn": { - "message": "Verifikasikan token ini di $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifikasikan token ini di $1 dan pastikan ini adalah token yang ingin Anda perdagangkan.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versi" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 70e81c595852..4dac80c253b3 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1387,10 +1387,6 @@ "message": "Devi avere $1 $2 in più per completare lo scambio", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Non ci sono token disponibile con questo nome $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapCustom": { "message": "personalizza" }, @@ -1419,12 +1415,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Errore recuperando le quotazioni" }, - "swapFetchingTokens": { - "message": "Recuperando i token..." - }, - "swapLowSlippageError": { - "message": "La transazione può fallire, il massimo slippage è troppo basso." - }, "swapMaxSlippage": { "message": "Slippage massimo" }, @@ -1484,9 +1474,6 @@ "swapRequestForQuotation": { "message": "Richiedi quotazione" }, - "swapReviewSwap": { - "message": "Verifica Scambio" - }, "swapSelect": { "message": "Selezione" }, @@ -1519,44 +1506,15 @@ "message": "Scambia da $1 a $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationMessage": { - "message": "Verifica sempre l'indirizzo del token su $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificato solo su una fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificato su $1 fonti.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Transazione completata" }, "swapUnknown": { "message": "Sconosciuto" }, - "swapVerifyTokenExplanation": { - "message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponibili allo scambio", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, - "swapsAdvancedOptions": { - "message": "Impostazioni Avanzate" - }, - "swapsExcessiveSlippageWarning": { - "message": "L'importo di slippage è troppo alto e risulterà in una tariffa sconveniente. Riduci la tolleranza allo slippage ad un valore minore di 15%." - }, "swapsMaxSlippage": { "message": "Tolleranza Slippage" }, - "swapsNotEnoughForTx": { - "message": "Non hai abbastanza $1 per completare la transazione", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Vedi in attività" }, @@ -1691,10 +1649,6 @@ "userName": { "message": "Nome utente" }, - "verifyThisTokenOn": { - "message": "Verifica questo token su $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Vedi tutti i dettagli" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index cadc1ab1e302..73f3f8300646 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "続ける際には、最新のクォートを表示する準備ができています" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1と一致するトークンがありません", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "ハードウェアウォレットで確定" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "見積もり取得エラー" }, - "swapFetchingTokens": { - "message": "トークンを取得中..." - }, "swapFromTo": { "message": "$1から$2へのスワップ", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "高スリッページ" }, - "swapHighSlippageWarning": { - "message": "スリッページが非常に大きいです。" - }, "swapIncludesMMFee": { "message": "$1%のMetaMask手数料が含まれています。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "低スリッページ" }, - "swapLowSlippageError": { - "message": "トランザクションが失敗する可能性があります。最大スリッページが低すぎます。" - }, "swapMaxSlippage": { "message": "最大スリッページ" }, @@ -5344,9 +5331,6 @@ "message": "最大$1%の価格差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "プライスインパクトとは、現在の市場価格と取引の約定時に受け取る金額の差のことです。プライスインパクトは、流動性プールに対する取引の大きさにより発生します。" - }, "swapPriceUnavailableDescription": { "message": "市場価格のデータが不足しているため、プライスインパクトを測定できませんでした。スワップする前に、これから受領するトークンの額に問題がないか確認してください。" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "見積もりのリクエスト" }, - "swapReviewSwap": { - "message": "スワップの確認" - }, - "swapSearchNameOrAddress": { - "message": "名前を検索するかアドレスを貼り付けてください" - }, "swapSelect": { "message": "選択" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低スリッページ" }, - "swapSlippageNegative": { - "message": "スリッページは0以上でなければなりません。" - }, "swapSlippageNegativeDescription": { "message": "スリッページは0以上でなければなりません" }, @@ -5502,20 +5477,6 @@ "message": "$1を$2にスワップ", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "このトークンは手動で追加されました。" - }, - "swapTokenVerificationMessage": { - "message": "常に$1のトークンアドレスを確認してください。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1つのソースでのみ検証済みです。" - }, - "swapTokenVerificationSources": { - "message": "$1個のソースで検証済みです。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1は1つのソースでしか検証されていません。進める前に$2で検証することをご検討ください。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "不明" }, - "swapVerifyTokenExplanation": { - "message": "複数のトークンで同じ名前とシンボルを使用できます。$1をチェックして、これが探しているトークンであることを確認してください。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2がスワップに使用可能です", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%スリッページ" }, - "swapsAdvancedOptions": { - "message": "詳細オプション" - }, - "swapsExcessiveSlippageWarning": { - "message": "スリッページ額が非常に大きいので、レートが不利になります。最大スリッページを15%未満の値に減らしてください。" - }, "swapsMaxSlippage": { "message": "最大スリッページ" }, - "swapsNotEnoughForTx": { - "message": "トランザクションを完了させるには、$1が不足しています", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1が不足しています", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "サードパーティの詳細を確認" }, - "verifyThisTokenOn": { - "message": "このトークンを$1で検証", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "このトークンを$1で検証して、取引したいトークンであることを確認してください。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "バージョン" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 760ad7df43dc..be1de55c51c7 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "계속하기 원하시면 최신 견적을 보여드리겠습니다" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1와(과) 일치하는 토큰이 없습니다.", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "하드웨어 지갑으로 컨펌합니다." }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "견적을 가져오는 중 오류 발생" }, - "swapFetchingTokens": { - "message": "토큰 가져오는 중..." - }, "swapFromTo": { "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "높은 슬리피지" }, - "swapHighSlippageWarning": { - "message": "슬리패지 금액이 아주 큽니다." - }, "swapIncludesMMFee": { "message": "$1%의 MetaMask 요금이 포함됩니다.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "낮은 슬리피지" }, - "swapLowSlippageError": { - "message": "트랜잭션이 실패할 수도 있습니다. 최대 슬리패지가 너무 낮습니다." - }, "swapMaxSlippage": { "message": "최대 슬리패지" }, @@ -5344,9 +5331,6 @@ "message": "~$1%의 가격 차이", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "가격 영향은 현재 시장 가격과 트랜잭션 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 트랜잭션의 크기를 나타내는 함수입니다." - }, "swapPriceUnavailableDescription": { "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수가 만족스러운지 컨펌하시기 바랍니다." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "견적 요청" }, - "swapReviewSwap": { - "message": "스왑 검토" - }, - "swapSearchNameOrAddress": { - "message": "이름 검색 또는 주소 붙여넣기" - }, "swapSelect": { "message": "선택" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "낮은 슬리피지" }, - "swapSlippageNegative": { - "message": "슬리패지는 0보다 크거나 같아야 합니다." - }, "swapSlippageNegativeDescription": { "message": "슬리피지는 0보다 크거나 같아야 합니다." }, @@ -5502,20 +5477,6 @@ "message": "$1에서 $2(으)로 스왑", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "이 토큰은 직접 추가되었습니다." - }, - "swapTokenVerificationMessage": { - "message": "항상 $1에서 토큰 주소를 컨펌하세요.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "1개의 소스에서만 확인됩니다." - }, - "swapTokenVerificationSources": { - "message": "$1개 소스에서 확인되었습니다.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 토큰은 1 소스에서만 확인됩니다. 계속 진행하기 전에 $2에서도 확인하세요.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "알 수 없음" }, - "swapVerifyTokenExplanation": { - "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1에서 원하는 토큰인지 확인하세요.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 스왑 가능", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% 슬리패지" }, - "swapsAdvancedOptions": { - "message": "고급 옵션" - }, - "swapsExcessiveSlippageWarning": { - "message": "슬리패지 금액이 너무 커서 전환율이 좋지 않습니다. 슬리패지 허용치를 15% 값 이하로 줄이세요." - }, "swapsMaxSlippage": { "message": "슬리피지 허용치" }, - "swapsNotEnoughForTx": { - "message": "$1이(가) 부족하여 이 트랜잭션을 완료할 수 없습니다.", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 부족", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "타사 세부 정보 확인" }, - "verifyThisTokenOn": { - "message": "$1에서 이 토큰 확인", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "$1에서 이 토큰이 트랜잭션할 토큰이 맞는지 확인하세요.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "버전" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index e12eb4379cf1..1687cb7818f0 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1268,10 +1268,6 @@ "message": "Kailangan mo ng $1 pa $2 para makumpleto ang pag-swap na ito", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin ang iyong hardware wallet" }, @@ -1313,9 +1309,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1323,12 +1316,6 @@ "swapGasFeesSplit": { "message": "Hahatiin sa pagitan ng dalawang transaksyon na ito ang mga bayarin sa gas sa nakaraang screen." }, - "swapHighSlippageWarning": { - "message": "Sobrang laki ng halaga ng slippage." - }, - "swapLowSlippageError": { - "message": "Posibleng hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -1355,9 +1342,6 @@ "message": "Kaibahan sa presyo na ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang epekto sa presyo ay ang pagkakaiba sa kasalukuyang presyo sa merkado at sa halagang natanggap sa pag-execute ng transaksyon. Ang epekto sa presyo ay isang function ng laki ng iyong trade kumpara sa laki ng liquidity pool." - }, "swapPriceUnavailableDescription": { "message": "Hindi natukoy ang epekto sa presyo dahil sa kakulangan ng data sa presyo sa merkado. Pakikumpirma na kumportable ka sa dami ng mga token na matatanggap mo bago makipag-swap." }, @@ -1397,9 +1381,6 @@ "swapRequestForQuotation": { "message": "Mag-request ng quotation" }, - "swapReviewSwap": { - "message": "Suriin ang Pag-swap" - }, "swapSelect": { "message": "Piliin" }, @@ -1412,9 +1393,6 @@ "swapSelectQuotePopoverDescription": { "message": "Makikita sa ibaba ang lahat ng quote na nakuha mula sa maraming pinagkukunan ng liquidity." }, - "swapSlippageNegative": { - "message": "Dapat ay mas malaki sa o katumbas ng zero ang slippage" - }, "swapSource": { "message": "Pinagkunan ng liquidity" }, @@ -1442,20 +1420,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manual na idinagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang address ng token sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify lang sa 1 source." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTransactionComplete": { "message": "Nakumpleto ang transaksyon" }, @@ -1465,30 +1429,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta ito sa masamang rating. Pakibabaan ang iyong tolerance ng slippage sa value na mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Tolerance ng Slippage" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Tingnan sa aktibidad" }, @@ -1648,14 +1594,6 @@ "userName": { "message": "Username" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at tiyaking ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Tingnan ang lahat ng detalye" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 06c9fbe38adf..656733cecf65 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Estamos prontos para exibir as últimas cotações quando quiser continuar" }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Slippage alto" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Slippage baixo" }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -5344,9 +5331,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto do preço é a diferença entre o preço de mercado atual e o valor recebido quando é executada a transação. O impacto do preço é resultado do tamanho da sua transação relativo ao tamanho do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, - "swapSearchNameOrAddress": { - "message": "Pesquise o nome ou cole o endereço" - }, "swapSelect": { "message": "Selecione" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Slippage baixo" }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSlippageNegativeDescription": { "message": "O slippage deve ser maior ou igual a zero" }, @@ -5502,20 +5477,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Esse token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 só foi verificado em 1 fonte. Considere verificá-lo em $2 antes de prosseguir.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 insuficiente", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Verificar dados do terceiro" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Versão" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 2becf1c495a1..6062013d6d6f 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1894,10 +1894,6 @@ "message": "Você precisa de mais $1 $2 para concluir essa troca", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, - "swapBuildQuotePlaceHolderText": { - "message": "Nenhum token disponível correspondente a $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -1949,9 +1945,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Erro ao obter cotações" }, - "swapFetchingTokens": { - "message": "Obtendo tokens..." - }, "swapFromTo": { "message": "A troca de $1 para $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -1969,16 +1962,10 @@ "message": "As taxas de gás são pagas aos mineradores de criptoativos que processam as transações na rede de $1. A MetaMask não lucra com taxas de gás.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, - "swapHighSlippageWarning": { - "message": "O valor de slippage está muito alto." - }, "swapIncludesMMFee": { "message": "Inclui uma taxa de $1% da MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapLowSlippageError": { - "message": "A transação pode falhar; o slippage máximo está baixo demais." - }, "swapMaxSlippage": { "message": "Slippage máximo" }, @@ -2009,9 +1996,6 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "O impacto no preço é a diferença entre o preço de mercado atual e o valor recebido durante a execução da transação. O impacto no preço é uma função do porte da sua operação em relação ao porte do pool de liquidez." - }, "swapPriceUnavailableDescription": { "message": "O impacto no preço não pôde ser determinado devido à ausência de dados sobre o preço de mercado. Confirme que você está satisfeito com a quantidade de tokens que você está prestes a receber antes de fazer a troca." }, @@ -2051,9 +2035,6 @@ "swapRequestForQuotation": { "message": "Solicitação de cotação" }, - "swapReviewSwap": { - "message": "Revisar troca" - }, "swapSelect": { "message": "Selecione" }, @@ -2066,9 +2047,6 @@ "swapSelectQuotePopoverDescription": { "message": "Abaixo estão todas as cotações reunidas de diversas fontes de liquidez." }, - "swapSlippageNegative": { - "message": "O slippage deve ser maior ou igual a zero" - }, "swapSource": { "message": "Fonte de liquidez" }, @@ -2102,20 +2080,6 @@ "message": "Trocar $1 por $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Este token foi adicionado manualmente." - }, - "swapTokenVerificationMessage": { - "message": "Sempre confirme o endereço do token no $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Verificado somente em 1 fonte." - }, - "swapTokenVerificationSources": { - "message": "Verificado em $1 fontes.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTooManyDecimalsError": { "message": "$1 permite até $2 decimais", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" @@ -2129,30 +2093,12 @@ "swapUnknown": { "message": "Desconhecido" }, - "swapVerifyTokenExplanation": { - "message": "Vários tokens podem usar o mesmo nome e símbolo. Confira $1 para verificar se esse é o token que você está buscando.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 disponível para troca", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% de slippage" }, - "swapsAdvancedOptions": { - "message": "Opções avançadas" - }, - "swapsExcessiveSlippageWarning": { - "message": "O valor de slippage está muito alto e resultará em uma taxa ruim. Reduza sua tolerância a slippage para um valor inferior a 15%." - }, "swapsMaxSlippage": { "message": "Tolerância a slippage" }, - "swapsNotEnoughForTx": { - "message": "Não há $1 suficiente para concluir essa transação", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsViewInActivity": { "message": "Ver na atividade" }, @@ -2389,14 +2335,6 @@ "userName": { "message": "Nome de usuário" }, - "verifyThisTokenOn": { - "message": "Verifique esse token no $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Verifique esse token no $1 e confirme que é o token que você deseja negociar.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "Ver todos os detalhes" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 2308eb10721e..0c2f92821ed2 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Мы готовы показать вам последние котировки, когда вы захотите продолжить" }, - "swapBuildQuotePlaceHolderText": { - "message": "Нет доступных токенов, соответствующих $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Подтвердите с помощью аппаратного кошелька" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Ошибка при получении котировок" }, - "swapFetchingTokens": { - "message": "Получение токенов..." - }, "swapFromTo": { "message": "Своп $1 на $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Высокое проскальзывание" }, - "swapHighSlippageWarning": { - "message": "Сумма проскальзывания очень велика." - }, "swapIncludesMMFee": { "message": "Включает комиссию MetaMask в размере $1%.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Низкое проскальзывание" }, - "swapLowSlippageError": { - "message": "Возможно, не удастся выполнить транзакцию. Ммаксимальное проскальзывание слишком низкое." - }, "swapMaxSlippage": { "message": "Максимальное проскальзывание" }, @@ -5344,9 +5331,6 @@ "message": "Разница в цене составляет ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время исполнения транзакции. Колебание цены зависит от соотношения размера вашей сделки и размера пула ликвидности." - }, "swapPriceUnavailableDescription": { "message": "Не удалось определить колебание цены из-за отсутствия данных о рыночных ценах. Перед свопом убедитесь, что вас устраивает количество токенов, которое вы получите." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Запрос котировки" }, - "swapReviewSwap": { - "message": "Проверить своп" - }, - "swapSearchNameOrAddress": { - "message": "Выполните поиск по имени или вставьте адрес" - }, "swapSelect": { "message": "Выбрать" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Низкое проскальзывание" }, - "swapSlippageNegative": { - "message": "Проскальзывание должно быть больше нуля или равно нулю" - }, "swapSlippageNegativeDescription": { "message": "Проскальзывание должно быть больше или равно нулю" }, @@ -5502,20 +5477,6 @@ "message": "Выполнить своп $1 на $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Этот токен был добавлен вручную." - }, - "swapTokenVerificationMessage": { - "message": "Всегда проверяйте адрес токена на $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Токен проверен только в 1 источнике." - }, - "swapTokenVerificationSources": { - "message": "Токен проверен в $1 источниках.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 проверяется только на 1 источнике. Попробуйте проверить его на $2, прежде чем продолжить.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Неизвестно" }, - "swapVerifyTokenExplanation": { - "message": "Для обозначения нескольких токенов могут использоваться одно и то же имя и символ. Убедитесь, что $1 — это именно тот токен, который вы ищете.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 доступны для свопа", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% проскальзывания" }, - "swapsAdvancedOptions": { - "message": "Дополнительные параметры" - }, - "swapsExcessiveSlippageWarning": { - "message": "Величина проскальзывания очень велика. Сделка будет невыгодной. Снизьте допуск проскальзывания ниже 15%." - }, "swapsMaxSlippage": { "message": "Допуск проскальзывания" }, - "swapsNotEnoughForTx": { - "message": "Недостаточно $1 для выполнения этой транзакции", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Недостаточно $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Проверьте информацию о третьей стороне" }, - "verifyThisTokenOn": { - "message": "Проверить этот токен на $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Проверьте этот токен на $1 и убедитесь, что это тот токен, которым вы хотите торговать.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Версия" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 8e3d8fd7fdd0..61d8ff6e5d8c 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Handa kaming ipakita sa iyo ang mga pinakabagong quote kapag gusto mo ng magpatuloy" }, - "swapBuildQuotePlaceHolderText": { - "message": "Walang available na token na tumutugma sa $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin gamit ang iyong wallet na hardware" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Nagka-error sa pagkuha ng mga quote" }, - "swapFetchingTokens": { - "message": "Kinukuha ang mga token..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Mataas na slippage" }, - "swapHighSlippageWarning": { - "message": "Napakataas ng halaga ng slippage." - }, "swapIncludesMMFee": { "message": "Kasama ang $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Mababang slippage" }, - "swapLowSlippageError": { - "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." - }, "swapMaxSlippage": { "message": "Max na slippage" }, @@ -5344,9 +5331,6 @@ "message": "Deperensya ng presyo ng ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Ang price impact ay ang pagkakaiba sa pagitan ng kasalukuyang market price at ang halagang natanggap sa panahon ng pagpapatupad ng transaksyon. Ang price impact ay isang function ng laki ng iyong trade kaugnay sa laki ng pool ng liquidity." - }, "swapPriceUnavailableDescription": { "message": "Hindi matukoy ang price impact dahil sa kakulangan ng data ng market price. Pakikumpirma na komportable ka sa dami ng mga token na matatanggap mo bago mag-swap." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Humiling ng quotation" }, - "swapReviewSwap": { - "message": "I-review ang pag-swap" - }, - "swapSearchNameOrAddress": { - "message": "Hanapin ang pangalan o i-paste ang address" - }, "swapSelect": { "message": "Piliin" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mababang slippage" }, - "swapSlippageNegative": { - "message": "Ang slippage ay dapat mas malaki o katumbas ng zero" - }, "swapSlippageNegativeDescription": { "message": "Dapat na mas malaki o katumbas ng zero ang slippage" }, @@ -5502,20 +5477,6 @@ "message": "I-swap ang $1 sa $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Manwal na naidagdag ang token na ito." - }, - "swapTokenVerificationMessage": { - "message": "Palaging kumpirmahin ang token address sa $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Na-verify sa 1 pinagmulan lang." - }, - "swapTokenVerificationSources": { - "message": "Na-verify sa $1 na source.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "Na-verify $1 sa 1 pinagmulan lang. Pag-isipang i-verify ito sa $2 bago magpatuloy.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Hindi Alam" }, - "swapVerifyTokenExplanation": { - "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Available ang $1 $2 na i-swap", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0% Slippage" }, - "swapsAdvancedOptions": { - "message": "Mga Advanced na Opsyon" - }, - "swapsExcessiveSlippageWarning": { - "message": "Masyadong mataas ang halaga ng slippage at magreresulta sa masamang rate. Mangyaring bawasan ang iyong slippage tolerance sa halagang mas mababa sa 15%." - }, "swapsMaxSlippage": { "message": "Slippage tolerance" }, - "swapsNotEnoughForTx": { - "message": "Hindi sapat ang $1 para makumpleto ang transaksyong ito", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Hindi sapat ang $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "I-verify ang mga detalye ng third-party" }, - "verifyThisTokenOn": { - "message": "I-verify ang token na ito sa $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "I-verify ang token na ito sa $1 at siguruhin na ito ang token na gusto mong i-trade.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Bersyon" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 06d2f1de953f..d80d6564b880 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Devam etmek istediğinizde size en yeni kotaları göstermeye hazırız" }, - "swapBuildQuotePlaceHolderText": { - "message": "$1 ile eşleşen token yok", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Donanım cüzdanınızla onaylayın" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Teklifler alınırken hata" }, - "swapFetchingTokens": { - "message": "Tokenler alınıyor..." - }, "swapFromTo": { "message": "$1 ile $2 swap işlemi", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Yüksek kayma" }, - "swapHighSlippageWarning": { - "message": "Kayma tutarı çok yüksek." - }, "swapIncludesMMFee": { "message": "%$1 MetaMask ücreti dahildir.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Düşük kayma" }, - "swapLowSlippageError": { - "message": "İşlem başarısız olabilir, maks. kayma çok düşük." - }, "swapMaxSlippage": { "message": "Maks. kayma" }, @@ -5344,9 +5331,6 @@ "message": "~%$1 fiyat farkı", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Fiyat etkisi, mevcut piyasa fiyatı ile işlem gerçekleştirildiği sırada alınan tutar arasındaki farktır. Fiyat etkisi, likidite havuzunun boyutuna bağlı olarak işleminizin boyutunun bir fonksiyonudur." - }, "swapPriceUnavailableDescription": { "message": "Fiyat etkisi, piyasa fiyat verisinin mevcut olmaması nedeniyle belirlenememiştir. Swap işlemini gerçekleştirmeden önce lütfen almak üzere olduğunuz token tutarının sizin için uygun olduğunu onaylayın." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Teklif talebi" }, - "swapReviewSwap": { - "message": "Swap'ı incele" - }, - "swapSearchNameOrAddress": { - "message": "İsmi arayın veya adresi yapıştırın" - }, "swapSelect": { "message": "Seç" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Düşük kayma" }, - "swapSlippageNegative": { - "message": "Kayma en az sıfır olmalıdır" - }, "swapSlippageNegativeDescription": { "message": "Kayma en az sıfır olmalıdır" }, @@ -5502,20 +5477,6 @@ "message": "$1 ile $2 swap gerçekleştir", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Bu token manuel olarak eklendi." - }, - "swapTokenVerificationMessage": { - "message": "Her zaman token adresini $1 üzerinde onaylayın.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Sadece 1 kaynakta doğrulandı." - }, - "swapTokenVerificationSources": { - "message": "$1 kaynakta doğrulandı.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 sadece 1 kaynakta doğrulandı. İlerlemeden önce $2 üzerinde doğrulamayı deneyin.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Bilinmiyor" }, - "swapVerifyTokenExplanation": { - "message": "Birden fazla token aynı adı ve sembolü kullanabilir. Aradığınız tokenin bu olup olmadığını $1 alanında kontrol edin.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 için swap işlemi yapılabilir", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "%0 Kayma" }, - "swapsAdvancedOptions": { - "message": "Gelişmiş seçenekler" - }, - "swapsExcessiveSlippageWarning": { - "message": "Kayma tutarı çok yüksek ve kötü bir orana neden olacak. Lütfen kayma toleransınızı %15'in altında bir değere düşürün." - }, "swapsMaxSlippage": { "message": "Kayma toleransı" }, - "swapsNotEnoughForTx": { - "message": "Bu işlemi tamamlamak için yeterli $1 yok", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Yeterli $1 yok", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Üçüncü taraf bilgilerini doğrula" }, - "verifyThisTokenOn": { - "message": "Şurada bu tokeni doğrula: $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Bu tokeni $1 ile doğrulayın ve işlem yapmak istediğiniz tokenin bu olduğundan emin olun.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Sürüm" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 89772c1d4eec..0bac5423d1ee 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "Chúng tôi sẵn sàng cho bạn xem báo giá mới nhất khi bạn muốn tiếp tục" }, - "swapBuildQuotePlaceHolderText": { - "message": "Không có token nào khớp với $1", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "Xác nhận ví cứng của bạn" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "Lỗi tìm nạp báo giá" }, - "swapFetchingTokens": { - "message": "Đang tìm nạp token..." - }, "swapFromTo": { "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "Mức trượt giá cao" }, - "swapHighSlippageWarning": { - "message": "Số tiền trượt giá rất cao." - }, "swapIncludesMMFee": { "message": "Bao gồm $1% phí của MetaMask.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "Mức trượt giá thấp" }, - "swapLowSlippageError": { - "message": "Giao dịch có thể không thành công, mức trượt giá tối đa quá thấp." - }, "swapMaxSlippage": { "message": "Mức trượt giá tối đa" }, @@ -5344,9 +5331,6 @@ "message": "Chênh lệch giá ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "Tác động về giá là mức chênh lệch giữa giá thị trường hiện tại và số tiền nhận được trong quá trình thực hiện giao dịch. Tác động giá là một hàm trong quy mô giao dịch của bạn so với quy mô của nhóm thanh khoản." - }, "swapPriceUnavailableDescription": { "message": "Không thể xác định tác động giá do thiếu dữ liệu giá thị trường. Vui lòng xác nhận rằng bạn cảm thấy thoải mái với số lượng token bạn sắp nhận được trước khi hoán đổi." }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "Yêu cầu báo giá" }, - "swapReviewSwap": { - "message": "Xem lại giao dịch hoán đổi" - }, - "swapSearchNameOrAddress": { - "message": "Tìm kiếm tên hoặc dán địa chỉ" - }, "swapSelect": { "message": "Chọn" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "Mức trượt giá thấp" }, - "swapSlippageNegative": { - "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" - }, "swapSlippageNegativeDescription": { "message": "Mức trượt giá phải lớn hơn hoặc bằng 0" }, @@ -5502,20 +5477,6 @@ "message": "Hoán đổi $1 sang $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "Token này đã được thêm theo cách thủ công." - }, - "swapTokenVerificationMessage": { - "message": "Luôn xác nhận địa chỉ token trên $1.", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "Chỉ được xác minh trên 1 nguồn." - }, - "swapTokenVerificationSources": { - "message": "Đã xác minh trên $1 nguồn.", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 chỉ được xác minh trên 1 nguồn. Hãy xem xét xác minh nó trên $2 trước khi tiếp tục.", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "Không xác định" }, - "swapVerifyTokenExplanation": { - "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "Có sẵn $1 $2 để hoán đổi", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "Mức trượt giá 0%" }, - "swapsAdvancedOptions": { - "message": "Tùy chọn nâng cao" - }, - "swapsExcessiveSlippageWarning": { - "message": "Mức trượt giá quá cao và sẽ dẫn đến tỷ giá không sinh lời. Vui lòng giảm giới hạn trượt giá xuống một giá trị thấp hơn 15%." - }, "swapsMaxSlippage": { "message": "Giới hạn trượt giá" }, - "swapsNotEnoughForTx": { - "message": "Không đủ $1 để hoàn thành giao dịch này", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "Không đủ $1", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "Xác minh thông tin bên thứ ba" }, - "verifyThisTokenOn": { - "message": "Xác minh token này trên $1", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "Hãy xác minh token này trên $1 và đảm bảo đây là token bạn muốn giao dịch.", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "Phiên bản" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index b4816b165545..7209fb1c5b44 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -5192,10 +5192,6 @@ "swapAreYouStillThereDescription": { "message": "如果您想继续,我们准备好为您显示最新报价" }, - "swapBuildQuotePlaceHolderText": { - "message": "没有与 $1 匹配的代币", - "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" - }, "swapConfirmWithHwWallet": { "message": "使用您的硬件钱包确认" }, @@ -5260,9 +5256,6 @@ "swapFetchingQuotesErrorTitle": { "message": "获取报价出错" }, - "swapFetchingTokens": { - "message": "获取代币中……" - }, "swapFromTo": { "message": "$1 到 $2 的交换", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -5283,9 +5276,6 @@ "swapHighSlippage": { "message": "高滑点" }, - "swapHighSlippageWarning": { - "message": "滑点金额非常高。" - }, "swapIncludesMMFee": { "message": "包括 $1% 的 MetaMask 费用。", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -5307,9 +5297,6 @@ "swapLowSlippage": { "message": "低滑点" }, - "swapLowSlippageError": { - "message": "交易可能会失败,最大滑点过低。" - }, "swapMaxSlippage": { "message": "最大滑点" }, @@ -5344,9 +5331,6 @@ "message": "~$1% 的价差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceImpactTooltip": { - "message": "价格影响是当前市场价格与交易执行期间收到的金额之间的差异。价格影响是您的交易规模相对于流动性池规模的一个函数。" - }, "swapPriceUnavailableDescription": { "message": "由于缺乏市场价格数据,无法确定价格影响。在交换之前,请确认您对即将收到的代币数量感到满意。" }, @@ -5393,12 +5377,6 @@ "swapRequestForQuotation": { "message": "请求报价" }, - "swapReviewSwap": { - "message": "审查交换" - }, - "swapSearchNameOrAddress": { - "message": "搜索名称或粘贴地址" - }, "swapSelect": { "message": "选择" }, @@ -5431,9 +5409,6 @@ "swapSlippageLowTitle": { "message": "低滑点" }, - "swapSlippageNegative": { - "message": "滑点必须大于或等于0" - }, "swapSlippageNegativeDescription": { "message": "滑点必须大于或等于 0" }, @@ -5502,20 +5477,6 @@ "message": "用 $1 交换 $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, - "swapTokenVerificationAddedManually": { - "message": "此代币已手动添加。" - }, - "swapTokenVerificationMessage": { - "message": "始终在 $1 上确认代币地址。", - "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." - }, - "swapTokenVerificationOnlyOneSource": { - "message": "仅在1个来源上进行了验证。" - }, - "swapTokenVerificationSources": { - "message": "在 $1 个来源上进行了验证。", - "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." - }, "swapTokenVerifiedOn1SourceDescription": { "message": "$1 仅在 1 个源上进行了验证。在继续之前,考虑在 $2 上进行验证。", "description": "$1 is a token name, $2 points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" @@ -5536,30 +5497,12 @@ "swapUnknown": { "message": "未知" }, - "swapVerifyTokenExplanation": { - "message": "多个代币可以使用相同的名称和符号。检查 $1 以确认这是您正在寻找的代币。", - "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." - }, - "swapYourTokenBalance": { - "message": "$1 $2 可用于交换", - "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" - }, "swapZeroSlippage": { "message": "0%滑点" }, - "swapsAdvancedOptions": { - "message": "高级选项" - }, - "swapsExcessiveSlippageWarning": { - "message": "滑点金额太高,会导致不良率。请将最大滑点降低到低于15%的值。" - }, "swapsMaxSlippage": { "message": "最大滑点" }, - "swapsNotEnoughForTx": { - "message": "没有足够的 $1 来完成此交易", - "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" - }, "swapsNotEnoughToken": { "message": "$1 不足", "description": "Tells the user that they don't have enough of a token for a proposed swap. $1 is a token symbol" @@ -6055,14 +5998,6 @@ "verifyContractDetails": { "message": "验证第三方详情" }, - "verifyThisTokenOn": { - "message": "在 $1 上验证此代币", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, - "verifyThisUnconfirmedTokenOn": { - "message": "在 $1 上验证此代币,并确保这是您想要交易的代币。", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "version": { "message": "版本" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 32e98ed12288..7cdfa8e28add 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1211,9 +1211,6 @@ "supportCenter": { "message": "造訪我們的協助中心" }, - "swapSearchNameOrAddress": { - "message": "用名稱搜尋或貼上地址" - }, "switchEthereumChainConfirmationDescription": { "message": "這將在 MetaMask 中將目前選擇的網路切換到剛才新增的網路:" }, @@ -1373,10 +1370,6 @@ "userName": { "message": "使用者名稱" }, - "verifyThisTokenOn": { - "message": "在 $1 驗證這個代幣的資訊", - "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" - }, "viewAllDetails": { "message": "查看所有詳情" }, diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 107c1bd7ad14..17d68bd0e500 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -1497,10 +1497,6 @@ "ui/pages/swaps/awaiting-swap/swap-failure-icon.test.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.js", "ui/pages/swaps/awaiting-swap/swap-success-icon.test.js", - "ui/pages/swaps/build-quote/build-quote.js", - "ui/pages/swaps/build-quote/build-quote.stories.js", - "ui/pages/swaps/build-quote/build-quote.test.js", - "ui/pages/swaps/build-quote/index.js", "ui/pages/swaps/countdown-timer/countdown-timer.js", "ui/pages/swaps/countdown-timer/countdown-timer.stories.js", "ui/pages/swaps/countdown-timer/countdown-timer.test.js", @@ -1510,14 +1506,6 @@ "ui/pages/swaps/create-new-swap/create-new-swap.js", "ui/pages/swaps/create-new-swap/create-new-swap.test.js", "ui/pages/swaps/create-new-swap/index.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js", - "ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js", - "ui/pages/swaps/dropdown-input-pair/index.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js", - "ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js", - "ui/pages/swaps/dropdown-search-list/index.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.stories.js", "ui/pages/swaps/exchange-rate-display/exchange-rate-display.test.js", @@ -1542,12 +1530,6 @@ "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.test.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js", "ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.test.js", - "ui/pages/swaps/main-quote-summary/index.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js", - "ui/pages/swaps/main-quote-summary/main-quote-summary.test.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.js", - "ui/pages/swaps/main-quote-summary/quote-backdrop.test.js", "ui/pages/swaps/searchable-item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/index.js", "ui/pages/swaps/searchable-item-list/item-list/item-list.component.js", @@ -1568,10 +1550,6 @@ "ui/pages/swaps/select-quote-popover/sort-list/index.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.js", "ui/pages/swaps/select-quote-popover/sort-list/sort-list.test.js", - "ui/pages/swaps/slippage-buttons/index.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js", - "ui/pages/swaps/slippage-buttons/slippage-buttons.test.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.js", "ui/pages/swaps/smart-transaction-status/arrow-icon.test.js", "ui/pages/swaps/smart-transaction-status/canceled-icon.js", @@ -1597,11 +1575,6 @@ "ui/pages/swaps/view-on-block-explorer/index.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.js", "ui/pages/swaps/view-on-block-explorer/view-on-block-explorer.test.js", - "ui/pages/swaps/view-quote/index.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.js", - "ui/pages/swaps/view-quote/view-quote-price-difference.test.js", - "ui/pages/swaps/view-quote/view-quote.js", - "ui/pages/swaps/view-quote/view-quote.test.js", "ui/pages/token-details/index.js", "ui/pages/token-details/token-details-page.js", "ui/pages/token-details/token-details-page.test.js", diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 82c55c9bd7e0..2d9e50002a18 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -1209,10 +1209,6 @@ "extension_active": true, "mobile_active": true }, - "swapRedesign": { - "extensionActive": true, - "mobileActive": false - }, "zksync": { "extensionActive": true, "extension_active": true, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index a18f2e0b6944..55ffa7f9ba1b 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -397,10 +397,6 @@ export const createSwapsMockStore = () => { mobileActive: true, extensionActive: true, }, - swapRedesign: { - mobileActive: true, - extensionActive: true, - }, }, quotes: { TEST_AGG_1: { diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 0e1947d023f3..63bcdd2f58e6 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -28,7 +28,7 @@ import { import { I18nContext } from '../../../contexts/i18n'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF SEND_ROUTE, } from '../../../helpers/constants/routes'; @@ -270,10 +270,10 @@ const CoinButtons = ({ dispatch(setSwapsFromToken(defaultSwapsToken)); if (usingHardwareWallet) { if (global.platform.openExtensionInBrowser) { - global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); + global.platform.openExtensionInBrowser(PREPARE_SWAP_ROUTE); } } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } } ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js index 9d30874c6b7b..904862ae7bb8 100644 --- a/ui/components/multichain/app-header/app-header.js +++ b/ui/components/multichain/app-header/app-header.js @@ -9,7 +9,6 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { - BUILD_QUOTE_ROUTE, CONFIRM_TRANSACTION_ROUTE, SWAPS_ROUTE, } from '../../../helpers/constants/routes'; @@ -66,17 +65,13 @@ export const AppHeader = ({ location }) => { const isSwapsPage = Boolean( matchPath(location.pathname, { path: SWAPS_ROUTE, exact: false }), ); - const isSwapsBuildQuotePage = Boolean( - matchPath(location.pathname, { path: BUILD_QUOTE_ROUTE, exact: false }), - ); const unapprovedTransactions = useSelector(getUnapprovedTransactions); const hasUnapprovedTransactions = Object.keys(unapprovedTransactions).length > 0; - const disableAccountPicker = - isConfirmationPage || (isSwapsPage && !isSwapsBuildQuotePage); + const disableAccountPicker = isConfirmationPage || isSwapsPage; const disableNetworkPicker = isSwapsPage || diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index cf8348243238..ece4292fdf14 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -34,11 +34,11 @@ import { import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, - BUILD_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, SWAPS_MAINTENANCE_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../helpers/constants/routes'; import { fetchSwapsFeatureFlags, @@ -335,15 +335,6 @@ export const getCurrentSmartTransactionsEnabled = (state) => { return smartTransactionsEnabled && !currentSmartTransactionsError; }; -export const getSwapRedesignEnabled = (state) => { - const swapRedesign = - state.metamask.swapsState?.swapsFeatureFlags?.swapRedesign; - if (swapRedesign === undefined) { - return true; // By default show the redesign if we don't have feature flags returned yet. - } - return swapRedesign.extensionActive; -}; - export const getSwapsQuoteRefreshTime = (state) => state.metamask.swapsState.swapsQuoteRefreshTime; @@ -526,12 +517,12 @@ export { slice as swapsSlice, }; -export const navigateBackToBuildQuote = (history) => { +export const navigateBackToPrepareSwap = (history) => { return async (dispatch) => { // TODO: Ensure any fetch in progress is cancelled await dispatch(setBackgroundSwapRouteState('')); dispatch(navigatedBackToBuildQuote()); - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); }; }; diff --git a/ui/ducks/swaps/swaps.test.js b/ui/ducks/swaps/swaps.test.js index 83c133572c0d..fb2cc5e78a05 100644 --- a/ui/ducks/swaps/swaps.test.js +++ b/ui/ducks/swaps/swaps.test.js @@ -924,24 +924,5 @@ describe('Ducks - Swaps', () => { expect(newState.customGas.limit).toBe(null); }); }); - - describe('getSwapRedesignEnabled', () => { - it('returns true if feature flags are not returned from backend yet', () => { - const state = createSwapsMockStore(); - delete state.metamask.swapsState.swapsFeatureFlags.swapRedesign; - expect(swaps.getSwapRedesignEnabled(state)).toBe(true); - }); - - it('returns false if the extension feature flag for swaps redesign is false', () => { - const state = createSwapsMockStore(); - state.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = false; - expect(swaps.getSwapRedesignEnabled(state)).toBe(false); - }); - - it('returns true if the extension feature flag for swaps redesign is true', () => { - const state = createSwapsMockStore(); - expect(swaps.getSwapRedesignEnabled(state)).toBe(true); - }); - }); }); }); diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 5e4fffe413e2..bf38109ec9d7 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -241,12 +241,6 @@ PATH_NAME_MAP[PREPARE_SWAP_ROUTE] = 'Prepare Swap Page'; export const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; PATH_NAME_MAP[SWAPS_NOTIFICATION_ROUTE] = 'Swaps Notification Page'; -export const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; -PATH_NAME_MAP[BUILD_QUOTE_ROUTE] = 'Swaps Build Quote Page'; - -export const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; -PATH_NAME_MAP[VIEW_QUOTE_ROUTE] = 'Swaps View Quotes Page'; - export const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; PATH_NAME_MAP[LOADING_QUOTES_ROUTE] = 'Swaps Loading Quotes Page'; diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index bb3f129bade8..7921af85f2ce 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -6,7 +6,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { SEND_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, ///: END:ONLY_INCLUDE_IF } from '../../../helpers/constants/routes'; import { startNewDraftTransaction } from '../../../ducks/send'; @@ -276,12 +276,12 @@ const TokenButtons = ({ ); if (usingHardwareWallet) { global.platform.openExtensionInBrowser?.( - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, undefined, false, ); } else { - history.push(BUILD_QUOTE_ROUTE); + history.push(PREPARE_SWAP_ROUTE); } ///: END:ONLY_INCLUDE_IF }} diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index a73cfa370681..0d0d4c21c71f 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -64,8 +64,6 @@ describe('Bridge', () => { it('renders the component with initial props', async () => { const swapsMockStore = createBridgeMockStore({ extensionSupport: true }); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = - true; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider( diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 2df3f2907266..37c147427ac5 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -62,8 +62,7 @@ import { CONNECTED_ROUTE, CONNECTED_ACCOUNTS_ROUTE, AWAITING_SWAP_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -328,10 +327,8 @@ export default class Home extends PureComponent { const canRedirect = !isNotification && !stayOnHomePage; if (canRedirect && showAwaitingSwapScreen) { history.push(AWAITING_SWAP_ROUTE); - } else if (canRedirect && haveSwapsQuotes) { - history.push(VIEW_QUOTE_ROUTE); - } else if (canRedirect && swapsFetchParams) { - history.push(BUILD_QUOTE_ROUTE); + } else if (canRedirect && (haveSwapsQuotes || swapsFetchParams)) { + history.push(PREPARE_SWAP_ROUTE); } else if (firstPermissionsRequestId) { history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); } else if (pendingConfirmationsPrioritized.length > 0) { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 7cfd33f655ac..a27d59e3b33b 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -71,7 +71,6 @@ import { SWAPS_ROUTE, SETTINGS_ROUTE, UNLOCK_ROUTE, - BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, ONBOARDING_ROUTE, ONBOARDING_UNLOCK_ROUTE, @@ -493,13 +492,6 @@ export default class Routes extends Component { ); } - onSwapsBuildQuotePage() { - const { location } = this.props; - return Boolean( - matchPath(location.pathname, { path: BUILD_QUOTE_ROUTE, exact: false }), - ); - } - onHomeScreen() { const { location } = this.props; return location.pathname === DEFAULT_ROUTE; diff --git a/ui/pages/swaps/__snapshots__/index.test.js.snap b/ui/pages/swaps/__snapshots__/index.test.js.snap index 779bb78555d5..c7a58c20dac8 100644 --- a/ui/pages/swaps/__snapshots__/index.test.js.snap +++ b/ui/pages/swaps/__snapshots__/index.test.js.snap @@ -12,17 +12,29 @@ exports[`Swap renders the component with initial props 1`] = ` class="swaps__header" >

+ class="box box--margin-left-4 box--display-flex box--flex-direction-row box--justify-content-center box--width-1/12" + tabindex="0" + > + +
Swap
- Cancel +
await dispatch(navigateBackToBuildQuote(history))} + onCancel={async () => + await dispatch(navigateBackToPrepareSwap(history)) + } submitText={submitText} disabled={submittingSwap} hideCancel={errorKey !== QUOTES_EXPIRED_ERROR} diff --git a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap b/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap deleted file mode 100644 index b0551966d1c6..000000000000 --- a/ui/pages/swaps/build-quote/__snapshots__/build-quote.test.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BuildQuote renders the component with initial props 1`] = ` -
- - - -
-`; diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js deleted file mode 100644 index 23ca35b3f2e9..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.js +++ /dev/null @@ -1,800 +0,0 @@ -import React, { useContext, useEffect, useState, useCallback } from 'react'; -import BigNumber from 'bignumber.js'; -import PropTypes from 'prop-types'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import classnames from 'classnames'; -import { uniqBy, isEqual } from 'lodash'; -import { useHistory } from 'react-router-dom'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - useTokensToSearch, - getRenderableTokenData, -} from '../../../hooks/useTokensToSearch'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { I18nContext } from '../../../contexts/i18n'; -import DropdownInputPair from '../dropdown-input-pair'; -import DropdownSearchList from '../dropdown-search-list'; -import SlippageButtons from '../slippage-buttons'; -import { getTokens, getConversionRate } from '../../../ducks/metamask/metamask'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - VIEW_QUOTE_ROUTE, - LOADING_QUOTES_ROUTE, -} from '../../../helpers/constants/routes'; - -import { - fetchQuotesAndSetQuoteState, - setSwapsFromToken, - setSwapToToken, - getFromToken, - getToToken, - getBalanceError, - getTopAssets, - getFetchParams, - getQuotes, - setBalanceError, - setFromTokenInputValue, - setFromTokenError, - setMaxSlippage, - setReviewSwapClickedTimestamp, - getCurrentSmartTransactionsEnabled, - getFromTokenInputValue, - getFromTokenError, - getMaxSlippage, - getIsFeatureFlagLoaded, - getSmartTransactionFees, - getLatestAddedTokenTo, -} from '../../../ducks/swaps/swaps'; -import { - getSwapsDefaultToken, - getTokenExchangeRates, - getCurrentCurrency, - getCurrentChainId, - getRpcPrefsForCurrentProvider, - getTokenList, - isHardwareWallet, - getHardwareWalletType, - getUseCurrencyRateCheck, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; - -import { getURLHostName } from '../../../helpers/utils/util'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; - -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../../../../shared/modules/swaps.utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventLinkType, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; -import { - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - TokenBucketPriority, - MAX_ALLOWED_SLIPPAGE, -} from '../../../../shared/constants/swaps'; - -import { - resetSwapsPostFetchState, - ignoreTokens, - setBackgroundSwapRouteState, - clearSwapsQuotes, - stopPollingForQuotes, - clearSmartTransactionFees, -} from '../../../store/actions'; -import { countDecimals, fetchTokenPrice } from '../swaps.util'; -import SwapsFooter from '../swaps-footer'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; -import { fetchTokenBalance } from '../../../../shared/lib/token-util'; -import { shouldEnableDirectWrapping } from '../../../../shared/lib/swaps-utils'; -import { - getValueFromWeiHex, - hexToDecimal, -} from '../../../../shared/modules/conversion.utils'; - -const fuseSearchKeys = [ - { name: 'name', weight: 0.499 }, - { name: 'symbol', weight: 0.499 }, - { name: 'address', weight: 0.002 }, -]; - -let timeoutIdForQuotesPrefetching; - -export default function BuildQuote({ - ethBalance, - selectedAccountAddress, - shuffledTokensList, -}) { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - const trackEvent = useContext(MetaMetricsContext); - - const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = - useState(undefined); - const [verificationClicked, setVerificationClicked] = useState(false); - - const isFeatureFlagLoaded = useSelector(getIsFeatureFlagLoaded); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const { sourceTokenInfo = {}, destinationTokenInfo = {} } = - fetchParams?.metaData || {}; - const tokens = useSelector(getTokens, isEqual); - const topAssets = useSelector(getTopAssets, isEqual); - const fromToken = useSelector(getFromToken, isEqual); - const fromTokenInputValue = useSelector(getFromTokenInputValue); - const fromTokenError = useSelector(getFromTokenError); - const maxSlippage = useSelector(getMaxSlippage); - const toToken = useSelector(getToToken, isEqual) || destinationTokenInfo; - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); - const tokenList = useSelector(getTokenList, isEqual); - const quotes = useSelector(getQuotes, isEqual); - const areQuotesPresent = Object.keys(quotes).length > 0; - const latestAddedTokenTo = useSelector(getLatestAddedTokenTo, isEqual); - - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const conversionRate = useSelector(getConversionRate); - const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const smartTransactionFees = useSelector(getSmartTransactionFees); - const currentCurrency = useSelector(getCurrentCurrency); - - const fetchParamsFromToken = isSwapsDefaultTokenSymbol( - sourceTokenInfo?.symbol, - chainId, - ) - ? defaultSwapsToken - : sourceTokenInfo; - - const { loading, tokensWithBalances } = useTokenTracker({ tokens }); - - // If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance - // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that - // the balance of the token can appear in the from token selection dropdown - const fromTokenArray = - !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance - ? [fromToken] - : []; - const usersTokens = uniqBy( - [...tokensWithBalances, ...tokens, ...fromTokenArray], - 'address', - ); - const memoizedUsersTokens = useEqualityCheck(usersTokens); - - const selectedFromToken = getRenderableTokenData( - fromToken || fetchParamsFromToken, - tokenConversionRates, - conversionRate, - currentCurrency, - chainId, - tokenList, - ); - - const tokensToSearchSwapFrom = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.owned, - }); - const tokensToSearchSwapTo = useTokensToSearch({ - usersTokens: memoizedUsersTokens, - topTokens: topAssets, - shuffledTokensList, - tokenBucketPriority: TokenBucketPriority.top, - }); - const selectedToToken = - tokensToSearchSwapFrom.find(({ address }) => - isEqualCaseInsensitive(address, toToken?.address), - ) || toToken; - const toTokenIsNotDefault = - selectedToToken?.address && - !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); - const occurrences = Number( - selectedToToken?.occurances || selectedToToken?.occurrences || 0, - ); - const { - address: fromTokenAddress, - symbol: fromTokenSymbol, - string: fromTokenString, - decimals: fromTokenDecimals, - balance: rawFromTokenBalance, - } = selectedFromToken || {}; - const { address: toTokenAddress } = selectedToToken || {}; - - const fromTokenBalance = - rawFromTokenBalance && - calcTokenAmount(rawFromTokenBalance, fromTokenDecimals).toString(10); - - const prevFromTokenBalance = usePrevious(fromTokenBalance); - - const swapFromTokenFiatValue = useTokenFiatAmount( - fromTokenAddress, - fromTokenInputValue || 0, - fromTokenSymbol, - { - showFiat: useCurrencyRateCheck, - }, - true, - ); - const swapFromEthFiatValue = useEthFiatAmount( - fromTokenInputValue || 0, - { showFiat: useCurrencyRateCheck }, - true, - ); - const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) - ? swapFromEthFiatValue - : swapFromTokenFiatValue; - - const onInputChange = useCallback( - (newInputValue, balance) => { - dispatch(setFromTokenInputValue(newInputValue)); - const newBalanceError = new BigNumber(newInputValue || 0).gt( - balance || 0, - ); - // "setBalanceError" is just a warning, a user can still click on the "Review swap" button. - if (balanceError !== newBalanceError) { - dispatch(setBalanceError(newBalanceError)); - } - dispatch( - setFromTokenError( - fromToken && countDecimals(newInputValue) > fromToken.decimals - ? 'tooManyDecimals' - : null, - ), - ); - }, - [dispatch, fromToken, balanceError], - ); - - const onFromSelect = (token) => { - if ( - token?.address && - !swapFromFiatValue && - fetchedTokenExchangeRate !== null - ) { - fetchTokenPrice(token.address).then((rate) => { - if (rate !== null && rate !== undefined) { - setFetchedTokenExchangeRate(rate); - } - }); - } else { - setFetchedTokenExchangeRate(null); - } - if ( - token?.address && - !memoizedUsersTokens.find((usersToken) => - isEqualCaseInsensitive(usersToken.address, token.address), - ) - ) { - fetchTokenBalance( - token.address, - selectedAccountAddress, - global.ethereumProvider, - ).then((fetchedBalance) => { - if (fetchedBalance?.balance) { - const balanceAsDecString = fetchedBalance.balance.toString(10); - const userTokenBalance = calcTokenAmount( - balanceAsDecString, - token.decimals, - ); - dispatch( - setSwapsFromToken({ - ...token, - string: userTokenBalance.toString(10), - balance: balanceAsDecString, - }), - ); - } - }); - } - dispatch(setSwapsFromToken(token)); - onInputChange( - token?.address ? fromTokenInputValue : '', - token.string, - token.decimals, - ); - }; - - const blockExplorerTokenLink = getTokenTrackerLink( - selectedToToken.address, - chainId, - null, // no networkId - null, // no holderAddress - { - blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, - }, - ); - - const blockExplorerLabel = rpcPrefs.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) - : t('etherscan'); - - const { address: toAddress } = toToken || {}; - const onToSelect = useCallback( - (token) => { - if (latestAddedTokenTo && token.address !== toAddress) { - dispatch( - ignoreTokens({ - tokensToIgnore: toAddress, - dontShowLoadingIndicator: true, - }), - ); - } - dispatch(setSwapToToken(token)); - setVerificationClicked(false); - }, - [dispatch, latestAddedTokenTo, toAddress], - ); - - const hideDropdownItemIf = useCallback( - (item) => isEqualCaseInsensitive(item.address, fromTokenAddress), - [fromTokenAddress], - ); - - const tokensWithBalancesFromToken = tokensWithBalances.find((token) => - isEqualCaseInsensitive(token.address, fromToken?.address), - ); - const previousTokensWithBalancesFromToken = usePrevious( - tokensWithBalancesFromToken, - ); - - useEffect(() => { - const notDefault = !isSwapsDefaultTokenAddress( - tokensWithBalancesFromToken?.address, - chainId, - ); - const addressesAreTheSame = isEqualCaseInsensitive( - tokensWithBalancesFromToken?.address, - previousTokensWithBalancesFromToken?.address, - ); - const balanceHasChanged = - tokensWithBalancesFromToken?.balance !== - previousTokensWithBalancesFromToken?.balance; - if (notDefault && addressesAreTheSame && balanceHasChanged) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: tokensWithBalancesFromToken?.balance, - string: tokensWithBalancesFromToken?.string, - }), - ); - } - }, [ - dispatch, - tokensWithBalancesFromToken, - previousTokensWithBalancesFromToken, - fromToken, - chainId, - ]); - - // If the eth balance changes while on build quote, we update the selected from token - useEffect(() => { - if ( - isSwapsDefaultTokenAddress(fromToken?.address, chainId) && - fromToken?.balance !== hexToDecimal(ethBalance) - ) { - dispatch( - setSwapsFromToken({ - ...fromToken, - balance: hexToDecimal(ethBalance), - string: getValueFromWeiHex({ - value: ethBalance, - numberOfDecimals: 4, - toDenomination: 'ETH', - }), - }), - ); - } - }, [dispatch, fromToken, ethBalance, chainId]); - - useEffect(() => { - if (prevFromTokenBalance !== fromTokenBalance) { - onInputChange(fromTokenInputValue, fromTokenBalance); - } - }, [ - onInputChange, - prevFromTokenBalance, - fromTokenInputValue, - fromTokenBalance, - ]); - - const trackBuildQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'Build Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - }, [ - trackEvent, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - useEffect(() => { - dispatch(resetSwapsPostFetchState()); - dispatch(setReviewSwapClickedTimestamp()); - trackBuildQuotePageLoadedEvent(); - }, [dispatch, trackBuildQuotePageLoadedEvent]); - - useEffect(() => { - if (smartTransactionsEnabled && smartTransactionFees?.tradeTxFees) { - // We want to clear STX fees, because we only want to use fresh ones on the View Quote page. - clearSmartTransactionFees(); - } - }, [smartTransactionsEnabled, smartTransactionFees]); - - const BlockExplorerLink = () => { - return ( - { - /* istanbul ignore next */ - trackEvent({ - event: MetaMetricsEventName.ExternalLinkClicked, - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: MetaMetricsEventLinkType.TokenTracker, - location: 'Swaps Confirmation', - url_domain: getURLHostName(blockExplorerTokenLink), - }, - }); - global.platform.openTab({ - url: blockExplorerTokenLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerLabel} - - ); - }; - - let tokenVerificationDescription = ''; - if (blockExplorerTokenLink) { - if (occurrences === 1) { - tokenVerificationDescription = t('verifyThisTokenOn', [ - , - ]); - } else if (occurrences === 0) { - tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [ - , - ]); - } - } - - const swapYourTokenBalance = t('swapYourTokenBalance', [ - fromTokenString || '0', - fromTokenSymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol || '', - ]); - - const isDirectWrappingEnabled = shouldEnableDirectWrapping( - chainId, - fromTokenAddress, - selectedToToken.address, - ); - const isReviewSwapButtonDisabled = - fromTokenError || - !isFeatureFlagLoaded || - !Number(fromTokenInputValue) || - !selectedToToken?.address || - !fromTokenAddress || - Number(maxSlippage) < 0 || - Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || - (toTokenIsNotDefault && occurrences < 2 && !verificationClicked); - - // It's triggered every time there is a change in form values (token from, token to, amount and slippage). - useEffect(() => { - dispatch(clearSwapsQuotes()); - dispatch(stopPollingForQuotes()); - const prefetchQuotesWithoutRedirecting = async () => { - const pageRedirectionDisabled = true; - await dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - pageRedirectionDisabled, - ), - ); - }; - // Delay fetching quotes until a user is done typing an input value. If they type a new char in less than a second, - // we will cancel previous setTimeout call and start running a new one. - timeoutIdForQuotesPrefetching = setTimeout(() => { - timeoutIdForQuotesPrefetching = null; - if (!isReviewSwapButtonDisabled) { - // Only do quotes prefetching if the Review swap button is enabled. - prefetchQuotesWithoutRedirecting(); - } - }, 1000); - return () => clearTimeout(timeoutIdForQuotesPrefetching); - }, [ - dispatch, - history, - maxSlippage, - trackEvent, - isReviewSwapButtonDisabled, - fromTokenInputValue, - fromTokenAddress, - toTokenAddress, - smartTransactionsOptInStatus, - ]); - - return ( -
-
-
-
{t('swapSwapFrom')}
- {!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && ( -
- onInputChange(fromTokenBalance || '0', fromTokenBalance) - } - > - {t('max')} -
- )} -
- { - /* istanbul ignore next */ - onInputChange(value, fromTokenBalance); - }} - inputValue={fromTokenInputValue} - leftValue={fromTokenInputValue && swapFromFiatValue} - selectedItem={selectedFromToken} - maxListItems={30} - loading={ - loading && - (!tokensToSearchSwapFrom?.length || - !topAssets || - !Object.keys(topAssets).length) - } - selectPlaceHolderText={t('swapSelect')} - hideItemIf={(item) => - isEqualCaseInsensitive(item.address, selectedToToken?.address) - } - listContainerClassName="build-quote__open-dropdown" - autoFocus - /> -
- {!fromTokenError && - !balanceError && - fromTokenSymbol && - swapYourTokenBalance} - {!fromTokenError && balanceError && fromTokenSymbol && ( -
-
- {t('swapsNotEnoughForTx', [fromTokenSymbol])} -
-
- {swapYourTokenBalance} -
-
- )} - {fromTokenError && ( - <> -
- {t('swapTooManyDecimalsError', [ - fromTokenSymbol, - fromTokenDecimals, - ])} -
-
{swapYourTokenBalance}
- - )} -
-
- -
-
-
{t('swapSwapTo')}
-
-
- -
- {toTokenIsNotDefault && - (occurrences < 2 ? ( - -
- {occurrences === 1 - ? t('swapTokenVerificationOnlyOneSource') - : t('swapTokenVerificationAddedManually')} -
-
{tokenVerificationDescription}
-
- } - primaryAction={ - /* istanbul ignore next */ - verificationClicked - ? null - : { - label: t('continue'), - onClick: () => setVerificationClicked(true), - } - } - withRightButton - infoTooltipText={ - blockExplorerTokenLink && - t('swapVerifyTokenExplanation', [blockExplorerLabel]) - } - /> - ) : ( - - ))} - {!isDirectWrappingEnabled && ( -
- { - dispatch(setMaxSlippage(newSlippage)); - }} - maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE} - currentSlippage={maxSlippage} - isDirectWrappingEnabled={isDirectWrappingEnabled} - /> -
- )} -
- { - // We need this to know how long it took to go from clicking on the Review swap button to rendered View Quote page. - dispatch(setReviewSwapClickedTimestamp(Date.now())); - // In case that quotes prefetching is waiting to be executed, but hasn't started yet, - // we want to cancel it and fetch quotes from here. - if (timeoutIdForQuotesPrefetching) { - clearTimeout(timeoutIdForQuotesPrefetching); - dispatch( - fetchQuotesAndSetQuoteState( - history, - fromTokenInputValue, - maxSlippage, - trackEvent, - ), - ); - } else if (areQuotesPresent) { - // If there are prefetched quotes already, go directly to the View Quote page. - history.push(VIEW_QUOTE_ROUTE); - } else { - // If the "Review swap" button was clicked while quotes are being fetched, go to the Loading Quotes page. - await dispatch(setBackgroundSwapRouteState('loading')); - history.push(LOADING_QUOTES_ROUTE); - } - } - } - submitText={t('swapReviewSwap')} - disabled={isReviewSwapButtonDisabled} - hideCancel - showTermsOfService - /> -
- ); -} - -BuildQuote.propTypes = { - ethBalance: PropTypes.string, - selectedAccountAddress: PropTypes.string, - shuffledTokensList: PropTypes.array, -}; diff --git a/ui/pages/swaps/build-quote/build-quote.stories.js b/ui/pages/swaps/build-quote/build-quote.stories.js deleted file mode 100644 index 008b4b4a3ed4..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.stories.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { shuffle } from 'lodash'; -import testData from '../../../../.storybook/test-data'; -import BuildQuote from './build-quote'; - -const tokenValuesArr = shuffle(testData.metamask.tokenList); - -export default { - title: 'Pages/Swaps/BuildQuote', - - argTypes: { - ethBalance: { - control: { type: 'text' }, - }, - selectedAccountAddress: { - control: { type: 'text' }, - }, - shuffledTokensList: { control: 'object' }, - }, - args: { - ethBalance: '0x8', - selectedAccountAddress: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - shuffledTokensList: tokenValuesArr, - }, -}; - -export const DefaultStory = (args) => { - return ( - <> - - - ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/build-quote/build-quote.test.js b/ui/pages/swaps/build-quote/build-quote.test.js deleted file mode 100644 index aa8738e43ecb..000000000000 --- a/ui/pages/swaps/build-quote/build-quote.test.js +++ /dev/null @@ -1,223 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import { createTestProviderTools } from '../../../../test/stub/provider'; -import { - setSwapsFromToken, - setSwapToToken, - setFromTokenInputValue, -} from '../../../ducks/swaps/swaps'; -import { mockNetworkState } from '../../../../test/stub/networks'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import BuildQuote from '.'; - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - ethBalance: '0x8', - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - shuffledTokensList: [], - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - ignoreTokens: jest.fn(), - setBackgroundSwapRouteState: jest.fn(), - clearSwapsQuotes: jest.fn(), - stopPollingForQuotes: jest.fn(), - clearSmartTransactionFees: jest.fn(), - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(), -}); - -jest.mock('../../../ducks/swaps/swaps', () => { - const actual = jest.requireActual('../../../ducks/swaps/swaps'); - return { - ...actual, - setSwapsFromToken: jest.fn(), - setSwapToToken: jest.fn(), - setFromTokenInputValue: jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }), - }; -}); - -jest.mock('../swaps.util', () => { - const actual = jest.requireActual('../swaps.util'); - return { - ...actual, - fetchTokenBalance: jest.fn(() => Promise.resolve()), - fetchTokenPrice: jest.fn(() => Promise.resolve()), - }; -}); - -const providerResultStub = { - eth_getCode: '0x123', - eth_call: - '0x00000000000000000000000000000000000000000000000029a2241af62c0000', -}; -const { provider } = createTestProviderTools({ - scaffold: providerResultStub, - networkId: '5', - chainId: '5', -}); - -describe('BuildQuote', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - - beforeEach(() => { - global.ethereumProvider = provider; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText } = renderWithProvider(, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Swap to')).toBeInTheDocument(); - expect(getByText('Select')).toBeInTheDocument(); - expect(getByText('Slippage tolerance')).toBeInTheDocument(); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('Review swap')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - }); - - it('switches swap from and to tokens', () => { - const setSwapFromTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const setSwapToTokenMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setSwapToToken.mockImplementation(setSwapToTokenMock); - const mockStore = createSwapsMockStore(); - const store = configureMockStore(middleware)(mockStore); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - , - store, - ); - expect(getByText('Swap from')).toBeInTheDocument(); - fireEvent.click(getByTestId('build-quote__swap-arrows')); - expect(setSwapsFromToken).toHaveBeenCalledWith(mockStore.swaps.toToken); - expect(setSwapToToken).toHaveBeenCalled(); - }); - - it('renders the block explorer link, only 1 verified source', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect(getByText('Only verified on 1 source.')).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('renders the block explorer link, 0 verified sources', () => { - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 0; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(, store); - expect(getByText('Swap from')).toBeInTheDocument(); - expect( - getByText('This token has been added manually.'), - ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); - }); - - it('clicks on a block explorer link', () => { - global.platform = { openTab: jest.fn() }; - const mockStore = createSwapsMockStore(); - mockStore.swaps.toToken.occurances = 1; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(, store); - const blockExplorer = getByText('etherscan.io'); - expect(blockExplorer).toBeInTheDocument(); - fireEvent.click(blockExplorer); - expect(global.platform.openTab).toHaveBeenCalledWith({ - url: 'https://etherscan.io/token/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - }); - }); - - it('clicks on the "max" link', () => { - const setFromTokenInputValueMock = jest.fn(() => { - return { - type: 'MOCK_ACTION', - }; - }); - setFromTokenInputValue.mockImplementation(setFromTokenInputValueMock); - const mockStore = createSwapsMockStore(); - mockStore.swaps.fromToken = 'DAI'; - const store = configureMockStore(middleware)({ - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - blockExplorerUrl: 'https://etherscan.io', - }), - }, - }); - const props = createProps(); - const { getByText } = renderWithProvider(, store); - const maxLink = getByText('Max'); - fireEvent.click(maxLink); - expect(setFromTokenInputValue).toHaveBeenCalled(); - }); -}); diff --git a/ui/pages/swaps/build-quote/index.js b/ui/pages/swaps/build-quote/index.js deleted file mode 100644 index 772229f2a187..000000000000 --- a/ui/pages/swaps/build-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './build-quote'; diff --git a/ui/pages/swaps/build-quote/index.scss b/ui/pages/swaps/build-quote/index.scss deleted file mode 100644 index 5d454002a0b8..000000000000 --- a/ui/pages/swaps/build-quote/index.scss +++ /dev/null @@ -1,223 +0,0 @@ -@use "design-system"; - -.build-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - padding-top: 4px; - - &__content { - display: flex; - height: 100%; - flex-direction: column; - padding-left: 24px; - padding-right: 24px; - } - - &__content { - display: flex; - } - - &__dropdown-swap-to-header { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: 0; - margin-bottom: 12px; - } - - &__dropdown-input-pair-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - width: 100%; - margin-bottom: 12px; - flex: 0.5 1 auto; - max-height: 56px; - } - - &__title, - &__input-label { - @include design-system.H5; - - font-weight: bold; - color: var(--color-text-default); - margin-top: 3px; - } - - &__swap-arrows-row { - width: 100%; - display: flex; - justify-content: flex-end; - padding-right: 16px; - padding-top: 12px; - height: 24px; - position: relative; - } - - &__swap-arrows { - display: flex; - flex: 0 0 auto; - height: 24px; - cursor: pointer; - background: unset; - color: var(--color-icon-muted); - } - - &__max-button { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - } - - &__balance-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-muted); - margin-top: 4px; - display: flex; - flex-flow: column; - height: 18px; - - &--error { - div:first-of-type { - font-weight: bold; - color: var(--color-text-default); - } - - .build-quote__form-error:first-of-type { - font-weight: bold; - color: var(--color-error-default); - } - - div:last-of-type { - font-weight: normal; - color: var(--color-text-alternative); - } - } - } - - &__slippage-buttons-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 32px; - } - - &__open-dropdown, - &__open-to-dropdown { - max-height: 330px; - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - position: absolute; - width: 100%; - } - - .dropdown-input-pair { - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 1px solid var(--color-border-default); - border-left: 0; - } - } - } - - &__open-to-dropdown { - max-height: 194px; - - @include design-system.screen-sm-min { - max-height: 276px; - } - } - - &__token-message { - @include design-system.H7; - - width: 100%; - color: var(--color-text-alternative); - margin-top: 4px; - - .info-tooltip { - display: inline-block; - } - } - - &__token-etherscan-link { - color: var(--color-primary-default); - cursor: pointer; - } - - &__token-tooltip-container { - // Needed to override the style property added by the react-tippy library - display: flex !important; - } - - &__bold { - font-weight: bold; - } - - &__underline { - text-decoration: underline; - } - - /* Prevents the swaps "Swap to" field from overflowing */ - .dropdown-input-pair__to .dropdown-search-list { - width: 100%; - } -} - -@keyframes slide-in { - 100% { transform: translateY(0%); } -} - -.smart-transactions-popover { - transform: translateY(-100%); - animation: slide-in 0.5s forwards; - - &__content { - flex-direction: column; - - ul { - list-style: inside; - } - - a { - color: var(--color-primary-default); - cursor: pointer; - } - } - - &__footer { - flex-direction: column; - flex: 1; - align-items: center; - border-top: 0; - - button { - border-radius: 50px; - } - - a { - font-size: inherit; - padding-bottom: 0; - } - } -} diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.js b/ui/pages/swaps/create-new-swap/create-new-swap.js index 3f19b68631b2..6d7963e36ca8 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.js @@ -9,7 +9,7 @@ import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, setSwapsFromToken, } from '../../../ducks/swaps/swaps'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; @@ -32,7 +32,7 @@ export default function CreateNewSwap({ sensitiveTrackingProperties }) { sensitiveProperties: sensitiveTrackingProperties, }); history.push(DEFAULT_ROUTE); // It cleans up Swaps state. - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); dispatch(setSwapsFromToken(defaultSwapsToken)); }} > diff --git a/ui/pages/swaps/create-new-swap/create-new-swap.test.js b/ui/pages/swaps/create-new-swap/create-new-swap.test.js index 0ce6fa400150..86accf04da23 100644 --- a/ui/pages/swaps/create-new-swap/create-new-swap.test.js +++ b/ui/pages/swaps/create-new-swap/create-new-swap.test.js @@ -10,7 +10,7 @@ import { } from '../../../../test/jest'; import { setSwapsFromToken, - navigateBackToBuildQuote, + navigateBackToPrepareSwap, } from '../../../ducks/swaps/swaps'; import CreateNewSwap from '.'; @@ -23,7 +23,7 @@ const createProps = (customProps = {}) => { }; const backgroundConnection = { - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), setBackgroundSwapRouteState: jest.fn(), navigatedBackToBuildQuote: jest.fn(), }; @@ -35,7 +35,7 @@ jest.mock('../../../ducks/swaps/swaps', () => { return { ...actual, setSwapsFromToken: jest.fn(), - navigateBackToBuildQuote: jest.fn(), + navigateBackToPrepareSwap: jest.fn(), }; }); @@ -63,12 +63,12 @@ describe('CreateNewSwap', () => { }; }); setSwapsFromToken.mockImplementation(setSwapFromTokenMock); - const navigateBackToBuildQuoteMock = jest.fn(() => { + const navigateBackToPrepareSwapMock = jest.fn(() => { return { type: 'MOCK_ACTION', }; }); - navigateBackToBuildQuote.mockImplementation(navigateBackToBuildQuoteMock); + navigateBackToPrepareSwap.mockImplementation(navigateBackToPrepareSwapMock); const store = configureMockStore(middleware)(createSwapsMockStore()); const { getByText } = renderWithProvider( @@ -77,6 +77,6 @@ describe('CreateNewSwap', () => { ); await fireEvent.click(getByText('Create a new swap')); expect(setSwapFromTokenMock).toHaveBeenCalledTimes(1); - expect(navigateBackToBuildQuoteMock).toHaveBeenCalledTimes(1); + expect(navigateBackToPrepareSwapMock).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/swaps/dropdown-input-pair/README.mdx b/ui/pages/swaps/dropdown-input-pair/README.mdx deleted file mode 100644 index cac84e714daa..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/README.mdx +++ /dev/null @@ -1,15 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; - -import DropdownInputPair from '.'; - -# Dropdown Input Pair - -Dropdown to choose cryptocurrency with amount input field. - - - - - -## Props - - diff --git a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap deleted file mode 100644 index d58907ed2684..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownInputPair renders the component with initial props 1`] = ` - -`; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js deleted file mode 100644 index 9da47ddf80ea..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.js +++ /dev/null @@ -1,177 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import DropdownSearchList from '../dropdown-search-list'; -import TextField from '../../../components/ui/text-field'; - -const characterWidthMap = { - 1: 5.86, - 2: 10.05, - 3: 10.45, - 4: 11.1, - 5: 10, - 6: 10.06, - 7: 9.17, - 8: 10.28, - 9: 10.06, - 0: 11.22, - '.': 4.55, -}; - -const getInputWidth = (value) => { - const valueString = String(value); - const charArray = valueString.split(''); - return charArray.reduce( - (inputWidth, _char) => inputWidth + characterWidthMap[_char], - 12, - ); -}; -export default function DropdownInputPair({ - itemsToSearch = [], - onInputChange, - inputValue = '', - onSelect, - leftValue, - selectedItem, - SearchListPlaceholder, - maxListItems, - selectPlaceHolderText, - loading, - hideItemIf, - listContainerClassName, - autoFocus, -}) { - const [isOpen, setIsOpen] = useState(false); - const open = () => setIsOpen(true); - const close = () => setIsOpen(false); - const inputRef = useRef(); - const onTextFieldChange = (event) => { - event.stopPropagation(); - // Automatically prefix value with 0. if user begins typing . - const valueToUse = event.target.value === '.' ? '0.' : event.target.value; - - // Regex that validates strings with only numbers, 'x.', '.x', and 'x.x' - const regexp = /^(\.\d+|\d+(\.\d+)?|\d+\.)$/u; - // If the value is either empty or contains only numbers and '.' and only has one '.', update input to match - if (valueToUse === '' || regexp.test(valueToUse)) { - onInputChange(valueToUse); - } else { - // otherwise, use the previously set inputValue (effectively denying the user from inputting the last char) - // or an empty string if we do not yet have an inputValue - onInputChange(inputValue || ''); - } - }; - const [applyTwoLineStyle, setApplyTwoLineStyle] = useState(null); - useEffect(() => { - setApplyTwoLineStyle( - (inputRef?.current?.getBoundingClientRect()?.width || 0) + - getInputWidth(inputValue || '') > - 137, - ); - }, [inputValue, inputRef]); - - return ( -
- - {!isOpen && ( - - )} - {!isOpen && leftValue && ( -
- ≈ {leftValue} -
- )} -
- ); -} - -DropdownInputPair.propTypes = { - /** - * Give items data for the component - */ - itemsToSearch: PropTypes.array, - /** - * Handler for input change - */ - onInputChange: PropTypes.func, - /** - * Show input value content - */ - inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * Handler for onSelect - */ - onSelect: PropTypes.func, - /** - * Set value to left - */ - leftValue: PropTypes.string, - /** - * Show selected item - */ - selectedItem: PropTypes.object, - /** - * Doesn't look like this is used - */ - SearchListPlaceholder: PropTypes.func, - /** - * Define maximum item per list - */ - maxListItems: PropTypes.number, - /** - * Show select placeholder text - */ - selectPlaceHolderText: PropTypes.string, - /** - * Check if the component is loading - */ - loading: PropTypes.bool, - /** - * Handler for hide item - */ - hideItemIf: PropTypes.func, - /** - * Add custom CSS class for list container - */ - listContainerClassName: PropTypes.string, - /** - * Check if the component is auto focus - */ - autoFocus: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js deleted file mode 100644 index ff5a6c756c1b..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.stories.js +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import { useArgs } from '@storybook/client-api'; - -import README from './README.mdx'; -import DropdownInputPair from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: '.storybook/images/metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '.storybook/images/0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: '.storybook/images/AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: '.storybook/images/BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: '.storybook/images/CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: '.storybook/images/gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: '.storybook/images/gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: '.storybook/images/omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: '.storybook/images/sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: '.storybook/images/tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: '.storybook/images/wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: '.storybook/images/wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownInputPair', - - component: DropdownInputPair, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - itemsToSearch: { control: 'array' }, - onInputChange: { action: 'onInputChange' }, - inputValue: { control: 'text' }, - onSelect: { action: 'onSelect' }, - leftValue: { control: 'text' }, - selectedItem: { control: 'object' }, - maxListItems: { control: 'number' }, - selectPlaceHolderText: { control: 'text' }, - loading: { control: 'boolean' }, - listContainerClassName: { control: 'text' }, - autoFocus: { control: 'boolean' }, - }, -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = (args) => { - const [{ inputValue, selectedItem = tokensToSearch[0] }, updateArgs] = - useArgs(); - return ( - { - updateArgs({ ...args, inputValue: value }); - }} - selectedItem={selectedItem} - /> - ); -}; - -DefaultStory.storyName = 'Default'; - -DefaultStory.args = { - itemsToSearch: tokensToSearch, - maxListItems: tokensToSearch.length, - loading: false, -}; diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js deleted file mode 100644 index e9f319d25fcc..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownInputPair from '.'; - -const createProps = (customProps = {}) => { - return { - onInputChange: jest.fn(), - ...customProps, - }; -}; - -describe('DropdownInputPair', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - , - store, - ); - expect(getByPlaceholderText('0')).toBeInTheDocument(); - expect( - document.querySelector('.dropdown-input-pair__input'), - ).toMatchSnapshot(); - }); - - it('changes the input field', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByPlaceholderText } = renderWithProvider( - , - store, - ); - fireEvent.change(getByPlaceholderText('0'), { - target: { value: 1.1 }, - }); - expect(props.onInputChange).toHaveBeenCalledWith('1.1'); - }); -}); diff --git a/ui/pages/swaps/dropdown-input-pair/index.js b/ui/pages/swaps/dropdown-input-pair/index.js deleted file mode 100644 index d89fc83b8de2..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-input-pair'; diff --git a/ui/pages/swaps/dropdown-input-pair/index.scss b/ui/pages/swaps/dropdown-input-pair/index.scss deleted file mode 100644 index 30d5440e0de6..000000000000 --- a/ui/pages/swaps/dropdown-input-pair/index.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use "design-system"; - -.dropdown-input-pair { - display: flex; - width: 312px; - height: 60px; - position: relative; - - &__input { - margin: 0 !important; - - input { - @include design-system.H4; - - padding-top: 6px; - } - - div { - border: 1px solid var(--color-border-default); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; - height: 60px; - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; /* Firefox */ - } - } - - &__list { - &--full-width { - width: 100%; - } - } - - &__left-value { - @include design-system.H7; - - position: absolute; - right: 16px; - height: 100%; - display: flex; - align-items: center; - color: var(--color-text-alternative); - - &--two-lines { - right: inherit; - left: 157px; - align-items: unset; - top: 34px; - } - } - - .dropdown-input-pair__selector--closed { - height: 60px; - width: 142px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &__two-line-input { - div { - align-items: flex-start; - } - - input { - padding-top: 14px; - } - } -} diff --git a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap b/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap deleted file mode 100644 index 6057b37ee370..000000000000 --- a/ui/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DropdownSearchList renders the component with initial props 1`] = ` -
- -
-`; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js deleted file mode 100644 index 1182ad12d72a..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js +++ /dev/null @@ -1,334 +0,0 @@ -import React, { - useState, - useCallback, - useEffect, - useContext, - useRef, -} from 'react'; -import { useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { isEqual } from 'lodash'; -import { I18nContext } from '../../../contexts/i18n'; -import SearchableItemList from '../searchable-item-list'; -import PulseLoader from '../../../components/ui/pulse-loader'; -import UrlIcon from '../../../components/ui/url-icon'; -import { - Icon, - IconName, - IconSize, -} from '../../../components/component-library'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import ImportToken from '../import-token'; -import { - isHardwareWallet, - getHardwareWalletType, - getCurrentChainId, - getRpcPrefsForCurrentProvider, -} from '../../../selectors/selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; -import { getURLHostName } from '../../../helpers/utils/util'; -import { getCurrentSmartTransactionsEnabled } from '../../../ducks/swaps/swaps'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; - -export default function DropdownSearchList({ - searchListClassName, - itemsToSearch, - selectPlaceHolderText, - fuseSearchKeys, - defaultToAll, - maxListItems, - onSelect, - startingItem, - onOpen, - onClose, - className = '', - externallySelectedItem, - selectorClosedClassName, - loading, - hideRightLabels, - hideItemIf, - listContainerClassName, - shouldSearchForImports, -}) { - const t = useContext(I18nContext); - const [isOpen, setIsOpen] = useState(false); - const [isImportTokenModalOpen, setIsImportTokenModalOpen] = useState(false); - const [selectedItem, setSelectedItem] = useState(startingItem); - const [tokenForImport, setTokenForImport] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - - const trackEvent = useContext(MetaMetricsContext); - - const close = useCallback(() => { - setIsOpen(false); - onClose?.(); - }, [onClose]); - - const onClickItem = useCallback( - (item) => { - onSelect?.(item); - setSelectedItem(item); - close(); - }, - [onSelect, close], - ); - - const onOpenImportTokenModalClick = (item) => { - setTokenForImport(item); - setIsImportTokenModalOpen(true); - }; - - /* istanbul ignore next */ - const onImportTokenClick = () => { - trackEvent({ - event: 'Token Imported', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - symbol: tokenForImport?.symbol, - address: tokenForImport?.address, - chain_id: chainId, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }, - }); - // Only when a user confirms import of a token, we add it and show it in a dropdown. - onSelect?.(tokenForImport); - setSelectedItem(tokenForImport); - setTokenForImport(null); - close(); - }; - - const onImportTokenCloseClick = () => { - setIsImportTokenModalOpen(false); - close(); - }; - - const onClickSelector = useCallback(() => { - if (!isOpen) { - setIsOpen(true); - onOpen?.(); - } - }, [isOpen, onOpen]); - - const prevExternallySelectedItemRef = useRef(); - useEffect(() => { - prevExternallySelectedItemRef.current = externallySelectedItem; - }); - const prevExternallySelectedItem = prevExternallySelectedItemRef.current; - - useEffect(() => { - if ( - externallySelectedItem && - !isEqual(externallySelectedItem, selectedItem) - ) { - setSelectedItem(externallySelectedItem); - } else if (prevExternallySelectedItem && !externallySelectedItem) { - setSelectedItem(null); - } - }, [externallySelectedItem, selectedItem, prevExternallySelectedItem]); - - const onKeyUp = (e) => { - if (e.key === 'Escape') { - close(); - } else if (e.key === 'Enter') { - onClickSelector(e); - } - }; - - const blockExplorerLink = - rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? - null; - - const blockExplorerHostName = getURLHostName(blockExplorerLink); - - const importTokenProps = { - onImportTokenCloseClick, - onImportTokenClick, - setIsImportTokenModalOpen, - tokenForImport, - }; - - return ( -
- {tokenForImport && isImportTokenModalOpen && ( - - )} - {!isOpen && ( -
-
- {selectedItem?.iconUrl && ( - - )} - {!selectedItem?.iconUrl && ( -
- )} -
-
- - {selectedItem?.symbol || selectPlaceHolderText} - -
-
-
- -
- )} - {isOpen && ( - <> - - /* istanbul ignore next */ - loading ? ( -
- -
- - {t('swapFetchingTokens')} - -
-
- ) : ( -
- {t('swapBuildQuotePlaceHolderText', [searchQuery])} - {blockExplorerLink && ( -
- { - trackEvent({ - event: 'Clicked Block Explorer Link', - category: MetaMetricsEventCategory.Swaps, - properties: { - link_type: 'Token Tracker', - action: 'Verify Contract Address', - block_explorer_domain: blockExplorerHostName, - }, - }); - global.platform.openTab({ - url: blockExplorerLink, - }); - }} - target="_blank" - rel="noopener noreferrer" - > - {blockExplorerHostName} - , - ])} - /> -
- )} -
- ) - } - searchPlaceholderText={t('swapSearchNameOrAddress')} - fuseSearchKeys={fuseSearchKeys} - defaultToAll={defaultToAll} - onClickItem={onClickItem} - onOpenImportTokenModalClick={onOpenImportTokenModalClick} - maxListItems={maxListItems} - className={classnames( - 'dropdown-search-list__token-container', - searchListClassName, - { - 'dropdown-search-list--open': isOpen, - }, - )} - hideRightLabels={hideRightLabels} - hideItemIf={hideItemIf} - listContainerClassName={listContainerClassName} - shouldSearchForImports={shouldSearchForImports} - searchQuery={searchQuery} - setSearchQuery={setSearchQuery} - /> -
{ - event.stopPropagation(); - setIsOpen(false); - onClose?.(); - }} - /> - - )} -
- ); -} - -DropdownSearchList.propTypes = { - itemsToSearch: PropTypes.array, - onSelect: PropTypes.func, - searchListClassName: PropTypes.string, - fuseSearchKeys: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string, - weight: PropTypes.number, - }), - ), - defaultToAll: PropTypes.bool, - maxListItems: PropTypes.number, - startingItem: PropTypes.object, - onOpen: PropTypes.func, - onClose: PropTypes.func, - className: PropTypes.string, - externallySelectedItem: PropTypes.object, - loading: PropTypes.bool, - selectPlaceHolderText: PropTypes.string, - selectorClosedClassName: PropTypes.string, - hideRightLabels: PropTypes.bool, - hideItemIf: PropTypes.func, - listContainerClassName: PropTypes.string, - shouldSearchForImports: PropTypes.bool, -}; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js deleted file mode 100644 index 73ec3ea7aec8..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.stories.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import DropdownSearchList from '.'; - -const tokens = [ - { - primaryLabel: 'MetaMark (META)', - name: 'MetaMark', - iconUrl: 'metamark.svg', - erc20: true, - decimals: 18, - symbol: 'META', - address: '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4', - }, - { - primaryLabel: '0x (ZRX)', - name: '0x', - iconUrl: '0x.svg', - erc20: true, - symbol: 'ZRX', - decimals: 18, - address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', - }, - { - primaryLabel: 'AirSwap Token (AST)', - name: 'AirSwap Token', - iconUrl: 'AST.png', - erc20: true, - symbol: 'AST', - decimals: 4, - address: '0x27054b13b1B798B345b591a4d22e6562d47eA75a', - }, - { - primaryLabel: 'Basic Attention Token (BAT)', - name: 'Basic Attention Token', - iconUrl: 'BAT_icon.svg', - erc20: true, - symbol: 'BAT', - decimals: 18, - address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', - }, - { - primaryLabel: 'Civil Token (CVL)', - name: 'Civil Token', - iconUrl: 'CVL_token.svg', - erc20: true, - symbol: 'CVL', - decimals: 18, - address: '0x01FA555c97D7958Fa6f771f3BbD5CCD508f81e22', - }, - { - primaryLabel: 'Gladius (GLA)', - name: 'Gladius', - iconUrl: 'gladius.svg', - erc20: true, - symbol: 'GLA', - decimals: 8, - address: '0x71D01dB8d6a2fBEa7f8d434599C237980C234e4C', - }, - { - primaryLabel: 'Gnosis Token (GNO)', - name: 'Gnosis Token', - iconUrl: 'gnosis.svg', - erc20: true, - symbol: 'GNO', - decimals: 18, - address: '0x6810e776880C02933D47DB1b9fc05908e5386b96', - }, - { - primaryLabel: 'OmiseGO (OMG)', - name: 'OmiseGO', - iconUrl: 'omg.jpg', - erc20: true, - symbol: 'OMG', - decimals: 18, - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - }, - { - primaryLabel: 'Sai Stablecoin v1.0 (SAI)', - name: 'Sai Stablecoin v1.0', - iconUrl: 'sai.svg', - erc20: true, - symbol: 'SAI', - decimals: 18, - address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', - }, - { - primaryLabel: 'Tether USD (USDT)', - name: 'Tether USD', - iconUrl: 'tether_usd.png', - erc20: true, - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - }, - { - primaryLabel: 'WednesdayCoin (WED)', - name: 'WednesdayCoin', - iconUrl: 'wed.png', - erc20: true, - symbol: 'WED', - decimals: 18, - address: '0x7848ae8F19671Dc05966dafBeFbBbb0308BDfAbD', - }, - { - primaryLabel: 'Wrapped BTC (WBTC)', - name: 'Wrapped BTC', - iconUrl: 'wbtc.png', - erc20: true, - symbol: 'WBTC', - decimals: 8, - address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', - }, -]; - -export default { - title: 'Pages/Swaps/DropdownSearchList', -}; - -const tokensToSearch = tokens.map((token) => ({ - ...token, - primaryLabel: token.symbol, - secondaryLabel: token.name, - rightPrimaryLabel: `${(Math.random() * 100).toFixed( - Math.floor(Math.random() * 6), - )} ${token.symbol}`, - rightSecondaryLabel: `$${(Math.random() * 1000).toFixed(2)}`, -})); - -export const DefaultStory = () => { - return ( -
- -
- ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js deleted file mode 100644 index f0ae4a889169..000000000000 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, - fireEvent, -} from '../../../../test/jest'; -import DropdownSearchList from '.'; - -const createProps = (customProps = {}) => { - return { - startingItem: { - iconUrl: 'iconUrl', - symbol: 'symbol', - }, - ...customProps, - }; -}; - -jest.mock('../searchable-item-list', () => jest.fn(() => null)); - -describe('DropdownSearchList', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { container, getByText } = renderWithProvider( - , - store, - ); - expect(container).toMatchSnapshot(); - expect(getByText('symbol')).toBeInTheDocument(); - }); - - it('renders the component, opens the list and closes it', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { getByTestId } = renderWithProvider( - , - store, - ); - const dropdownSearchList = getByTestId('dropdown-search-list'); - expect(dropdownSearchList).toBeInTheDocument(); - fireEvent.click(dropdownSearchList); - const closeButton = getByTestId('dropdown-search-list__close-area'); - expect(closeButton).toBeInTheDocument(); - fireEvent.click(closeButton); - expect(closeButton).not.toBeInTheDocument(); - }); -}); diff --git a/ui/pages/swaps/dropdown-search-list/index.js b/ui/pages/swaps/dropdown-search-list/index.js deleted file mode 100644 index 3dd2e4ecf63e..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './dropdown-search-list'; diff --git a/ui/pages/swaps/dropdown-search-list/index.scss b/ui/pages/swaps/dropdown-search-list/index.scss deleted file mode 100644 index 904f9530d341..000000000000 --- a/ui/pages/swaps/dropdown-search-list/index.scss +++ /dev/null @@ -1,167 +0,0 @@ -@use "design-system"; - -.dropdown-search-list { - &__search-list-open { - margin: 24px; - box-shadow: none; - border-radius: 6px; - min-height: 297px; - width: 100%; - } - - &__token-container { - margin: 0; - min-height: auto; - border: 1px solid var(--color-border-default); - box-sizing: border-box; - box-shadow: none; - border-radius: 6px; - width: 100%; - } - - &--open { - box-shadow: var(--shadow-size-sm) var(--color-shadow-default); - border: 1px solid var(--color-border-default); - } - - &__close-area { - position: fixed; - top: 0; - left: 0; - z-index: 1000; - width: 100%; - height: 100%; - } - - &__selector-closed-container { - display: flex; - width: 100%; - position: relative; - align-items: center; - max-height: 60px; - transition: 200ms ease-in-out; - border-radius: 6px; - box-shadow: none; - border: 1px solid var(--color-border-default); - height: 60px; - - &:hover { - background: var(--color-background-default-hover); - } - } - - &__caret { - position: absolute; - right: 16px; - color: var(--color-icon-default); - } - - &__selector-closed { - display: flex; - flex-flow: row nowrap; - align-items: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - position: relative; - align-items: center; - flex: 1; - height: 60px; - - i { - font-size: 1.2em; - } - - .dropdown-search-list__item-labels { - width: 100%; - } - } - - &__selector-closed-icon { - width: 34px; - height: 34px; - } - - &__closed-primary-label { - @include design-system.H4; - - color: var(--color-text-default); - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__search-list--open { - box-shadow: var(--shadow-size-md) var(--color-shadow-default); - border: 1px solid var(--color-border-muted); - } - - &__default-dropdown-icon { - width: 34px; - height: 34px; - border-radius: 50%; - background: var(--color-background-alternative); - flex: 0 1 auto; - } - - &__labels { - display: flex; - justify-content: space-between; - width: 100%; - flex: 1; - } - - &__item-labels { - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 8px; - } - - &__select-default { - color: var(--color-text-muted); - } - - &__placeholder { - @include design-system.H6; - - padding: 16px; - color: var(--color-text-alternative); - min-height: 300px; - position: relative; - z-index: 1002; - background: var(--color-background-default); - border-radius: 6px; - min-height: 194px; - overflow: hidden; - text-overflow: ellipsis; - - .searchable-item-list__item--add-token { - padding: 8px 0; - } - } - - &__loading-item { - transition: 200ms ease-in-out; - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: center; - padding: 16px 12px; - box-sizing: border-box; - cursor: pointer; - border-top: 1px solid var(--color-border-muted); - position: relative; - z-index: 1; - background: var(--color-background-default); - } - - &__loading-item-text-container { - margin-left: 4px; - } - - &__loading-item-text { - font-weight: bold; - } -} diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js index 877e38aa7c84..cf079acc9623 100644 --- a/ui/pages/swaps/index.js +++ b/ui/pages/swaps/index.js @@ -14,7 +14,6 @@ import { Redirect, } from 'react-router-dom'; import { shuffle, isEqual } from 'lodash'; -import classnames from 'classnames'; import { TransactionStatus } from '@metamask/transaction-controller'; import { I18nContext } from '../../contexts/i18n'; @@ -40,11 +39,8 @@ import { prepareToLeaveSwaps, fetchSwapsLivenessAndFeatureFlags, getReviewSwapClickedTimestamp, - getPendingSmartTransactions, getCurrentSmartTransactionsEnabled, getCurrentSmartTransactionsError, - navigateBackToBuildQuote, - getSwapRedesignEnabled, setTransactionSettingsOpened, getLatestAddedTokenTo, } from '../../ducks/swaps/swaps'; @@ -57,8 +53,6 @@ import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, DEFAULT_ROUTE, @@ -99,10 +93,8 @@ import AwaitingSignatures from './awaiting-signatures'; import SmartTransactionStatus from './smart-transaction-status'; import AwaitingSwap from './awaiting-swap'; import LoadingQuote from './loading-swaps-quotes'; -import BuildQuote from './build-quote'; import PrepareSwapPage from './prepare-swap-page/prepare-swap-page'; import NotificationPage from './notification-page/notification-page'; -import ViewQuote from './view-quote'; export default function Swap() { const t = useContext(I18nContext); @@ -117,7 +109,6 @@ export default function Swap() { const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const isSmartTransactionStatusRoute = pathname === SMART_TRANSACTION_STATUS_ROUTE; - const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE; const isPrepareSwapRoute = pathname === PREPARE_SWAP_ROUTE; const [currentStxErrorTracked, setCurrentStxErrorTracked] = useState(false); @@ -140,7 +131,6 @@ export default function Swap() { const tokenList = useSelector(getTokenList, isEqual); const shuffledTokensList = shuffle(Object.values(tokenList)); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const pendingSmartTransactions = useSelector(getPendingSmartTransactions); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( getSmartTransactionsOptInStatus, @@ -149,7 +139,6 @@ export default function Swap() { const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); - const swapRedesignEnabled = useSelector(getSwapRedesignEnabled); const currentSmartTransactionsError = useSelector( getCurrentSmartTransactionsError, ); @@ -358,167 +347,72 @@ export default function Swap() {
- {!swapRedesignEnabled && ( -
{ - await dispatch(navigateBackToBuildQuote(history)); - }} - > - {isViewQuoteRoute && t('edit')} -
- )} - {swapRedesignEnabled && ( - { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && ( - - )} - - )} -
{t('swap')}
- {!swapRedesignEnabled && ( -
{ - clearTemporaryTokenRef.current(); - dispatch(clearSwapsState()); - await dispatch(resetBackgroundSwapsState()); - history.push(DEFAULT_ROUTE); - }} - > - {!isAwaitingSwapRoute && - !isAwaitingSignaturesRoute && - !isSmartTransactionStatusRoute && - t('cancel')} -
- )} - {swapRedesignEnabled && ( - { - if (e.key === 'Enter') { - dispatch(setTransactionSettingsOpened(true)); - } - }} - > - {isPrepareSwapRoute && ( + { + if (e.key === 'Enter') { + redirectToDefaultRoute(); + } + }} + > + {!isAwaitingSwapRoute && + !isAwaitingSignaturesRoute && + !isSmartTransactionStatusRoute && ( { - dispatch(setTransactionSettingsOpened(true)); - }} + onClick={redirectToDefaultRoute} style={{ cursor: 'pointer' }} - title={t('transactionSettings')} + title={t('cancel')} /> )} - - )} + +
{t('swap')}
+ { + if (e.key === 'Enter') { + dispatch(setTransactionSettingsOpened(true)); + } + }} + > + {isPrepareSwapRoute && ( + { + dispatch(setTransactionSettingsOpened(true)); + }} + style={{ cursor: 'pointer' }} + title={t('transactionSettings')} + /> + )} +
-
+
- { - if (swapRedesignEnabled) { - return ; - } - if (tradeTxData && !conversionError) { - return ; - } else if (tradeTxData && routeState) { - return ; - } else if (routeState === 'loading' && aggregatorMetadata) { - return ; - } - - return ( - - ); - }} - /> { - if (!swapRedesignEnabled) { - return ; - } - - return ( - - ); - }} - /> - { - if ( - pendingSmartTransactions.length > 0 && - routeState === 'smartTransactionStatus' - ) { - return ( - - ); - } - if (swapRedesignEnabled) { - return ; - } - if (Object.values(quotes).length) { - return ( - - ); - } else if (fetchParams) { - return ; - } - return ; - }} + render={() => ( + + )} /> ); } - return ; + return ; }} /> ) : ( - + ); }} /> @@ -585,7 +479,7 @@ export default function Swap() { return swapsEnabled === false ? ( ) : ( - + ); }} /> diff --git a/ui/pages/swaps/index.scss b/ui/pages/swaps/index.scss index 33fc1e9a17cb..4de2ac35707f 100644 --- a/ui/pages/swaps/index.scss +++ b/ui/pages/swaps/index.scss @@ -3,27 +3,21 @@ @import 'awaiting-swap/index'; @import 'awaiting-signatures/index'; @import 'smart-transaction-status/index'; -@import 'build-quote/index'; @import 'prepare-swap-page/index'; @import 'notification-page/index'; @import 'countdown-timer/index'; -@import 'dropdown-input-pair/index'; -@import 'dropdown-search-list/index'; @import 'exchange-rate-display/index'; @import 'fee-card/index'; @import 'loading-swaps-quotes/index'; -@import 'main-quote-summary/index'; @import 'searchable-item-list/index'; @import 'select-quote-popover/index'; -@import 'slippage-buttons/index'; @import 'swaps-footer/index'; -@import 'view-quote/index'; @import 'create-new-swap/index'; @import 'view-on-block-explorer/index'; @import 'transaction-settings/index'; @import 'list-with-search/index'; -@import 'popover-custom-background/index'; @import 'mascot-background-animation/index'; +@import 'selected-token/index'; .swaps { display: flex; @@ -78,13 +72,7 @@ } @include design-system.screen-sm-min { - width: 348px; - } - - &--redesign-enabled { - @include design-system.screen-sm-min { - width: 100%; - } + width: 100%; } } @@ -119,25 +107,6 @@ } } - &__header-cancel { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-right: 24px; - flex: 1; - text-align: right; - } - - &__header-edit { - @include design-system.H7; - - color: var(--color-primary-default); - cursor: pointer; - padding-left: 24px; - flex: 1; - } - .actionable-message__message &__notification-close-button { background-color: transparent; position: absolute; diff --git a/ui/pages/swaps/index.test.js b/ui/pages/swaps/index.test.js index aa4109567f99..4157b614562f 100644 --- a/ui/pages/swaps/index.test.js +++ b/ui/pages/swaps/index.test.js @@ -22,7 +22,7 @@ jest.mock('react-router-dom', () => ({ }), useLocation: jest.fn(() => { return { - pathname: '/swaps/build-quote', + pathname: '/swaps/prepare-swap-page', }; }), })); @@ -81,12 +81,10 @@ describe('Swap', () => { it('renders the component with initial props', async () => { const swapsMockStore = createSwapsMockStore(); - swapsMockStore.metamask.swapsState.swapsFeatureFlags.swapRedesign.extensionActive = false; const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider(, store); await waitFor(() => expect(featureFlagsNock.isDone()).toBe(true)); expect(getByText('Swap')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js index a8b7c9bf2a51..e98d275f8aa8 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js @@ -6,7 +6,7 @@ import { shuffle } from 'lodash'; import { useHistory } from 'react-router-dom'; import isEqual from 'lodash/isEqual'; import { - navigateBackToBuildQuote, + navigateBackToPrepareSwap, getFetchParams, getQuotesFetchStartTime, getCurrentSmartTransactionsEnabled, @@ -183,7 +183,7 @@ export default function LoadingSwapsQuotes({ submitText={t('back')} onSubmit={async () => { trackEvent(quotesRequestCancelledEventConfig); - await dispatch(navigateBackToBuildQuote(history)); + await dispatch(navigateBackToPrepareSwap(history)); }} hideCancel /> diff --git a/ui/pages/swaps/main-quote-summary/README.mdx b/ui/pages/swaps/main-quote-summary/README.mdx deleted file mode 100644 index c32397d1e762..000000000000 --- a/ui/pages/swaps/main-quote-summary/README.mdx +++ /dev/null @@ -1,14 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; -import MainQuoteSummary from '.'; - -# MainQuoteSummary - -MainQuoteSummary displays the quote of a swap. - - - - - -## Props - - \ No newline at end of file diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap deleted file mode 100644 index 3b202988fc42..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainQuoteSummary renders the component with initial props 1`] = ` -
- - 2 - -
- - E - -
- - ETH - -
-`; - -exports[`MainQuoteSummary renders the component with initial props 2`] = ` -
-
- - B - -
- - BAT - -
-`; - -exports[`MainQuoteSummary renders the component with initial props 3`] = ` -
-
-
- - 0.2 - -
-
-
-`; - -exports[`MainQuoteSummary renders the component with initial props 4`] = ` -
-
- - -
-
-`; diff --git a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap b/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap deleted file mode 100644 index 2a1fef0ff0ac..000000000000 --- a/ui/pages/swaps/main-quote-summary/__snapshots__/quote-backdrop.test.js.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QuotesBackdrop renders the component with initial props 1`] = ` - - - -`; - -exports[`QuotesBackdrop renders the component with initial props 2`] = ` - - - - - - - - - -`; - -exports[`QuotesBackdrop renders the component with initial props 3`] = ` - - - - -`; diff --git a/ui/pages/swaps/main-quote-summary/index.js b/ui/pages/swaps/main-quote-summary/index.js deleted file mode 100644 index 235070e29323..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './main-quote-summary'; diff --git a/ui/pages/swaps/main-quote-summary/index.scss b/ui/pages/swaps/main-quote-summary/index.scss deleted file mode 100644 index 3f7693705db8..000000000000 --- a/ui/pages/swaps/main-quote-summary/index.scss +++ /dev/null @@ -1,125 +0,0 @@ -@use "design-system"; - -.main-quote-summary { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - position: relative; - width: 100%; - color: var(--color-text-default); - margin-top: 28px; - margin-bottom: 56px; - - &__source-row, - &__destination-row { - width: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - - @include design-system.H6; - - color: var(--color-text-alternative); - } - - &__source-row { - align-items: center; - } - - &__source-row-value, - &__source-row-symbol { - // Each of these spans can be half their container width minus the space - // needed for the token icon and the span margins - max-width: calc(50% - 13px); - } - - - &__source-row-value { - margin-right: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__source-row-symbol { - margin-left: 5px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__destination-row { - margin-top: 6px; - } - - &__destination-row-symbol { - margin-left: 5px; - color: var(--color-text-default); - } - - &__icon, - &__icon-fallback { - height: 16px; - width: 16px; - } - - &__icon-fallback { - padding-top: 0; - font-size: 12px; - line-height: 16px; - } - - &__down-arrow { - margin-top: 5px; - color: var(--color-icon-muted); - } - - &__details { - display: flex; - flex-flow: column; - align-items: center; - width: 310px; - position: relative; - } - - &__quote-details-top { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - width: 100%; - } - - &__quote-large { - display: flex; - align-items: flex-start; - margin-top: 8px; - height: 50px; - } - - &__quote-large-number { - font-size: 50px; - line-height: 48px; - } - - &__quote-large-white { - font-size: 40px; - text-overflow: ellipsis; - width: 295px; - overflow: hidden; - white-space: nowrap; - } - - &__exchange-rate-container { - display: flex; - justify-content: center; - align-items: center; - width: 287px; - margin-top: 14px; - } - - &__exchange-rate-display { - color: var(--color-text-alternative); - } -} diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.js deleted file mode 100644 index d7ff9646a3a6..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.js +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BigNumber from 'bignumber.js'; -import Tooltip from '../../../components/ui/tooltip'; -import UrlIcon from '../../../components/ui/url-icon'; -import ExchangeRateDisplay from '../exchange-rate-display'; -import { formatSwapsValueForDisplay } from '../swaps.util'; -import { - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; - -function getFontSizesAndLineHeights(fontSizeScore) { - if (fontSizeScore <= 9) { - return [50, 48]; - } - if (fontSizeScore <= 13) { - return [40, 32]; - } - return [26, 15]; -} - -export default function MainQuoteSummary({ - sourceValue, - sourceSymbol, - sourceDecimals, - sourceIconUrl, - destinationValue, - destinationSymbol, - destinationDecimals, - destinationIconUrl, -}) { - const sourceAmount = toPrecisionWithoutTrailingZeros( - calcTokenAmount(sourceValue, sourceDecimals).toString(10), - 12, - ); - const destinationAmount = calcTokenAmount( - destinationValue, - destinationDecimals, - ); - - const amountToDisplay = formatSwapsValueForDisplay(destinationAmount); - const amountDigitLength = amountToDisplay.match(/\d+/gu).join('').length; - const [numberFontSize, lineHeight] = - getFontSizesAndLineHeights(amountDigitLength); - let ellipsedAmountToDisplay = amountToDisplay; - - if (amountDigitLength > 20) { - ellipsedAmountToDisplay = `${amountToDisplay.slice(0, 20)}...`; - } - - return ( -
-
-
-
- - {formatSwapsValueForDisplay(sourceAmount)} - - - - {sourceSymbol} - -
- -
- - - {destinationSymbol} - -
-
- - - {`${ellipsedAmountToDisplay}`} - - -
-
-
- -
-
-
- ); -} - -MainQuoteSummary.propTypes = { - /** - * The amount that will be sent in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - sourceValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the source token. - */ - sourceDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** - * The ticker symbol for the source token. - */ - sourceSymbol: PropTypes.string.isRequired, - - /** - * The amount that will be received in the smallest denomination. - * For example, wei is the smallest denomination for ether. - */ - destinationValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.instanceOf(BigNumber), - ]).isRequired, - - /** - * Maximum number of decimal places for the destination token. - */ - destinationDecimals: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - - /** - * The ticker symbol for the destination token. - */ - destinationSymbol: PropTypes.string.isRequired, - - /** - * The location of the source token icon file. - */ - sourceIconUrl: PropTypes.string, - - /** - * The location of the destination token icon file. - */ - destinationIconUrl: PropTypes.string, -}; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js deleted file mode 100644 index 56ed74624193..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.stories.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import README from './README.mdx'; -import MainQuoteSummary from './main-quote-summary'; - -export default { - title: 'Pages/Swaps/MainQuoteSummary', - - component: MainQuoteSummary, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - sourceValue: { - control: 'text', - }, - sourceDecimals: { - control: 'number', - }, - sourceSymbol: { - control: 'text', - }, - destinationValue: { - control: 'text', - }, - destinationDecimals: { - control: 'number', - }, - destinationSymbol: { - control: 'text', - }, - sourceIconUrl: { - control: 'text', - }, - destinationIconUrl: { - control: 'text', - }, - }, - args: { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'ABC', - sourceIconUrl: '.storybook/images/metamark.svg', - destinationIconUrl: '.storybook/images/sai.svg', - }, -}; - -export const DefaultStory = (args) => { - return ( -
- -
- ); -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js b/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js deleted file mode 100644 index 85e17bd48de4..000000000000 --- a/ui/pages/swaps/main-quote-summary/main-quote-summary.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import MainQuoteSummary from '.'; - -const createProps = (customProps = {}) => { - return { - sourceValue: '2000000000000000000', - sourceDecimals: 18, - sourceSymbol: 'ETH', - destinationValue: '200000000000000000', - destinationDecimals: 18, - destinationSymbol: 'BAT', - ...customProps, - }; -}; - -describe('MainQuoteSummary', () => { - it('renders the component with initial props', () => { - const props = createProps(); - const { getAllByText } = renderWithProvider( - , - ); - expect(getAllByText(props.sourceSymbol)).toHaveLength(2); - expect(getAllByText(props.destinationSymbol)).toHaveLength(2); - expect( - document.querySelector('.main-quote-summary__source-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__destination-row'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__quote-large'), - ).toMatchSnapshot(); - expect( - document.querySelector('.main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.js deleted file mode 100644 index 44351c73bb5d..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.js +++ /dev/null @@ -1,89 +0,0 @@ -/* eslint-disable @metamask/design-tokens/color-no-hex*/ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default function QuotesBackdrop({ withTopTab }) { - return ( - - - - {withTopTab && ( - - )} - - - - - - - - - - - - - - - - - - - - - - ); -} - -QuotesBackdrop.propTypes = { - withTopTab: PropTypes.bool, -}; diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js deleted file mode 100644 index 00d23c2656d6..000000000000 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { renderWithProvider } from '../../../../test/jest'; -import QuotesBackdrop from './quote-backdrop'; - -const createProps = (customProps = {}) => { - return { - withTopTab: false, - ...customProps, - }; -}; - -describe('QuotesBackdrop', () => { - it('renders the component with initial props', () => { - const { container } = renderWithProvider( - , - ); - expect(container.firstChild.nodeName).toBe('svg'); - expect(document.querySelector('g')).toMatchSnapshot(); - expect(document.querySelector('filter')).toMatchSnapshot(); - expect(document.querySelector('linearGradient')).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/popover-custom-background/index.scss b/ui/pages/swaps/popover-custom-background/index.scss deleted file mode 100644 index 07bc852edbfd..000000000000 --- a/ui/pages/swaps/popover-custom-background/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.popover-custom-background { - height: 100%; - width: 100%; - background: var(--color-background-alternative); - opacity: 0.6; -} diff --git a/ui/pages/swaps/popover-custom-background/popover-custom-background.js b/ui/pages/swaps/popover-custom-background/popover-custom-background.js deleted file mode 100644 index 8e8af648ad7f..000000000000 --- a/ui/pages/swaps/popover-custom-background/popover-custom-background.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Box from '../../../components/ui/box'; - -const PopoverCustomBackground = ({ onClose }) => { - return ; -}; - -export default PopoverCustomBackground; - -PopoverCustomBackground.propTypes = { - onClose: PropTypes.func, -}; diff --git a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap b/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap deleted file mode 100644 index 8991347eecfc..000000000000 --- a/ui/pages/swaps/prepare-swap-page/__snapshots__/prepare-swap-page.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrepareSwapPage renders the component with initial props 1`] = `null`; diff --git a/ui/pages/swaps/prepare-swap-page/index.scss b/ui/pages/swaps/prepare-swap-page/index.scss index 1909f11024b1..60e24c6cdbce 100644 --- a/ui/pages/swaps/prepare-swap-page/index.scss +++ b/ui/pages/swaps/prepare-swap-page/index.scss @@ -27,12 +27,6 @@ margin-top: 16px; position: relative; - .dropdown-input-pair__input { - input { - text-align: right; - } - } - .MuiInputBase-root { padding-right: 0; } @@ -124,98 +118,6 @@ } } - .dropdown-search-list { - background-color: var(--color-background-alternative); - border-radius: 100px; - - &__select-default { - color: var(--color-text-default); - } - - &__labels { - flex: auto; - max-width: 110px; - - &--with-icon { - max-width: 95px; - } - } - - &__closed-primary-label { - font-weight: 500; - } - - &__selector-closed-container { - border: 0; - border-radius: 100px; - height: 32px; - max-height: 32px; - max-width: 165px; - width: auto; - } - - &__selector-closed-icon { - width: 24px; - height: 24px; - margin-right: 8px; - } - - &__selector-closed { - height: 32px; - max-width: 140px; - - div { - display: flex; - } - - &__item-labels { - width: 100%; - margin-left: 0; - } - } - } - - .dropdown-input-pair { - height: 32px; - width: auto; - - &__selector--closed { - height: 32px; - border-top-right-radius: 100px; - border-bottom-right-radius: 100px; - } - - .searchable-item-list { - &__item--add-token { - display: none; - } - } - - &__to { - display: flex; - justify-content: space-between; - align-items: center; - - .searchable-item-list { - &__item--add-token { - display: flex; - } - } - } - - &__input { - div { - border: 0; - } - } - - &__two-line-input { - input { - padding-bottom: 0; - } - } - } - &__token-etherscan-link { color: var(--color-primary-default); cursor: pointer; @@ -306,12 +208,6 @@ width: 100%; } - .main-quote-summary { - &__exchange-rate-display { - width: auto; - } - } - &::after { // Hide preloaded images. position: absolute; width: 0; @@ -365,6 +261,10 @@ &__edit-limit { white-space: nowrap; } + + &__exchange-rate-display { + color: var(--color-text-alternative); + } } @keyframes slide-in { diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js index d576dad0933d..a76160ef77bb 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js @@ -82,9 +82,6 @@ describe('PrepareSwapPage', () => { store, ); expect(getByText('Select token')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); }); it('switches swap from and to tokens', () => { diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 9921161c4da4..4c47437b1bd8 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -1205,7 +1205,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { secondaryTokenDecimals={destinationTokenDecimals} secondaryTokenSymbol={destinationTokenSymbol} boldSymbols={false} - className="main-quote-summary__exchange-rate-display" + className="review-quote__exchange-rate-display" showIconForSwappingTokens={false} /> diff --git a/ui/pages/swaps/select-quote-popover/quote-details/index.scss b/ui/pages/swaps/select-quote-popover/quote-details/index.scss index 861759235aba..80e034ab1d4d 100644 --- a/ui/pages/swaps/select-quote-popover/quote-details/index.scss +++ b/ui/pages/swaps/select-quote-popover/quote-details/index.scss @@ -30,10 +30,6 @@ align-items: center; height: inherit; - .view-quote__conversion-rate-eth-label { - color: var(--color-text-default); - } - i { color: var(--color-primary-default); } @@ -68,14 +64,6 @@ } } - .view-quote__conversion-rate-token-label { - @include design-system.H6; - - color: var(--color-text-default); - font-weight: bold; - margin-left: 2px; - } - &__metafox-logo { width: 17px; margin-right: 4px; diff --git a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap index 11cd9372ed7f..189eda3e38e3 100644 --- a/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap +++ b/ui/pages/swaps/selected-token/__snapshots__/selected-token.test.js.snap @@ -3,40 +3,44 @@ exports[`SelectedToken renders the component with initial props 1`] = `
-
`; @@ -44,31 +48,35 @@ exports[`SelectedToken renders the component with initial props 1`] = ` exports[`SelectedToken renders the component with no token selected 1`] = `
-
`; diff --git a/ui/pages/swaps/selected-token/index.scss b/ui/pages/swaps/selected-token/index.scss new file mode 100644 index 000000000000..bc69a934ae02 --- /dev/null +++ b/ui/pages/swaps/selected-token/index.scss @@ -0,0 +1,142 @@ +@use "design-system"; + +.selected-token { + .selected-token-list { + background-color: var(--color-background-alternative); + border-radius: 100px; + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__select-default { + color: var(--color-text-default); + } + + &__labels { + display: flex; + justify-content: space-between; + width: 100%; + flex: auto; + max-width: 110px; + + &--with-icon { + max-width: 95px; + } + } + + &__closed-primary-label { + @include design-system.H4; + + color: var(--color-text-default); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + + &__selector-closed-container { + display: flex; + position: relative; + align-items: center; + transition: 200ms ease-in-out; + box-shadow: none; + border: 0; + border-radius: 100px; + height: 32px; + max-height: 32px; + max-width: 165px; + width: auto; + + &:hover { + background: var(--color-background-default-hover); + } + } + + &__selector-closed-icon { + width: 24px; + height: 24px; + margin-right: 8px; + } + + &__selector-closed { + display: flex; + flex-flow: row nowrap; + padding: 16px 12px; + box-sizing: border-box; + cursor: pointer; + position: relative; + align-items: center; + flex: 1; + height: 32px; + max-width: 140px; + + i { + font-size: 1.2em; + } + + div { + display: flex; + } + + &__item-labels { + width: 100%; + margin-left: 0; + } + } + + &__item-labels { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 8px; + } + } + + .selected-token-input-pair { + height: 32px; + width: auto; + + &__selector--closed { + height: 60px; + border-top-right-radius: 100px; + border-bottom-right-radius: 100px; + } + + .searchable-item-list { + &__item--add-token { + display: none; + } + } + + &__to { + display: flex; + justify-content: space-between; + align-items: center; + + .searchable-item-list { + &__item--add-token { + display: flex; + } + } + } + + &__input { + div { + border: 0; + } + } + + &__two-line-input { + input { + padding-bottom: 0; + } + } + } +} diff --git a/ui/pages/swaps/selected-token/selected-token.js b/ui/pages/swaps/selected-token/selected-token.js index 516dbdc246e3..174a5925282b 100644 --- a/ui/pages/swaps/selected-token/selected-token.js +++ b/ui/pages/swaps/selected-token/selected-token.js @@ -29,52 +29,54 @@ export default function SelectedToken({ }; return ( -
-
- {hasIcon && ( - +
+
-
- - {selectedToken?.symbol || t('swapSelectAToken')} - + data-testid="selected-token-list" + tabIndex="0" + onClick={onClick} + onKeyUp={onKeyUp} + > +
+ {hasIcon && ( + + )} +
+
+ + {selectedToken?.symbol || t('swapSelectAToken')} + +
+
-
); } diff --git a/ui/pages/swaps/selected-token/selected-token.test.js b/ui/pages/swaps/selected-token/selected-token.test.js index 15cca222a8ae..af70ed0bd8f1 100644 --- a/ui/pages/swaps/selected-token/selected-token.test.js +++ b/ui/pages/swaps/selected-token/selected-token.test.js @@ -37,7 +37,7 @@ describe('SelectedToken', () => { it('renders the component and opens the list', () => { const props = createProps(); const { getByTestId } = renderWithProvider(); - const dropdownSearchList = getByTestId('dropdown-search-list'); + const dropdownSearchList = getByTestId('selected-token-list'); expect(dropdownSearchList).toBeInTheDocument(); fireEvent.click(dropdownSearchList); expect(props.onClick).toHaveBeenCalledTimes(1); diff --git a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap b/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap deleted file mode 100644 index 4f1f1d15f355..000000000000 --- a/ui/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SlippageButtons renders the component with initial props 1`] = ` - -`; - -exports[`SlippageButtons renders the component with initial props 2`] = ` -
- - - -
-`; diff --git a/ui/pages/swaps/slippage-buttons/index.js b/ui/pages/swaps/slippage-buttons/index.js deleted file mode 100644 index 6cdbe8843019..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './slippage-buttons'; diff --git a/ui/pages/swaps/slippage-buttons/index.scss b/ui/pages/swaps/slippage-buttons/index.scss deleted file mode 100644 index de6e68ba3556..000000000000 --- a/ui/pages/swaps/slippage-buttons/index.scss +++ /dev/null @@ -1,111 +0,0 @@ -@use "design-system"; - -.slippage-buttons { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - - &__header { - display: flex; - align-items: center; - color: var(--color-primary-default); - margin-bottom: 0; - margin-left: auto; - margin-right: auto; - background: unset; - - &--open { - margin-bottom: 8px; - } - } - - &__content { - padding-left: 10px; - } - - &__dropdown-content { - display: flex; - align-items: center; - } - - &__buttons-prefix { - display: flex; - align-items: center; - margin-right: 8px; - } - - &__button-group { - & &-custom-button { - cursor: text; - display: flex; - align-items: center; - justify-content: center; - position: relative; - min-width: 72px; - margin-right: 0; - } - } - - &__custom-input { - display: flex; - justify-content: center; - - input { - border: none; - width: 64px; - text-align: center; - background: var(--color-primary-default); - color: var(--color-primary-inverse); - font-weight: inherit; - - &::-webkit-input-placeholder { /* WebKit, Blink, Edge */ - color: var(--color-primary-inverse); - } - - &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &::-moz-placeholder { /* Mozilla Firefox 19+ */ - color: var(--color-primary-inverse); - opacity: 1; - } - - &:-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: var(--color-primary-inverse); - } - - &::-ms-input-placeholder { /* Microsoft Edge */ - color: var(--color-primary-inverse); - } - - &::placeholder { /* Most modern browsers support this now. */ - color: var(--color-primary-inverse); - } - } - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ - } - - input[type=number] { - -moz-appearance: textfield; - } - - &--danger { - input { - background: var(--color-error-default); - } - } - } - - &__percentage-suffix { - position: absolute; - right: 5px; - } -} diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.js deleted file mode 100644 index 387958753e3c..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.js +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; -import ButtonGroup from '../../../components/ui/button-group'; -import Button from '../../../components/ui/button'; -import InfoTooltip from '../../../components/ui/info-tooltip'; -import { Slippage } from '../../../../shared/constants/swaps'; -import { Text } from '../../../components/component-library'; -import { - TextVariant, - TextColor, -} from '../../../helpers/constants/design-system'; - -export default function SlippageButtons({ - onSelect, - maxAllowedSlippage, - currentSlippage, - isDirectWrappingEnabled, -}) { - const t = useContext(I18nContext); - const [customValue, setCustomValue] = useState(() => { - if ( - typeof currentSlippage === 'number' && - !Object.values(Slippage).includes(currentSlippage) - ) { - return currentSlippage.toString(); - } - return ''; - }); - const [enteringCustomValue, setEnteringCustomValue] = useState(false); - const [activeButtonIndex, setActiveButtonIndex] = useState(() => { - if (currentSlippage === Slippage.high) { - return 1; // 3% slippage. - } else if (currentSlippage === Slippage.default) { - return 0; // 2% slippage. - } else if (typeof currentSlippage === 'number') { - return 2; // Custom slippage. - } - return 0; - }); - const [open, setOpen] = useState(() => { - return currentSlippage !== Slippage.default; // Only open Advanced options by default if it's not default slippage. - }); - const [inputRef, setInputRef] = useState(null); - - let errorText = ''; - if (customValue) { - // customValue is a string, e.g. '0' - if (Number(customValue) < 0) { - errorText = t('swapSlippageNegative'); - } else if (Number(customValue) > 0 && Number(customValue) <= 1) { - // We will not show this warning for 0% slippage, because we will only - // return non-slippage quotes from off-chain makers. - errorText = t('swapLowSlippageError'); - } else if ( - Number(customValue) >= 5 && - Number(customValue) <= maxAllowedSlippage - ) { - errorText = t('swapHighSlippageWarning'); - } else if (Number(customValue) > maxAllowedSlippage) { - errorText = t('swapsExcessiveSlippageWarning'); - } - } - - const customValueText = customValue || t('swapCustom'); - - useEffect(() => { - if ( - inputRef && - enteringCustomValue && - window.document.activeElement !== inputRef - ) { - inputRef.focus(); - } - }, [inputRef, enteringCustomValue]); - - return ( -
- -
- {open && ( - <> - {!isDirectWrappingEnabled && ( -
-
- - {t('swapsMaxSlippage')} - - -
- - - - - -
- )} - - )} - {errorText && ( - - {errorText} - - )} -
-
- ); -} - -SlippageButtons.propTypes = { - onSelect: PropTypes.func.isRequired, - maxAllowedSlippage: PropTypes.number.isRequired, - currentSlippage: PropTypes.number, - isDirectWrappingEnabled: PropTypes.bool, -}; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js deleted file mode 100644 index 68cfd8762f44..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.stories.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import SlippageButtons from './slippage-buttons'; - -export default { - title: 'Pages/Swaps/SlippageButtons', -}; - -export const DefaultStory = () => ( -
- -
-); - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js b/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js deleted file mode 100644 index 7834eefd59ea..000000000000 --- a/ui/pages/swaps/slippage-buttons/slippage-buttons.test.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -import { renderWithProvider, fireEvent } from '../../../../test/jest'; -import { Slippage } from '../../../../shared/constants/swaps'; -import SlippageButtons from './slippage-buttons'; - -const createProps = (customProps = {}) => { - return { - onSelect: jest.fn(), - maxAllowedSlippage: 15, - currentSlippage: Slippage.high, - smartTransactionsEnabled: false, - ...customProps, - }; -}; - -describe('SlippageButtons', () => { - it('renders the component with initial props', () => { - const { getByText, queryByText, getByTestId } = renderWithProvider( - , - ); - expect(getByText('2%')).toBeInTheDocument(); - expect(getByText('3%')).toBeInTheDocument(); - expect(getByText('custom')).toBeInTheDocument(); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect( - document.querySelector('.slippage-buttons__header'), - ).toMatchSnapshot(); - expect( - document.querySelector('.slippage-buttons__button-group'), - ).toMatchSnapshot(); - expect(queryByText('Smart Swaps')).not.toBeInTheDocument(); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('renders slippage with a custom value', () => { - const { getByText } = renderWithProvider( - , - ); - expect(getByText('2.5')).toBeInTheDocument(); - }); - - it('renders the default slippage with Advanced options hidden', () => { - const { getByText, queryByText } = renderWithProvider( - , - ); - expect(getByText('Advanced options')).toBeInTheDocument(); - expect(document.querySelector('.fa-angle-down')).toBeInTheDocument(); - expect(queryByText('2%')).not.toBeInTheDocument(); - }); - - it('opens the Advanced options section and sets a default slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - , - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button0')); - expect(getByTestId('button-group__button0')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('opens the Advanced options section and sets a high slippage', () => { - const { getByText, getByTestId } = renderWithProvider( - , - ); - fireEvent.click(getByText('Advanced options')); - fireEvent.click(getByTestId('button-group__button1')); - expect(getByTestId('button-group__button1')).toHaveAttribute( - 'aria-checked', - 'true', - ); - }); - - it('sets a custom slippage value', () => { - const { getByTestId } = renderWithProvider( - , - ); - fireEvent.click(getByTestId('button-group__button2')); - expect(getByTestId('button-group__button2')).toHaveAttribute( - 'aria-checked', - 'true', - ); - const input = getByTestId('slippage-buttons__custom-slippage'); - fireEvent.change(input, { target: { value: 5 } }); - fireEvent.click(document); - expect(input).toHaveAttribute('value', '5'); - }); -}); diff --git a/ui/pages/swaps/smart-transaction-status/index.scss b/ui/pages/swaps/smart-transaction-status/index.scss index d19add085c65..f21e3e7752bf 100644 --- a/ui/pages/swaps/smart-transaction-status/index.scss +++ b/ui/pages/swaps/smart-transaction-status/index.scss @@ -70,4 +70,16 @@ &__remaining-time { font-variant-numeric: tabular-nums; } + + &__icon, + &__icon-fallback { + height: 16px; + width: 16px; + } + + &__icon-fallback { + padding-top: 0; + font-size: 12px; + line-height: 16px; + } } diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index e6a77f9474fb..d6e7f4d653cf 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -26,7 +26,7 @@ import { import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { DEFAULT_ROUTE, - BUILD_QUOTE_ROUTE, + PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { Text } from '../../../components/component-library'; import Box from '../../../components/ui/box'; @@ -323,12 +323,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsSourceTokenInfo.iconUrl ? ( ) : null} @@ -337,12 +337,12 @@ export default function SmartTransactionStatusPage() { {fetchParamsDestinationTokenInfo.iconUrl ? ( ) : null} { diff --git a/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap deleted file mode 100644 index 5116743379bc..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote-price-difference.test.js.snap +++ /dev/null @@ -1,233 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`View Price Quote Difference displays a fiat error when calculationError is present 1`] = ` -
-
-
- -
-
-
-
-
- Check your rate before proceeding -
-
-
- -
-
-
- Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping. -
- -
-
-
-
-
-
-
-`; - -exports[`View Price Quote Difference displays an error when in high bucket 1`] = ` -
-
-
- -
-
-
-
-
- Price difference of ~% -
-
-
- -
-
-
- You are about to swap 1 ETH (~) for 42.947749 LINK (~). -
- -
-
-
-
-
-
-
-`; - -exports[`View Price Quote Difference displays an error when in medium bucket 1`] = ` -
-
-
- -
-
-
-
-
- Price difference of ~% -
-
-
- -
-
-
- You are about to swap 1 ETH (~) for 42.947749 LINK (~). -
- -
-
-
-
-
-
-
-`; - -exports[`View Price Quote Difference should match snapshot 1`] = ` -
-
-
- -
-
-
-
-
- Price difference of ~% -
-
-
- -
-
-
- You are about to swap 1 ETH (~) for 42.947749 LINK (~). -
- -
-
-
-
-
-
-
-`; diff --git a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap b/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap deleted file mode 100644 index 3f6273f9f738..000000000000 --- a/ui/pages/swaps/view-quote/__snapshots__/view-quote.test.js.snap +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ViewQuote renders the component with EIP-1559 enabled 1`] = ` -
- - 10 - -
- DAI -
- - DAI - -
-`; - -exports[`ViewQuote renders the component with EIP-1559 enabled 2`] = ` -
-
- - -
-
-`; - -exports[`ViewQuote renders the component with initial props 1`] = ` -
- - 10 - -
- DAI -
- - DAI - -
-`; - -exports[`ViewQuote renders the component with initial props 2`] = ` -
-
- - -
-
-`; diff --git a/ui/pages/swaps/view-quote/index.js b/ui/pages/swaps/view-quote/index.js deleted file mode 100644 index 4a412aa905fe..000000000000 --- a/ui/pages/swaps/view-quote/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './view-quote'; diff --git a/ui/pages/swaps/view-quote/index.scss b/ui/pages/swaps/view-quote/index.scss deleted file mode 100644 index f96451cc6f4c..000000000000 --- a/ui/pages/swaps/view-quote/index.scss +++ /dev/null @@ -1,179 +0,0 @@ -@use "design-system"; - -.view-quote { - display: flex; - flex-flow: column; - align-items: center; - flex: 1; - width: 100%; - - &::after { // Hide preloaded images. - position: absolute; - width: 0; - height: 0; - overflow: hidden; - z-index: -1; - content: url('/images/transaction-background-top.svg') url('/images/transaction-background-bottom.svg'); // Preload images for the STX status page. - } - - &__content { - display: flex; - flex-flow: column; - align-items: center; - width: 100%; - height: 100%; - padding-left: 20px; - padding-right: 20px; - - &_modal > div:not(.view-quote__warning-wrapper) { - opacity: 0.6; - pointer-events: none; - } - - @include design-system.screen-sm-max { - overflow-y: auto; - max-height: 420px; - } - } - - @include design-system.screen-sm-min { - width: 348px; - } - - &__price-difference-warning { - &-wrapper { - width: 100%; - - &.low, - &.medium, - &.high { - .actionable-message { - &::before { - background: none; - } - - .actionable-message__message { - color: inherit; - } - - button { - font-size: design-system.$font-size-h8; - padding: 4px 12px; - border-radius: 42px; - } - } - } - - &.low { - .actionable-message { - button { - background: var(--color-primary-default); - color: var(--color-primary-inverse); - } - } - } - - &.medium { - .actionable-message { - border-color: var(--color-warning-default); - background: var(--color-warning-muted); - - button { - background: var(--color-warning-default); - } - } - } - - &.high { - .actionable-message { - border-color: var(--color-error-default); - background: var(--color-error-muted); - - button { - background: var(--color-error-default); - color: var(--color-error-inverse); - } - } - } - } - - &-contents { - display: flex; - text-align: left; - - &-title { - font-weight: bold; - } - - &-actions { - text-align: end; - padding-top: 10px; - } - - i { - margin-inline-start: 10px; - } - } - } - - &__warning-wrapper { - width: 100%; - align-items: center; - justify-content: center; - max-width: 340px; - margin-top: 8px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - &--thin { - min-height: 36px; - } - - display: flex; - } - } - - &__bold { - font-weight: bold; - } - - &__countdown-timer-container { - display: flex; - justify-content: center; - margin-top: 8px; - } - - &__fee-card-container { - display: flex; - align-items: center; - width: 100%; - max-width: 311px; - margin-bottom: 8px; - - @include design-system.screen-sm-min { - margin-bottom: 0; - } - } - - &__metamask-rate { - display: flex; - } - - &__metamask-rate-text { - @include design-system.H7; - - color: var(--color-text-alternative); - } - - &__metamask-rate-info-icon { - margin-left: 4px; - } - - &__thin-swaps-footer { - max-height: 82px; - - @include design-system.screen-sm-min { - height: 72px; - } - } -} diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.js b/ui/pages/swaps/view-quote/view-quote-price-difference.js deleted file mode 100644 index 2607243494d4..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useContext } from 'react'; - -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { I18nContext } from '../../../contexts/i18n'; - -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import Tooltip from '../../../components/ui/tooltip'; -import Box from '../../../components/ui/box'; -import { - JustifyContent, - DISPLAY, -} from '../../../helpers/constants/design-system'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import { Icon, IconName } from '../../../components/component-library'; - -export default function ViewQuotePriceDifference(props) { - const { - usedQuote, - sourceTokenValue, - destinationTokenValue, - onAcknowledgementClick, - acknowledged, - priceSlippageFromSource, - priceSlippageFromDestination, - priceDifferencePercentage, - priceSlippageUnknownFiatValue, - } = props; - - const t = useContext(I18nContext); - - let priceDifferenceTitle = ''; - let priceDifferenceMessage = ''; - let priceDifferenceClass = ''; - let priceDifferenceAcknowledgementText = ''; - if (priceSlippageUnknownFiatValue) { - // A calculation error signals we cannot determine dollar value - priceDifferenceTitle = t('swapPriceUnavailableTitle'); - priceDifferenceMessage = t('swapPriceUnavailableDescription'); - priceDifferenceClass = GasRecommendations.high; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } else { - priceDifferenceTitle = t('swapPriceDifferenceTitle', [ - priceDifferencePercentage, - ]); - priceDifferenceMessage = t('swapPriceDifference', [ - sourceTokenValue, // Number of source token to swap - usedQuote.sourceTokenInfo.symbol, // Source token symbol - priceSlippageFromSource, // Source tokens total value - destinationTokenValue, // Number of destination tokens in return - usedQuote.destinationTokenInfo.symbol, // Destination token symbol, - priceSlippageFromDestination, // Destination tokens total value - ]); - priceDifferenceClass = usedQuote.priceSlippage.bucket; - priceDifferenceAcknowledgementText = t('tooltipApproveButton'); - } - - return ( -
- -
- -
- {priceDifferenceTitle} -
- - - -
- {priceDifferenceMessage} - {!acknowledged && ( -
- -
- )} -
-
- } - /> -
- ); -} - -ViewQuotePriceDifference.propTypes = { - usedQuote: PropTypes.object, - sourceTokenValue: PropTypes.string, - destinationTokenValue: PropTypes.string, - onAcknowledgementClick: PropTypes.func, - acknowledged: PropTypes.bool, - priceSlippageFromSource: PropTypes.string, - priceSlippageFromDestination: PropTypes.string, - priceDifferencePercentage: PropTypes.number, - priceSlippageUnknownFiatValue: PropTypes.bool, -}; diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js deleted file mode 100644 index 5702f39f67fe..000000000000 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -describe('View Price Quote Difference', () => { - const mockState = { - metamask: { - tokens: [], - preferences: { showFiatInTestnets: true }, - currentCurrency: 'usd', - currencyRates: { - ETH: { - conversionRate: 600.0, - }, - }, - }, - }; - - const mockStore = configureMockStore()(mockState); - - // Sample transaction is 1 $ETH to ~42.880915 $LINK - const DEFAULT_PROPS = { - usedQuote: { - trade: { - data: '0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000007756e69737761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca0000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000024855454cb32d335f0000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000005fc7b7100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f161421c8e0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca', - from: '0xd7440fdcb70a9fba55dfe06942ddbc17679c90ac', - value: '0xde0b6b3a7640000', - gas: '0xbbfd0', - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', - }, - sourceAmount: '1000000000000000000', - destinationAmount: '42947749216634160067', - error: null, - sourceToken: '0x0000000000000000000000000000000000000000', - destinationToken: '0x514910771af9ca656af840dff83e8264ecf986ca', - approvalNeeded: null, - maxGas: 770000, - averageGas: 210546, - estimatedRefund: 80000, - fetchTime: 647, - aggregator: 'uniswap', - aggType: 'DEX', - fee: 0.875, - gasMultiplier: 1.5, - priceSlippage: { - ratio: 1.007876641534847, - calculationError: '', - bucket: GasRecommendations.low, - sourceAmountInETH: 1, - destinationAmountInETH: 0.9921849150875727, - }, - slippage: 2, - sourceTokenInfo: { - symbol: 'ETH', - name: 'Ether', - address: '0x0000000000000000000000000000000000000000', - decimals: 18, - iconUrl: 'images/black-eth-logo.png', - }, - destinationTokenInfo: { - address: '0x514910771af9ca656af840dff83e8264ecf986ca', - symbol: 'LINK', - decimals: 18, - occurances: 12, - iconUrl: - 'https://cloudflare-ipfs.com/ipfs/QmQhZAdcZvW9T2tPm516yHqbGkfhyZwTZmLixW9MXJudTA', - }, - ethFee: '0.011791', - ethValueOfTokens: '0.99220724791716534441', - overallValueOfQuote: '0.98041624791716534441', - metaMaskFeeInEth: '0.00875844985551091729', - isBestQuote: true, - savings: { - performance: '0.00207907025112527799', - fee: '0.005581', - metaMaskFee: '0.00875844985551091729', - total: '-0.0010983796043856393', - medianMetaMaskFee: '0.00874009740688812165', - }, - }, - sourceTokenValue: '1', - destinationTokenValue: '42.947749', - }; - - it('should match snapshot', () => { - const { container } = renderWithProvider( - , - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in medium bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.medium; - - const { container } = renderWithProvider( - , - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays an error when in high bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GasRecommendations.high; - - const { container } = renderWithProvider( - , - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); - - it('displays a fiat error when calculationError is present', () => { - const props = { ...DEFAULT_PROPS, priceSlippageUnknownFiatValue: true }; - props.usedQuote.priceSlippage.calculationError = - 'Could not determine price.'; - - const { container } = renderWithProvider( - , - mockStore, - ); - - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js deleted file mode 100644 index be02ba840eb5..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.js +++ /dev/null @@ -1,1089 +0,0 @@ -import React, { - useState, - useContext, - useMemo, - useEffect, - useRef, - useCallback, -} from 'react'; -import { shallowEqual, useSelector, useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import BigNumber from 'bignumber.js'; -import { isEqual } from 'lodash'; -import classnames from 'classnames'; -import { captureException } from '@sentry/browser'; - -import { I18nContext } from '../../../contexts/i18n'; -import SelectQuotePopover from '../select-quote-popover'; -import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; -import { usePrevious } from '../../../hooks/usePrevious'; -import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import FeeCard from '../fee-card'; -import { - getQuotes, - getApproveTxParams, - getFetchParams, - setBalanceError, - getQuotesLastFetched, - getBalanceError, - getCustomSwapsGas, // Gas limit. - getCustomMaxFeePerGas, - getCustomMaxPriorityFeePerGas, - getSwapsUserFeeLevel, - getDestinationTokenInfo, - getUsedSwapsGasPrice, - getTopQuote, - getUsedQuote, - signAndSendTransactions, - getBackgroundSwapRouteState, - swapsQuoteSelected, - getSwapsQuoteRefreshTime, - getReviewSwapClickedTimestamp, - signAndSendSwapsSmartTransaction, - getSwapsNetworkConfig, - getSmartTransactionsError, - getCurrentSmartTransactionsError, - getSwapsSTXLoading, - fetchSwapsSmartTransactionFees, - getSmartTransactionFees, - getCurrentSmartTransactionsEnabled, -} from '../../../ducks/swaps/swaps'; -import { - conversionRateSelector, - getSelectedAccount, - getCurrentCurrency, - getTokenExchangeRates, - getSwapsDefaultToken, - getCurrentChainId, - isHardwareWallet, - getHardwareWalletType, - checkNetworkAndAccountSupports1559, - getUSDConversionRate, -} from '../../../selectors'; -import { - getSmartTransactionsOptInStatus, - getSmartTransactionsEnabled, -} from '../../../../shared/modules/selectors'; -import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; -import { - getLayer1GasFee, - safeRefetchQuotes, - setCustomApproveTxData, - setSwapsErrorKey, - showModal, - setSwapsQuotesPollingLimitEnabled, -} from '../../../store/actions'; -import { SET_SMART_TRANSACTIONS_ERROR } from '../../../store/actionConstants'; -import { - ASSET_ROUTE, - BUILD_QUOTE_ROUTE, - DEFAULT_ROUTE, - SWAPS_ERROR_ROUTE, - AWAITING_SWAP_ROUTE, -} from '../../../helpers/constants/routes'; -import MainQuoteSummary from '../main-quote-summary'; -import { getCustomTxParamsData } from '../../confirmations/confirm-approve/confirm-approve.util'; -import ActionableMessage from '../../../components/ui/actionable-message/actionable-message'; -import { - quotesToRenderableData, - getRenderableNetworkFeesForQuote, - getFeeForSmartTransaction, -} from '../swaps.util'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { QUOTES_EXPIRED_ERROR } from '../../../../shared/constants/swaps'; -import { GasRecommendations } from '../../../../shared/constants/gas'; -import CountdownTimer from '../countdown-timer'; -import SwapsFooter from '../swaps-footer'; -import PulseLoader from '../../../components/ui/pulse-loader'; // TODO: Replace this with a different loading component. -import Box from '../../../components/ui/box'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { parseStandardTokenTransactionData } from '../../../../shared/modules/transaction.utils'; -import { getTokenValueParam } from '../../../../shared/lib/metamask-controller-utils'; -import { - calcGasTotal, - calcTokenAmount, - toPrecisionWithoutTrailingZeros, -} from '../../../../shared/lib/transactions-controller-utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { - calcTokenValue, - calculateMaxGasLimit, -} from '../../../../shared/lib/swaps-utils'; -import { - addHexes, - decGWEIToHexWEI, - decimalToHex, - decWEIToDecETH, - hexWEIToDecGWEI, - sumHexes, -} from '../../../../shared/modules/conversion.utils'; -import ViewQuotePriceDifference from './view-quote-price-difference'; - -let intervalId; - -export default function ViewQuote() { - const history = useHistory(); - const dispatch = useDispatch(); - const t = useContext(I18nContext); - const trackEvent = useContext(MetaMetricsContext); - - const [dispatchedSafeRefetch, setDispatchedSafeRefetch] = useState(false); - const [submitClicked, setSubmitClicked] = useState(false); - const [selectQuotePopoverShown, setSelectQuotePopoverShown] = useState(false); - const [warningHidden, setWarningHidden] = useState(false); - const [originalApproveAmount, setOriginalApproveAmount] = useState(null); - const [multiLayerL1FeeTotal, setMultiLayerL1FeeTotal] = useState(null); - const [multiLayerL1ApprovalFeeTotal, setMultiLayerL1ApprovalFeeTotal] = - useState(null); - // We need to have currentTimestamp in state, otherwise it would change with each rerender. - const [currentTimestamp] = useState(Date.now()); - - const [acknowledgedPriceDifference, setAcknowledgedPriceDifference] = - useState(false); - const priceDifferenceRiskyBuckets = [ - GasRecommendations.high, - GasRecommendations.medium, - ]; - - const routeState = useSelector(getBackgroundSwapRouteState); - const quotes = useSelector(getQuotes, isEqual); - useEffect(() => { - if (!Object.values(quotes).length) { - history.push(BUILD_QUOTE_ROUTE); - } else if (routeState === 'awaiting') { - history.push(AWAITING_SWAP_ROUTE); - } - }, [history, quotes, routeState]); - - const quotesLastFetched = useSelector(getQuotesLastFetched); - - // Select necessary data - const gasPrice = useSelector(getUsedSwapsGasPrice); - const customMaxGas = useSelector(getCustomSwapsGas); - const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas); - const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas); - const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); - const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); - const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); - const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); - const conversionRate = useSelector(conversionRateSelector); - const USDConversionRate = useSelector(getUSDConversionRate); - const currentCurrency = useSelector(getCurrentCurrency); - const swapsTokens = useSelector(getTokens, isEqual); - const networkAndAccountSupports1559 = useSelector( - checkNetworkAndAccountSupports1559, - ); - const balanceError = useSelector(getBalanceError); - const fetchParams = useSelector(getFetchParams, isEqual); - const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = useSelector(getUsedQuote, isEqual); - const tradeValue = usedQuote?.trade?.value ?? '0x0'; - const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); - const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); - const chainId = useSelector(getCurrentChainId); - const nativeCurrencySymbol = useSelector(getNativeCurrency); - const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); - const smartTransactionsOptInStatus = useSelector( - getSmartTransactionsOptInStatus, - ); - const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); - const swapsSTXLoading = useSelector(getSwapsSTXLoading); - const currentSmartTransactionsError = useSelector( - getCurrentSmartTransactionsError, - ); - const smartTransactionsError = useSelector(getSmartTransactionsError); - const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); - const currentSmartTransactionsEnabled = useSelector( - getCurrentSmartTransactionsEnabled, - ); - const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); - const unsignedTransaction = usedQuote.trade; - const isSmartTransaction = - currentSmartTransactionsEnabled && smartTransactionsOptInStatus; - - let gasFeeInputs; - if (networkAndAccountSupports1559) { - // For Swaps we want to get 'high' estimations by default. - // eslint-disable-next-line react-hooks/rules-of-hooks - gasFeeInputs = useGasFeeInputs(GasRecommendations.high, { - userFeeLevel: swapsUserFeeLevel || GasRecommendations.high, - }); - } - - const fetchParamsSourceToken = fetchParams?.sourceToken; - - const additionalTrackingParams = { - reg_tx_fee_in_usd: undefined, - reg_tx_fee_in_eth: undefined, - reg_tx_max_fee_in_usd: undefined, - reg_tx_max_fee_in_eth: undefined, - stx_fee_in_usd: undefined, - stx_fee_in_eth: undefined, - stx_max_fee_in_usd: undefined, - stx_max_fee_in_eth: undefined, - }; - - const usedGasLimit = - usedQuote?.gasEstimateWithRefund || - `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - - const maxGasLimit = calculateMaxGasLimit( - usedQuote?.gasEstimate, - usedQuote?.gasMultiplier, - usedQuote?.maxGas, - customMaxGas, - ); - - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - - // EIP-1559 gas fees. - if (networkAndAccountSupports1559) { - const { - maxFeePerGas: suggestedMaxFeePerGas, - maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates, - } = gasFeeInputs; - const estimatedBaseFee = gasFeeEstimates?.estimatedBaseFee ?? '0'; - maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decGWEIToHexWEI(estimatedBaseFee), - maxPriorityFeePerGas, - ); - } - let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); - if (multiLayerL1FeeTotal !== null) { - gasTotalInWeiHex = sumHexes( - gasTotalInWeiHex || '0x0', - multiLayerL1FeeTotal || '0x0', - ); - } - - const { tokensWithBalances } = useTokenTracker({ - tokens: swapsTokens, - includeFailedTokens: true, - }); - const balanceToken = - fetchParamsSourceToken === defaultSwapsToken.address - ? defaultSwapsToken - : tokensWithBalances.find(({ address }) => - isEqualCaseInsensitive(address, fetchParamsSourceToken), - ); - - const selectedFromToken = balanceToken || usedQuote.sourceTokenInfo; - const tokenBalance = - tokensWithBalances?.length && - calcTokenAmount( - selectedFromToken.balance || '0x0', - selectedFromToken.decimals, - ).toFixed(9); - const tokenBalanceUnavailable = - tokensWithBalances && balanceToken === undefined; - - const approveData = parseStandardTokenTransactionData(approveTxParams?.data); - const approveValue = approveData && getTokenValueParam(approveData); - const approveAmount = - approveValue && - selectedFromToken?.decimals !== undefined && - calcTokenAmount(approveValue, selectedFromToken.decimals).toFixed(9); - const approveGas = approveTxParams?.gas; - - const renderablePopoverData = useMemo(() => { - return quotesToRenderableData({ - quotes, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - conversionRate, - currentCurrency, - approveGas, - tokenConversionRates: memoizedTokenConversionRates, - chainId, - smartTransactionEstimatedGas: - smartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - multiLayerL1ApprovalFeeTotal, - }); - }, [ - quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, - conversionRate, - currentCurrency, - approveGas, - memoizedTokenConversionRates, - chainId, - smartTransactionFees?.tradeTxFees, - nativeCurrencySymbol, - smartTransactionsEnabled, - smartTransactionsOptInStatus, - multiLayerL1ApprovalFeeTotal, - ]); - - const renderableDataForUsedQuote = renderablePopoverData.find( - (renderablePopoverDatum) => - renderablePopoverDatum.aggId === usedQuote.aggregator, - ); - - const { - destinationTokenDecimals, - destinationTokenSymbol, - destinationTokenValue, - destinationIconUrl, - sourceTokenDecimals, - sourceTokenSymbol, - sourceTokenValue, - sourceTokenIconUrl, - } = renderableDataForUsedQuote; - - let { feeInFiat, feeInEth, rawEthFee, feeInUsd } = - getRenderableNetworkFeesForQuote({ - tradeGas: usedGasLimit, - approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee); - - const renderableMaxFees = getRenderableNetworkFeesForQuote({ - tradeGas: maxGasLimit, - approveGas, - gasPrice: maxFeePerGas || gasPrice, - currentCurrency, - conversionRate, - USDConversionRate, - tradeValue, - sourceSymbol: sourceTokenSymbol, - sourceAmount: usedQuote.sourceAmount, - chainId, - nativeCurrencySymbol, - multiLayerL1FeeTotal, - }); - let { - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = renderableMaxFees; - additionalTrackingParams.reg_tx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.reg_tx_max_fee_in_eth = Number(maxRawEthFee); - - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - const stxEstimatedFeeInWeiDec = - smartTransactionFees?.tradeTxFees.feeEstimate + - (smartTransactionFees?.approvalTxFees?.feeEstimate || 0); - const stxMaxFeeInWeiDec = - stxEstimatedFeeInWeiDec * swapsNetworkConfig.stxMaxFeeMultiplier; - ({ feeInFiat, feeInEth, rawEthFee, feeInUsd } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxEstimatedFeeInWeiDec, - })); - additionalTrackingParams.stx_fee_in_usd = Number(feeInUsd); - additionalTrackingParams.stx_fee_in_eth = Number(rawEthFee); - additionalTrackingParams.estimated_gas = - smartTransactionFees?.tradeTxFees.gasLimit; - ({ - feeInFiat: maxFeeInFiat, - feeInEth: maxFeeInEth, - rawEthFee: maxRawEthFee, - feeInUsd: maxFeeInUsd, - } = getFeeForSmartTransaction({ - chainId, - currentCurrency, - conversionRate, - USDConversionRate, - nativeCurrencySymbol, - feeInWeiDec: stxMaxFeeInWeiDec, - })); - additionalTrackingParams.stx_max_fee_in_usd = Number(maxFeeInUsd); - additionalTrackingParams.stx_max_fee_in_eth = Number(maxRawEthFee); - } - - const tokenCost = new BigNumber(usedQuote.sourceAmount); - const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus( - new BigNumber(gasTotalInWeiHex, 16), - ); - - const insufficientTokens = - (tokensWithBalances?.length || balanceError) && - tokenCost.gt(new BigNumber(selectedFromToken.balance || '0x0')); - - const insufficientEth = ethCost.gt(new BigNumber(ethBalance || '0x0')); - - const tokenBalanceNeeded = insufficientTokens - ? toPrecisionWithoutTrailingZeros( - calcTokenAmount(tokenCost, selectedFromToken.decimals) - .minus(tokenBalance) - .toString(10), - 6, - ) - : null; - - const ethBalanceNeeded = insufficientEth - ? toPrecisionWithoutTrailingZeros( - ethCost - .minus(ethBalance, 16) - .div('1000000000000000000', 10) - .toString(10), - 6, - ) - : null; - - let ethBalanceNeededStx; - if (isSmartTransaction && smartTransactionsError?.balanceNeededWei) { - ethBalanceNeededStx = decWEIToDecETH( - smartTransactionsError.balanceNeededWei - - smartTransactionsError.currentBalanceWei, - ); - } - - const destinationToken = useSelector(getDestinationTokenInfo, isEqual); - useEffect(() => { - if (isSmartTransaction) { - if (insufficientTokens) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens) { - dispatch(setBalanceError(false)); - } - } else if (insufficientTokens || insufficientEth) { - dispatch(setBalanceError(true)); - } else if (balanceError && !insufficientTokens && !insufficientEth) { - dispatch(setBalanceError(false)); - } - }, [ - insufficientTokens, - insufficientEth, - balanceError, - dispatch, - isSmartTransaction, - ]); - - useEffect(() => { - const currentTime = Date.now(); - const timeSinceLastFetched = currentTime - quotesLastFetched; - if ( - timeSinceLastFetched > swapsQuoteRefreshTime && - !dispatchedSafeRefetch - ) { - setDispatchedSafeRefetch(true); - dispatch(safeRefetchQuotes()); - } else if (timeSinceLastFetched > swapsQuoteRefreshTime) { - dispatch(setSwapsErrorKey(QUOTES_EXPIRED_ERROR)); - history.push(SWAPS_ERROR_ROUTE); - } - }, [ - quotesLastFetched, - dispatchedSafeRefetch, - dispatch, - history, - swapsQuoteRefreshTime, - ]); - - useEffect(() => { - if (!originalApproveAmount && approveAmount) { - setOriginalApproveAmount(approveAmount); - } - }, [originalApproveAmount, approveAmount]); - - // If it's not a Smart Transaction and ETH balance is needed, we want to show a warning. - const isNotStxAndEthBalanceIsNeeded = - (!currentSmartTransactionsEnabled || !smartTransactionsOptInStatus) && - ethBalanceNeeded; - - // If it's a Smart Transaction and ETH balance is needed, we want to show a warning. - const isStxAndEthBalanceIsNeeded = isSmartTransaction && ethBalanceNeededStx; - - // Indicates if we should show to a user a warning about insufficient funds for swapping. - const showInsufficientWarning = - (balanceError || - tokenBalanceNeeded || - isNotStxAndEthBalanceIsNeeded || - isStxAndEthBalanceIsNeeded) && - !warningHidden; - - const hardwareWalletUsed = useSelector(isHardwareWallet); - const hardwareWalletType = useSelector(getHardwareWalletType); - - const numberOfQuotes = Object.values(quotes).length; - const bestQuoteReviewedEventSent = useRef(); - const eventObjectBase = useMemo(() => { - return { - token_from: sourceTokenSymbol, - token_from_amount: sourceTokenValue, - token_to: destinationTokenSymbol, - token_to_amount: destinationTokenValue, - request_type: fetchParams?.balanceError, - slippage: fetchParams?.slippage, - custom_slippage: fetchParams?.slippage !== 2, - response_time: fetchParams?.responseTime, - best_quote_source: topQuote?.aggregator, - available_quotes: numberOfQuotes, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - stx_enabled: smartTransactionsEnabled, - current_stx_enabled: currentSmartTransactionsEnabled, - stx_user_opt_in: smartTransactionsOptInStatus, - }; - }, [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams?.balanceError, - fetchParams?.slippage, - fetchParams?.responseTime, - topQuote?.aggregator, - numberOfQuotes, - hardwareWalletUsed, - hardwareWalletType, - smartTransactionsEnabled, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - ]); - - const trackAllAvailableQuotesOpened = () => { - trackEvent({ - event: 'All Available Quotes Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackQuoteDetailsOpened = () => { - trackEvent({ - event: 'Quote Details Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - other_quote_selected: usedQuote?.aggregator !== topQuote?.aggregator, - other_quote_selected_source: - usedQuote?.aggregator === topQuote?.aggregator - ? null - : usedQuote?.aggregator, - }, - }); - }; - const trackEditSpendLimitOpened = () => { - trackEvent({ - event: 'Edit Spend Limit Opened', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - custom_spend_limit_set: originalApproveAmount === approveAmount, - custom_spend_limit_amount: - originalApproveAmount === approveAmount ? null : approveAmount, - }, - }); - }; - const trackBestQuoteReviewedEvent = useCallback(() => { - trackEvent({ - event: 'Best Quote Reviewed', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - network_fees: feeInFiat, - }, - }); - }, [trackEvent, eventObjectBase, feeInFiat]); - const trackViewQuotePageLoadedEvent = useCallback(() => { - trackEvent({ - event: 'View Quote Page Loaded', - category: MetaMetricsEventCategory.Swaps, - sensitiveProperties: { - ...eventObjectBase, - response_time: currentTimestamp - reviewSwapClickedTimestamp, - }, - }); - }, [ - trackEvent, - eventObjectBase, - currentTimestamp, - reviewSwapClickedTimestamp, - ]); - - useEffect(() => { - if ( - !bestQuoteReviewedEventSent.current && - [ - sourceTokenSymbol, - sourceTokenValue, - destinationTokenSymbol, - destinationTokenValue, - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - ].every((dep) => dep !== null && dep !== undefined) - ) { - bestQuoteReviewedEventSent.current = true; - trackBestQuoteReviewedEvent(); - } - }, [ - fetchParams, - topQuote, - numberOfQuotes, - feeInFiat, - destinationTokenSymbol, - destinationTokenValue, - sourceTokenSymbol, - sourceTokenValue, - trackBestQuoteReviewedEvent, - ]); - - const metaMaskFee = usedQuote.fee; - - /* istanbul ignore next */ - const onFeeCardTokenApprovalClick = () => { - trackEditSpendLimitOpened(); - dispatch( - showModal({ - name: 'EDIT_APPROVAL_PERMISSION', - decimals: selectedFromToken.decimals, - origin: 'MetaMask', - setCustomAmount: (newCustomPermissionAmount) => { - const customPermissionAmount = - newCustomPermissionAmount === '' - ? originalApproveAmount - : newCustomPermissionAmount; - const newData = getCustomTxParamsData(approveTxParams.data, { - customPermissionAmount, - decimals: selectedFromToken.decimals, - }); - - if ( - customPermissionAmount?.length && - approveTxParams.data !== newData - ) { - dispatch(setCustomApproveTxData(newData)); - } - }, - tokenAmount: originalApproveAmount, - customTokenAmount: - originalApproveAmount === approveAmount ? null : approveAmount, - tokenBalance, - tokenSymbol: selectedFromToken.symbol, - requiredMinimum: calcTokenAmount( - usedQuote.sourceAmount, - selectedFromToken.decimals, - ), - }), - ); - }; - const actionableBalanceErrorMessage = tokenBalanceUnavailable - ? t('swapTokenBalanceUnavailable', [sourceTokenSymbol]) - : t('swapApproveNeedMoreTokens', [ - - {tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded} - , - tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) - ? sourceTokenSymbol - : defaultSwapsToken.symbol, - ]); - - // Price difference warning - const priceSlippageBucket = usedQuote?.priceSlippage?.bucket; - const lastPriceDifferenceBucket = usePrevious(priceSlippageBucket); - - // If the user agreed to a different bucket of risk, make them agree again - useEffect(() => { - if ( - acknowledgedPriceDifference && - lastPriceDifferenceBucket === GasRecommendations.medium && - priceSlippageBucket === GasRecommendations.high - ) { - setAcknowledgedPriceDifference(false); - } - }, [ - priceSlippageBucket, - acknowledgedPriceDifference, - lastPriceDifferenceBucket, - ]); - - let viewQuotePriceDifferenceComponent = null; - const priceSlippageFromSource = useEthFiatAmount( - usedQuote?.priceSlippage?.sourceAmountInETH || 0, - { showFiat: true }, - ); - const priceSlippageFromDestination = useEthFiatAmount( - usedQuote?.priceSlippage?.destinationAmountInETH || 0, - { showFiat: true }, - ); - - // We cannot present fiat value if there is a calculation error or no slippage - // from source or destination - const priceSlippageUnknownFiatValue = - !priceSlippageFromSource || - !priceSlippageFromDestination || - Boolean(usedQuote?.priceSlippage?.calculationError); - - let priceDifferencePercentage = 0; - if (usedQuote?.priceSlippage?.ratio) { - priceDifferencePercentage = parseFloat( - new BigNumber(usedQuote.priceSlippage.ratio, 10) - .minus(1, 10) - .times(100, 10) - .toFixed(2), - 10, - ); - } - - const shouldShowPriceDifferenceWarning = - !tokenBalanceUnavailable && - !showInsufficientWarning && - usedQuote && - (priceDifferenceRiskyBuckets.includes(priceSlippageBucket) || - priceSlippageUnknownFiatValue); - - if (shouldShowPriceDifferenceWarning) { - viewQuotePriceDifferenceComponent = ( - { - setAcknowledgedPriceDifference(true); - }} - acknowledged={acknowledgedPriceDifference} - /> - ); - } - - const disableSubmissionDueToPriceWarning = - shouldShowPriceDifferenceWarning && !acknowledgedPriceDifference; - - const isShowingWarning = - showInsufficientWarning || shouldShowPriceDifferenceWarning; - - const isSwapButtonDisabled = Boolean( - submitClicked || - balanceError || - tokenBalanceUnavailable || - disableSubmissionDueToPriceWarning || - (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || - (!networkAndAccountSupports1559 && - (gasPrice === null || gasPrice === undefined)) || - (currentSmartTransactionsEnabled && - (currentSmartTransactionsError || smartTransactionsError)) || - (currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees), - ); - - useEffect(() => { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !insufficientTokens - ) { - const unsignedTx = { - from: unsignedTransaction.from, - to: unsignedTransaction.to, - value: unsignedTransaction.value, - data: unsignedTransaction.data, - gas: unsignedTransaction.gas, - chainId, - }; - intervalId = setInterval(() => { - if (!swapsSTXLoading) { - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } - }, swapsNetworkConfig.stxGetTransactionsRefreshTime); - dispatch( - fetchSwapsSmartTransactionFees({ - unsignedTransaction: unsignedTx, - approveTxParams, - fallbackOnNotEnoughFunds: false, - }), - ); - } else if (intervalId) { - clearInterval(intervalId); - } - return () => clearInterval(intervalId); - // eslint-disable-next-line - }, [ - dispatch, - currentSmartTransactionsEnabled, - smartTransactionsOptInStatus, - unsignedTransaction.data, - unsignedTransaction.from, - unsignedTransaction.value, - unsignedTransaction.gas, - unsignedTransaction.to, - chainId, - swapsNetworkConfig.stxGetTransactionsRefreshTime, - insufficientTokens, - ]); - - useEffect(() => { - // Thanks to the next line we will only do quotes polling 3 times before showing a Quote Timeout modal. - dispatch(setSwapsQuotesPollingLimitEnabled(true)); - if (reviewSwapClickedTimestamp) { - trackViewQuotePageLoadedEvent(); - } - }, [dispatch, trackViewQuotePageLoadedEvent, reviewSwapClickedTimestamp]); - - useEffect(() => { - // if smart transaction error is turned off, reset submit clicked boolean - if ( - !currentSmartTransactionsEnabled && - currentSmartTransactionsError && - submitClicked - ) { - setSubmitClicked(false); - } - }, [ - currentSmartTransactionsEnabled, - currentSmartTransactionsError, - submitClicked, - ]); - - useEffect(() => { - if (!usedQuote?.multiLayerL1TradeFeeTotal) { - return; - } - const getEstimatedL1Fees = async () => { - try { - let l1ApprovalFeeTotal = '0x0'; - if (approveTxParams) { - l1ApprovalFeeTotal = await dispatch( - getLayer1GasFee({ - transactionParams: { - ...approveTxParams, - gasPrice: addHexPrefix(approveTxParams.gasPrice), - value: '0x0', // For approval txs we need to use "0x0" here. - }, - chainId, - }), - ); - setMultiLayerL1ApprovalFeeTotal(l1ApprovalFeeTotal); - } - const l1FeeTotal = sumHexes( - usedQuote.multiLayerL1TradeFeeTotal, - l1ApprovalFeeTotal, - ); - setMultiLayerL1FeeTotal(l1FeeTotal); - } catch (e) { - captureException(e); - setMultiLayerL1FeeTotal(null); - setMultiLayerL1ApprovalFeeTotal(null); - } - }; - getEstimatedL1Fees(); - }, [unsignedTransaction, approveTxParams, chainId, usedQuote]); - - useEffect(() => { - if (isSmartTransaction) { - // Removes a smart transactions error when the component loads. - dispatch({ - type: SET_SMART_TRANSACTIONS_ERROR, - payload: null, - }); - } - }, [isSmartTransaction, dispatch]); - - return ( -
-
- { - /* istanbul ignore next */ - selectQuotePopoverShown && ( - setSelectQuotePopoverShown(false)} - onSubmit={(aggId) => dispatch(swapsQuoteSelected(aggId))} - swapToSymbol={destinationTokenSymbol} - initialAggId={usedQuote.aggregator} - onQuoteDetailsIsOpened={trackQuoteDetailsOpened} - hideEstimatedGasFee={ - smartTransactionsEnabled && smartTransactionsOptInStatus - } - /> - ) - } - -
- {viewQuotePriceDifferenceComponent} - {(showInsufficientWarning || tokenBalanceUnavailable) && ( - setWarningHidden(true) - } - /> - )} -
-
- -
- - {currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees && - !showInsufficientWarning && ( - - - - )} - {(!currentSmartTransactionsEnabled || - !smartTransactionsOptInStatus || - smartTransactionFees?.tradeTxFees) && ( -
- { - trackAllAvailableQuotesOpened(); - setSelectQuotePopoverShown(true); - } - } - maxPriorityFeePerGasDecGWEI={hexWEIToDecGWEI( - maxPriorityFeePerGas, - )} - maxFeePerGasDecGWEI={hexWEIToDecGWEI(maxFeePerGas)} - /> -
- )} -
- { - setSubmitClicked(true); - if (!balanceError) { - if ( - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - smartTransactionFees?.tradeTxFees - ) { - dispatch( - signAndSendSwapsSmartTransaction({ - unsignedTransaction, - trackEvent, - history, - additionalTrackingParams, - }), - ); - } else { - dispatch( - signAndSendTransactions( - history, - trackEvent, - additionalTrackingParams, - ), - ); - } - } else if (destinationToken.symbol === defaultSwapsToken.symbol) { - history.push(DEFAULT_ROUTE); - } else { - history.push(`${ASSET_ROUTE}/${destinationToken.address}`); - } - } - } - submitText={ - currentSmartTransactionsEnabled && - smartTransactionsOptInStatus && - swapsSTXLoading - ? t('preparingSwap') - : t('swap') - } - hideCancel - disabled={isSwapButtonDisabled} - className={isShowingWarning ? 'view-quote__thin-swaps-footer' : ''} - showTopBorder - /> -
- ); -} diff --git a/ui/pages/swaps/view-quote/view-quote.test.js b/ui/pages/swaps/view-quote/view-quote.test.js deleted file mode 100644 index 3193a54dfb35..000000000000 --- a/ui/pages/swaps/view-quote/view-quote.test.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { NetworkType } from '@metamask/controller-utils'; -import { NetworkStatus } from '@metamask/network-controller'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - renderWithProvider, - createSwapsMockStore, - MOCKS, -} from '../../../../test/jest'; - -import ViewQuote from '.'; - -jest.mock( - '../../../components/ui/info-tooltip/info-tooltip-icon', - () => () => '', -); - -jest.mock('../../confirmations/hooks/useGasFeeInputs', () => { - return { - useGasFeeInputs: () => { - return { - maxFeePerGas: 16, - maxPriorityFeePerGas: 3, - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - }; - }, - }; -}); - -const middleware = [thunk]; -const createProps = (customProps = {}) => { - return { - inputValue: '5', - onInputChange: jest.fn(), - ethBalance: '6 ETH', - setMaxSlippage: jest.fn(), - maxSlippage: 15, - selectedAccountAddress: 'selectedAccountAddress', - isFeatureFlagLoaded: false, - ...customProps, - }; -}; - -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - safeRefetchQuotes: jest.fn(), - setSwapsErrorKey: jest.fn(), - updateTransaction: jest.fn(), - getGasFeeTimeEstimate: jest.fn(), - setSwapsQuotesPollingLimitEnabled: jest.fn(), -}); - -describe('ViewQuote', () => { - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - , - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); - - it('renders the component with EIP-1559 enabled', () => { - const state = createSwapsMockStore(); - state.metamask.selectedNetworkClientId = NetworkType.mainnet; - state.metamask.networksMetadata = { - [NetworkType.mainnet]: { - EIPS: { 1559: true }, - status: NetworkStatus.Available, - }, - }; - const store = configureMockStore(middleware)(state); - const props = createProps(); - const { getByText, getByTestId } = renderWithProvider( - , - store, - ); - expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot(); - expect( - getByTestId('main-quote-summary__exchange-rate-container'), - ).toMatchSnapshot(); - expect(getByText('Estimated gas fee')).toBeInTheDocument(); - expect(getByText('0.00008 ETH')).toBeInTheDocument(); - expect(getByText('Max fee')).toBeInTheDocument(); - expect(getByText('Swap')).toBeInTheDocument(); - }); -}); From 42e5eab8c1cd1be84b0b1d3663a996c4f7b30ff3 Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Tue, 15 Oct 2024 15:13:19 -0700 Subject: [PATCH 31/41] fix: SENTRY_DSN_FAKE problem (#27881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixing this problem: https://github.com/MetaMask/metamask-extension/pull/27548/files#r1801845403 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27881?quickstart=1) ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/setupSentry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index d440578144cc..627f46fa79fa 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -238,8 +238,8 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - !getManifestFlags().sentry?.forceEnable || - (process.env.IN_TEST && !SENTRY_DSN_DEV) + process.env.IN_TEST && + (!SENTRY_DSN_DEV || !getManifestFlags().sentry?.forceEnable) ) { return SENTRY_DSN_FAKE; } From 3f69851404c12f654dffd92f84cae98e4ccef325 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 16 Oct 2024 10:42:39 +0200 Subject: [PATCH 32/41] test(mock-e2e): add private domains logic for the privacy report (#27844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Introduce the concept of "private domains" for the `privacy-snapshot.json`. This allow to hide some part of a host [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27844?quickstart=1) ## **Related issues** Required by: - https://github.com/MetaMask/metamask-extension/pull/27730 ## **Manual testing steps** 1. Use this PR to test the feature: - https://github.com/MetaMask/metamask-extension/pull/27730 2. `yarn build:test:flask` 3. Remove the this line https://github.com/MetaMask/metamask-extension/blob/d5715503202bfaf451f60a6392e48366291942f7/privacy-snapshot.json#L2 4. `yarn test:e2e:single test/e2e/flask/btc/btc-account-overview.spec.ts --browser=chrome --update-privacy-snapshot` 5. The `privacy-snapshot.json` should have been updated again with the line you just removed ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/mock-e2e.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 209777f32bd7..1d0783b82624 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -66,6 +66,18 @@ const emptyHtmlPage = () => ` const browserAPIRequestDomains = /^.*\.(googleapis\.com|google\.com|mozilla\.net|mozilla\.com|mozilla\.org|gvt1\.com)$/iu; +/** + * Some third-party providers might use random URLs that we don't want to track + * in the privacy report "in clear". We identify those private hosts with a + * `pattern` regexp and replace the original host by a more generic one (`host`). + * For example, "my-secret-host.provider.com" could be denoted as "*.provider.com" in + * the privacy report. This would prevent disclosing the "my-secret-host" subdomain + * in this case. + */ +const privateHostMatchers = [ + // { pattern: RegExp, host: string } +]; + /** * @typedef {import('mockttp').Mockttp} Mockttp * @typedef {import('mockttp').MockedEndpoint} MockedEndpoint @@ -712,6 +724,25 @@ async function setupMocking( const portfolioRequestsMatcher = (request) => request.headers.referer === 'https://portfolio.metamask.io/'; + /** + * Tests a request against private domains and returns a set of generic hostnames that + * match. + * + * @param request + * @returns A set of matched results. + */ + const matchPrivateHosts = (request) => { + const privateHosts = new Set(); + + for (const { pattern, host: privateHost } of privateHostMatchers) { + if (request.headers.host.match(pattern)) { + privateHosts.add(privateHost); + } + } + + return privateHosts; + }; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -721,6 +752,16 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { + const privateHosts = matchPrivateHosts(request); + if (privateHosts.size) { + for (const privateHost of privateHosts) { + privacyReport.add(privateHost); + } + // At this point, we know the request at least one private doamin, so we just stops here to avoid + // using the request any further. + return; + } + if ( request.headers.host.match(browserAPIRequestDomains) === null && !portfolioRequestsMatcher(request) From bb2e2a997e6a4ef4c07a6680f424b8be4d42f887 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:45:04 +0200 Subject: [PATCH 33/41] fix: flaky test `Snap Account Signatures and Disconnects can connect to the Test Dapp, then #signTypedDataV3, disconnect then connect, then #signTypedDataV4 (async flow approve)` (#27887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** After connecting to the test dapp we are automatically switched to Mainnet (this is a recent change, didn't happen before). Then there is a race condition where the network the dapp sees is different from the wallet one, causing a miss-match and making the signature fail never opening the dialog. This seems an issue on the wallet side, as we should preserve the selected network after connecting, as we used to do before. I've opened an issue for that [here](https://github.com/MetaMask/metamask-extension/issues/27891). This just fixes the issue on the e2e side, but it needs to be fixed on the wallet side and remove the extra step again once the issue is fixed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27887?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27892 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** See how you are automatically switched to mainnet https://github.com/user-attachments/assets/d8fa53f5-b207-46bc-8e54-073245eff7b7 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/accounts/common.ts | 45 +++++++++++++++++++++++-------------- test/e2e/helpers.js | 5 +---- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index eda4ef5fbf6f..62f3fc082b53 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -10,7 +10,6 @@ import { unlockWallet, validateContractDetails, multipleGanacheOptions, - regularDelayMs, } from '../helpers'; import { Driver } from '../webdriver/driver'; import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; @@ -67,23 +66,16 @@ export async function installSnapSimpleKeyring( await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); - // Wait until popup is closed before proceeding - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); await driver.waitForSelector({ @@ -159,7 +151,7 @@ export async function makeNewAccountAndSwitch(driver: Driver) { text: 'Add account', }); // Click the ok button on the success modal - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ css: '[data-testid="confirmation-submit-button"]', text: 'Ok', }); @@ -196,17 +188,40 @@ async function switchToAccount2(driver: Driver) { export async function connectAccountToTestDapp(driver: Driver) { await switchToOrOpenDapp(driver); - await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElementAndWaitForWindowToClose({ + + // Extra steps needed to preserve the current network. + // Those can be removed once the issue is fixed (#27891) + const edit = await driver.findClickableElements({ + text: 'Edit', + tag: 'button', + }); + await edit[1].click(); + + await driver.clickElement({ + tag: 'p', + text: 'Localhost 8545', + }); + + await driver.clickElement({ + text: 'Update', + tag: 'button', + }); + + // Connect to the test dapp + await driver.clickElement({ text: 'Connect', tag: 'button', }); await driver.switchToWindowWithUrl(DAPP_URL); + // Ensure network is preserved after connecting + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); } export async function disconnectFromTestDapp(driver: Driver) { @@ -296,12 +311,8 @@ export async function signData( }, async () => { await switchToOrOpenDapp(driver); - await driver.clickElement(locatorID); - // take extra time to load the popup - await driver.delay(500); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); }, ); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 65a405f5325d..564f99f2cde6 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -1208,10 +1208,7 @@ async function tempToggleSettingRedesignedConfirmations(driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); + await driver.clickElement('[data-testid="account-options-menu-button"]'); // fix race condition with mmi build if (process.env.MMI) { From 0edfb4881eda0abd83565ec6e61ab8ecea79ee28 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:11 +0200 Subject: [PATCH 34/41] fix: flaky test `Wallet Revoke Permissions should revoke eth_accounts permissions via test dapp` (#27894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The problem is that this test is utilizing a wrong pattern where we are asserting the element by its text, instead of wait for the exact selector we want by text. When when it's trying to assert the text, sometimes the value is empty. ![Screenshot from 2024-10-16 11-28-45](https://github.com/user-attachments/assets/846dcaaa-e15a-4265-87b8-578c820f6569) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27894?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27722 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../revoke-permissions.spec.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js index 14d2af60bdbc..696095e3fd79 100644 --- a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, openDapp, @@ -25,12 +24,10 @@ describe('Wallet Revoke Permissions', function () { // Get initial accounts permissions await driver.clickElement('#getPermissions'); - const permissionsResult = await driver.findElement( - '#permissionsResult', - ); - - // Eth_accounts permission - assert.equal(await permissionsResult.getText(), 'eth_accounts'); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'eth_accounts', + }); // Revoke eth_accounts permissions await driver.clickElement('#revokeAccountsPermission'); @@ -39,10 +36,10 @@ describe('Wallet Revoke Permissions', function () { await driver.clickElement('#getPermissions'); // Eth_accounts permissions removed - assert.equal( - await permissionsResult.getText(), - 'No permissions found.', - ); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'No permissions found.', + }); }, ); }); From 95045ed8f6ba8c06777c717df264257c310f5a2e Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:20 +0200 Subject: [PATCH 35/41] fix: flaky test `ERC721 NFTs testdapp interaction should prompt users to add their NFTs to their wallet (all at once)` (#27889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The problem is that we are looking for the Deposit transaction by its text in the activity tab, but this element updates its state, from `pending`to `confirm`, meaning that it can become stale when we do the assertion after (see video). ``` await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); assert.equal(await transactionItem.isDisplayed(), true); ``` To mitigate this problem, we wait until the transaction is confirmed, and then look for the Deposit tx. This not only fixes the race condition, but it also ensures that the tx is successful. ![Screenshot from 2024-10-16 08-35-56](https://github.com/user-attachments/assets/db176ec1-b71c-4019-bc67-7a4714c2c17b) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27889?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/26759 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** https://github.com/user-attachments/assets/0e463ddf-f744-4428-8472-0ec2a585171e ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tokens/nft/erc721-interaction.spec.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index c2d3b7a8ce82..9ebc247ea795 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -225,25 +225,30 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Mint', tag: 'button' }); // Notification - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + + // We need to wait until the transaction is confirmed before looking for the tx + // otherwise the element becomes stale, as it updates from 'pending' to 'confirmed' + await driver.waitForSelector('.transaction-status-label--confirmed'); + + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const nftsMintStatus = await driver.findElement({ @@ -255,7 +260,6 @@ describe('ERC721 NFTs testdapp interaction', function () { // watch all nfts await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirm watchNFT @@ -277,8 +281,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); await removeButtons[0].click(); - await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Add NFTs', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, From ec4fb5fcefd62980057dfee5da8bd3aa3627dd35 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:54:45 +0200 Subject: [PATCH 36/41] fix: flaky test `Permissions sets permissions and connect to Dapp` (#27888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We proceed switching to the Extension full view without waiting until the dialog is closed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27888?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27890 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../dapp-interactions/permissions.spec.js | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/test/e2e/tests/dapp-interactions/permissions.spec.js b/test/e2e/tests/dapp-interactions/permissions.spec.js index adf3b809a656..4b6c210f0a98 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -28,19 +28,15 @@ describe('Permissions', function () { tag: 'button', }); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - await driver.clickElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', }); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // shows connected sites await driver.clickElement( @@ -64,21 +60,17 @@ describe('Permissions', function () { assert.equal(domains.length, 1); // can get accounts within the dapp - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement({ text: 'eth_accounts', tag: 'button', }); - const getAccountsResult = await driver.waitForSelector({ + await driver.waitForSelector({ css: '#getAccountsResult', text: publicAddress, }); - assert.equal( - (await getAccountsResult.getText()).toLowerCase(), - publicAddress.toLowerCase(), - ); }, ); }); From 71de55b8a3c97f17ea92653b1607e2e78f976967 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:32:07 +0200 Subject: [PATCH 37/41] fix: flaky test `ERC1155 NFTs testdapp interaction should batch transfers ERC1155 token` (#27897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Same problem as https://github.com/MetaMask/metamask-extension/pull/27889. We are looking for transactions by its text in the activity tab, but the transaction element updates its state, from pending to confirm, meaning that it can become stale when we do the assertion after. ``` await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); assert.equal(await transactionItem.isDisplayed(), true); ``` ![Screenshot from 2024-10-16 12-05-08](https://github.com/user-attachments/assets/df2066a1-b692-4e5f-9961-6e2e4626aa00) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27897?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27896 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tokens/nft/erc1155-interaction.spec.js | 150 ++++++++---------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js index 31425140c7f4..1fed3946dea9 100644 --- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js @@ -38,33 +38,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#batchMintButton'); // Notification - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Mint await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -90,33 +84,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.fill('#batchTransferTokenAmounts', '1, 1, 1000000000000'); await driver.clickElement('#batchTransferFromButton'); - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Transfer await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -147,26 +135,20 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#setApprovalForAllERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - const displayedMessageTitle = await driver.findElement( - '[data-testid="confirm-approve-title"]', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + css: '[data-testid="confirm-approve-title"]', + text: expectedMessageTitle, + }); + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -185,27 +167,29 @@ describe('ERC1155 NFTs testdapp interaction', function () { '.set-approval-for-all-warning__content__header', ); assert.equal(await displayedWarning.getText(), expectedWarningMessage); - await driver.clickElement({ text: 'Approve', tag: 'button' }); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Approve', + tag: 'button', + }); // Switch to extension and check set approval for all transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const setApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await setApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that set approval for all action completed message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const setApprovalStatus = await driver.findElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.waitForSelector({ css: '#erc1155Status', text: 'Set Approval For All completed', }); - assert.equal(await setApprovalStatus.isDisplayed(), true); }, ); }); @@ -235,27 +219,22 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#revokeERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const displayedMessageTitle = await driver.findElement( - '.confirm-approve-content__title', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.waitForSelector({ + css: '.confirm-approve-content__title', + text: expectedMessageTitle, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -269,22 +248,25 @@ describe('ERC1155 NFTs testdapp interaction', function () { assert.equal(await params.getText(), 'Parameters: false'); // Click on extension popup to confirm revoke approval for all - await driver.clickElement('[data-testid="page-container-footer-next"]'); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); // Switch to extension and check revoke approval transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const revokeApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await revokeApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that revoke approval for all message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const revokeApprovalStatus = await driver.findElement({ css: '#erc1155Status', text: 'Revoke completed', From 130bdbf5d02702ef4227644aa7827715847e00fb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:43:59 -0400 Subject: [PATCH 38/41] test: [POM] Migrate signature with snap account e2e tests to page object modal (#27829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the snap account signature e2e tests to the Page Object Model (POM) pattern, improving test stability and maintainability. Changes include: - Migrate test `snap-account-signatures.spec.ts` to POM - Created all signature related functions in TestDapp class - Avoid several delays in the original function implementation - Reduced flakiness [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: #27835 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chloe Gao Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- .../accounts/snap-account-signatures.spec.ts | 51 --- test/e2e/page-objects/flows/sign.flow.ts | 169 +++++++++ .../pages/experimental-settings.ts | 10 +- .../pages/snap-simple-keyring-page.ts | 42 ++- test/e2e/page-objects/pages/test-dapp.ts | 335 +++++++++++++++++- .../account/snap-account-settings.spec.ts | 2 +- .../account/snap-account-signatures.spec.ts | 100 ++++++ ...55-revoke-set-approval-for-all-redesign.ts | 2 +- ...1155-set-approval-for-all-redesign.spec.ts | 2 +- ...21-revoke-set-approval-for-all-redesign.ts | 2 +- ...c721-set-approval-for-all-redesign.spec.ts | 2 +- 11 files changed, 624 insertions(+), 93 deletions(-) delete mode 100644 test/e2e/accounts/snap-account-signatures.spec.ts create mode 100644 test/e2e/page-objects/flows/sign.flow.ts create mode 100644 test/e2e/tests/account/snap-account-signatures.spec.ts diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts deleted file mode 100644 index 536d8168b1a3..000000000000 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Suite } from 'mocha'; -import { - tempToggleSettingRedesignedConfirmations, - withFixtures, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - signData, -} from './common'; - -describe('Snap Account Signatures', function (this: Suite) { - this.timeout(120000); // This test is very long, so we need an unusually high timeout - - // Run sync, async approve, and async reject flows - // (in Jest we could do this with test.each, but that does not exist here) - ['sync', 'approve', 'reject'].forEach((flowType) => { - // generate title of the test from flowType - const title = `can sign with ${flowType} flow`; - - it(title, async () => { - await withFixtures( - accountSnapFixtures(title), - async ({ driver }: { driver: Driver }) => { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // Run all 5 signature types - const locatorIDs = [ - '#personalSign', - '#signTypedData', - '#signTypedDataV3', - '#signTypedDataV4', - '#signPermit', - ]; - - for (const locatorID of locatorIDs) { - await signData(driver, locatorID, newPublicKey, flowType); - } - }, - ); - }); - }); -}); diff --git a/test/e2e/page-objects/flows/sign.flow.ts b/test/e2e/page-objects/flows/sign.flow.ts new file mode 100644 index 000000000000..c7d03bb4f96e --- /dev/null +++ b/test/e2e/page-objects/flows/sign.flow.ts @@ -0,0 +1,169 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import TestDapp from '../pages/test-dapp'; + +/** + * This function initiates the steps for a personal sign with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const personalSignWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.personalSign(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successPersonalSign(publicAddress); + } else { + await testDapp.check_failedPersonalSign( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedData with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedData(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedData(publicAddress); + } else { + await testDapp.check_failedSignTypedData( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV3 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV3WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV3(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV3(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV3( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV4 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV4WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV4(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV4(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV4( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signPermit with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signPermitWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signPermit(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignPermit(publicAddress); + } else { + await testDapp.check_failedSignPermit( + 'Error: Request rejected by user or snap.', + ); + } +}; diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts index 8c7129b17555..7cd780229acd 100644 --- a/test/e2e/page-objects/pages/experimental-settings.ts +++ b/test/e2e/page-objects/pages/experimental-settings.ts @@ -9,9 +9,12 @@ class ExperimentalSettings { private readonly experimentalPageTitle: object = { text: 'Experimental', - css: '.h4', + tag: 'h4', }; + private readonly redesignedSignatureToggle: string = + '[data-testid="toggle-redesigned-confirmations-container"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -33,6 +36,11 @@ class ExperimentalSettings { console.log('Toggle Add Account Snap on experimental setting page'); await this.driver.clickElement(this.addAccountSnapToggle); } + + async toggleRedesignedSignature(): Promise { + console.log('Toggle Redesigned Signature on experimental setting page'); + await this.driver.clickElement(this.redesignedSignatureToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts index 7f7a97d7d861..c75adb06da3a 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -9,11 +9,6 @@ class SnapSimpleKeyringPage { tag: 'h3', }; - private readonly accountSupportedMethods = { - text: 'Account Supported Methods', - tag: 'p', - }; - private readonly addtoMetamaskMessage = { text: 'Add to MetaMask', tag: 'h3', @@ -104,6 +99,11 @@ class SnapSimpleKeyringPage { tag: 'div', }; + private readonly newAccountMessage = { + text: '"address":', + tag: 'div', + }; + private readonly pageTitle = { text: 'Snap Simple Keyring', tag: 'p', @@ -161,16 +161,25 @@ class SnapSimpleKeyringPage { * Approves or rejects a transaction from a snap account on Snap Simple Keyring page. * * @param approveTransaction - Indicates if the transaction should be approved. Defaults to true. + * @param isSignatureRequest - Indicates if the request is a signature request. Defaults to false. */ async approveRejectSnapAccountTransaction( approveTransaction: boolean = true, + isSignatureRequest: boolean = false, ): Promise { console.log( 'Approve/Reject snap account transaction on Snap Simple Keyring page', ); - await this.driver.clickElementAndWaitToDisappear( - this.confirmationSubmitButton, - ); + if (isSignatureRequest) { + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + } else { + // For send eth requests, the origin screen is not closed automatically, so we cannot call clickElementAndWaitForWindowToClose here. + await this.driver.clickElementAndWaitToDisappear( + this.confirmationSubmitButton, + ); + } await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); @@ -242,7 +251,7 @@ class SnapSimpleKeyringPage { await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); - await this.check_accountSupportedMethodsDisplayed(); + await this.driver.waitForSelector(this.newAccountMessage); } async confirmCreateSnapOnConfirmationScreen(): Promise { @@ -255,15 +264,21 @@ class SnapSimpleKeyringPage { * * @param accountName - Optional: name for the snap account. Defaults to "SSK Account". * @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true. + * @returns the public key of the new created account */ async createNewAccount( accountName: string = 'SSK Account', isFirstAccount: boolean = true, - ): Promise { + ): Promise { console.log('Create new account on Snap Simple Keyring page'); await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); await this.confirmCreateSnapOnConfirmationScreen(); await this.confirmAddAccountDialog(accountName); + const newAccountJSONMessage = await ( + await this.driver.waitForSelector(this.newAccountMessage) + ).getText(); + const newPublicKey = JSON.parse(newAccountJSONMessage).address; + return newPublicKey; } /** @@ -331,13 +346,6 @@ class SnapSimpleKeyringPage { await this.driver.clickElement(this.useSyncApprovalToggle); } - async check_accountSupportedMethodsDisplayed(): Promise { - console.log( - 'Check new created account supported methods are displayed on simple keyring snap page', - ); - await this.driver.waitForSelector(this.accountSupportedMethods); - } - async check_errorRequestMessageDisplayed(): Promise { console.log( 'Check error request message is displayed on snap simple keyring page', diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 89ee6bc9cbd3..ffb1f9033bdb 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,5 +1,5 @@ import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { WINDOW_TITLES } from '../../helpers'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -7,40 +7,120 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; class TestDapp { private driver: Driver; - private erc721SetApprovalForAllButton: RawLocator; + private readonly confirmDialogScrollButton = + '[data-testid="signature-request-scroll-button"]'; - private erc1155SetApprovalForAllButton: RawLocator; + private readonly confirmSignatureButton = + '[data-testid="page-container-footer-next"]'; - private erc721RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155RevokeSetApprovalForAllButton = + '#revokeERC1155Button'; - private erc1155RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155SetApprovalForAllButton = + '#setApprovalForAllERC1155Button'; + + private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton'; + + private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + + private readonly mmlogo = '#mm-logo'; + + private readonly personalSignButton = '#personalSign'; + + private readonly personalSignResult = '#personalSignVerifyECRecoverResult'; + + private readonly personalSignSignatureRequestMessage = { + text: 'personal_sign', + tag: 'div', + }; + + private readonly personalSignVerifyButton = '#personalSignVerify'; + + private readonly signPermitButton = '#signPermit'; + + private readonly signPermitResult = '#signPermitResult'; + + private readonly signPermitSignatureRequestMessage = { + text: 'Permit', + tag: 'p', + }; + + private readonly signPermitVerifyButton = '#signPermitVerify'; + + private readonly signPermitVerifyResult = '#signPermitVerifyResult'; + + private readonly signTypedDataButton = '#signTypedData'; + + private readonly signTypedDataResult = '#signTypedDataResult'; + + private readonly signTypedDataSignatureRequestMessage = { + text: 'Hi, Alice!', + tag: 'div', + }; + + private readonly signTypedDataV3Button = '#signTypedDataV3'; + + private readonly signTypedDataV3Result = '#signTypedDataV3Result'; + + private readonly signTypedDataV3V4SignatureRequestMessage = { + text: 'Hello, Bob!', + tag: 'div', + }; + + private readonly signTypedDataV3VerifyButton = '#signTypedDataV3Verify'; + + private readonly signTypedDataV3VerifyResult = '#signTypedDataV3VerifyResult'; + + private readonly signTypedDataV4Button = '#signTypedDataV4'; + + private readonly signTypedDataV4Result = '#signTypedDataV4Result'; + + private readonly signTypedDataV4VerifyButton = '#signTypedDataV4Verify'; + + private readonly signTypedDataV4VerifyResult = '#signTypedDataV4VerifyResult'; + + private readonly signTypedDataVerifyButton = '#signTypedDataVerify'; + + private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; constructor(driver: Driver) { this.driver = driver; + } - this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; - this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; - this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; - this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.mmlogo); + } catch (e) { + console.log('Timeout while waiting for Test Dapp page to be loaded', e); + throw e; + } + console.log('Test Dapp page is loaded'); } - async open({ - contractAddress, + /** + * Open the test dapp page. + * + * @param options - The options for opening the test dapp page. + * @param options.contractAddress - The contract address to open the dapp with. Defaults to null. + * @param options.url - The URL of the dapp. Defaults to DAPP_URL. + * @returns A promise that resolves when the new page is opened. + */ + async openTestDappPage({ + contractAddress = null, url = DAPP_URL, }: { - contractAddress?: string; + contractAddress?: string | null; url?: string; - }) { + } = {}): Promise { const dappUrl = contractAddress ? `${url}/?contract=${contractAddress}` : url; - - return await this.driver.openNewPage(dappUrl); + await this.driver.openNewPage(dappUrl); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(method: string, params: any[]) { - await this.open({ + await this.openTestDappPage({ url: `${DAPP_URL}/request?method=${method}¶ms=${JSON.stringify( params, )}`, @@ -55,13 +135,230 @@ class TestDapp { await this.driver.clickElement(this.erc1155SetApprovalForAllButton); } - public async clickERC721RevokeSetApprovalForAllButton() { + async clickERC721RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc721RevokeSetApprovalForAllButton); } - public async clickERC1155RevokeSetApprovalForAllButton() { + async clickERC1155RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton); } -} + /** + * Verify the failed personal sign signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedPersonalSign(expectedFailedMessage: string) { + console.log('Verify failed personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.personalSignButton, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signPermit signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignPermit(expectedFailedMessage: string) { + console.log('Verify failed signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signPermitResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedData signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedData(expectedFailedMessage: string) { + console.log('Verify failed signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV3 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV3(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV3Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV4 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV4(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV4Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the successful personal sign signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successPersonalSign(publicKey: string) { + console.log('Verify successful personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.personalSignVerifyButton); + await this.driver.waitForSelector({ + css: this.personalSignResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signPermit signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignPermit(publicKey: string) { + console.log('Verify successful signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signPermitVerifyButton); + await this.driver.waitForSelector({ + css: this.signPermitVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedData signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedData(publicKey: string) { + console.log('Verify successful signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataVerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV3 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV3(publicKey: string) { + console.log('Verify successful signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV3VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV3VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV4 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV4(publicKey: string) { + console.log('Verify successful signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV4VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV4VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Sign a message with the personal sign method. + */ + async personalSign() { + console.log('Sign message with personal sign'); + await this.driver.clickElement(this.personalSignButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.personalSignSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign message with the signPermit method. + */ + async signPermit() { + console.log('Sign message with signPermit'); + await this.driver.clickElement(this.signPermitButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.signPermitSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedData method. + */ + async signTypedData() { + console.log('Sign message with signTypedData'); + await this.driver.clickElement(this.signTypedDataButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataSignatureRequestMessage, + ); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV3 method. + */ + async signTypedDataV3() { + console.log('Sign message with signTypedDataV3'); + await this.driver.clickElement(this.signTypedDataV3Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV4 method. + */ + async signTypedDataV4() { + console.log('Sign message with signTypedDataV4'); + await this.driver.clickElement(this.signTypedDataV4Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } +} export default TestDapp; diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts index cbd5f8814b7b..1a0c761fb4df 100644 --- a/test/e2e/tests/account/snap-account-settings.spec.ts +++ b/test/e2e/tests/account/snap-account-settings.spec.ts @@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite await settingsPage.goToExperimentalSettings(); const experimentalSettings = new ExperimentalSettings(driver); - await settingsPage.check_pageIsLoaded(); + await experimentalSettings.check_pageIsLoaded(); await experimentalSettings.toggleAddAccountSnap(); // Make sure the "Add account Snap" button is visible. diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts new file mode 100644 index 000000000000..f5010fb61269 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -0,0 +1,100 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + personalSignWithSnapAccount, + signPermitWithSnapAccount, + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, + signTypedDataWithSnapAccount, +} from '../../page-objects/flows/sign.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; + +describe('Snap Account Signatures @no-mmi', function (this: Suite) { + // Run sync, async approve, and async reject flows + // (in Jest we could do this with test.each, but that does not exist here) + + ['sync', 'approve', 'reject'].forEach((flowType) => { + // generate title of the test from flowType + const title = `can sign with ${flowType} flow`; + + it(title, async () => { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title, + }, + async ({ driver }: { driver: Driver }) => { + const isSyncFlow = flowType === 'sync'; + const approveTransaction = flowType === 'approve'; + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, isSyncFlow); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Run all 5 signature types + await new TestDapp(driver).openTestDappPage(); + await personalSignWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV3WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV4WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signPermitWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts index 7f26e02a572c..3e75adb34db8 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts @@ -57,7 +57,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts index 438b3e979d0a..0e1134737c87 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts index 5a8dcd3768f7..138695904e55 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts @@ -80,7 +80,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts index 7ca9518cabc2..589670212be1 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); From fd1fad8c1f3056b2364ef75b1c22a1305cc29203 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 16 Oct 2024 06:10:47 -0700 Subject: [PATCH 39/41] feat: Use requested permissions as default selected values for AmonHenV2 connection flow with case insensitive address comparison (#27517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Previously, the permission approval component for the AmonHenV2 Flow (accounts + permittedChains in one view) did not consider the caveat values of the requested permission as valid defaults. This PR makes the `ConnectPage` component use any caveat values in the permission request as the default selected before falling back to the previous default logic (currently selected account + all non test networks). Also adds case insensitive account address comparison to related flow [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27517?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** `CHAIN_PERMISSIONS=1 yarn start` ``` await window.ethereum.request({ "method": "wallet_requestPermissions", "params": [ { eth_accounts: { caveats: [ { type: 'restrictReturnedAccounts', value: ['0x5bA08AF1bc30f17272178bDcACA1C74e94955cF4'] } ] } } ], }); ``` ``` await window.ethereum.request({ "method": "wallet_requestPermissions", "params": [ { 'endowment:permitted-chains': { caveats: [ { type: 'restrictNetworkSwitching', value: ['0x1'] } ] } } ], }); ``` OR some combination of the above. You should see the accounts/chains in the request as the default is provided. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../edit-accounts-modal.tsx | 9 +- .../site-cell/site-cell.tsx | 5 +- .../__snapshots__/connect-page.test.tsx.snap | 240 ++++++++++++++++++ .../connect-page/connect-page.test.tsx | 37 +++ .../connect-page/connect-page.tsx | 33 ++- 5 files changed, 319 insertions(+), 5 deletions(-) diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx index ba842efc6a11..084596f07afb 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -35,6 +35,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; type EditAccountsModalProps = { activeTabOrigin: string; @@ -141,8 +142,12 @@ export const EditAccountsModal: React.FC = ({ isPinned={Boolean(account.pinned)} startAccessory={ + isEqualCaseInsensitive( + selectedAccountAddress, + account.address, + ), )} /> } diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index bb3a14a8f5e8..562d3e8c7d2e 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -19,6 +19,7 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../../shared/constants/metametrics'; +import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils'; import { SiteCellTooltip } from './site-cell-tooltip'; import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; @@ -59,7 +60,9 @@ export const SiteCell: React.FC = ({ const [showEditNetworksModal, setShowEditNetworksModal] = useState(false); const selectedAccounts = accounts.filter(({ address }) => - selectedAccountAddresses.includes(address), + selectedAccountAddresses.some((selectedAccountAddress) => + isEqualCaseInsensitive(selectedAccountAddress, address), + ), ); const selectedNetworks = allNetworks.filter(({ chainId }) => selectedChainIds.includes(chainId), diff --git a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap index e416011c1b08..ad53f67a7127 100644 --- a/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -249,3 +249,243 @@ exports[`ConnectPage should render correctly 1`] = `
`; + +exports[`ConnectPage should render with defaults from the requested permissions 1`] = ` +
+
+
+
+
+

+

+ Connect with MetaMask +

+

+ This site wants to + : +

+

+
+
+
+
+
+
+ +
+
+

+ See your accounts and suggest transactions +

+
+ + Requesting for Test Account + + +
+
+ +
+
+
+ +
+
+

+ Use your enabled networks +

+
+ + Requesting for + +
Alerts"" + data-tooltipped="" + style="display: inline;" + > +
+
+
+
+ Custom Mainnet RPC logo +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+`; diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx index d7c50c6aa501..ef705e474ad9 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { renderWithProvider } from '../../../../test/jest/rendering'; import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; import { ConnectPage, ConnectPageRequest } from './connect-page'; const render = ( @@ -74,4 +79,36 @@ describe('ConnectPage', () => { expect(confirmButton).toBeDefined(); expect(cancelButton).toBeDefined(); }); + + it('should render with defaults from the requested permissions', () => { + const { container } = render({ + request: { + id: '1', + origin: 'https://test.dapp', + permissions: { + [RestrictedMethods.eth_accounts]: { + caveats: [ + { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], + }, + ], + }, + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }); + expect(container).toMatchSnapshot(); + }); }); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index a30047fbd38a..45e6c5b1f48f 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -34,10 +34,19 @@ import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu'; import { TEST_CHAINS } from '../../../../shared/constants/network'; import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; export type ConnectPageRequest = { id: string; origin: string; + permissions?: Record< + string, + { caveats?: { type: string; value: string[] }[] } + >; }; type ConnectPageProps = { @@ -57,6 +66,20 @@ export const ConnectPage: React.FC = ({ }) => { const t = useI18nContext(); + const ethAccountsPermission = + request?.permissions?.[RestrictedMethods.eth_accounts]; + const requestedAccounts = + ethAccountsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, + )?.value || []; + + const permittedChainsPermission = + request?.permissions?.[EndowmentTypes.permittedChains]; + const requestedChainIds = + permittedChainsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const [nonTestNetworks, testNetworks] = useMemo( () => @@ -70,7 +93,10 @@ export const ConnectPage: React.FC = ({ ), [networkConfigurations], ); - const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId); + const defaultSelectedChainIds = + requestedChainIds.length > 0 + ? requestedChainIds + : nonTestNetworks.map(({ chainId }) => chainId); const [selectedChainIds, setSelectedChainIds] = useState( defaultSelectedChainIds, ); @@ -84,7 +110,10 @@ export const ConnectPage: React.FC = ({ }, [accounts, internalAccounts]); const currentAccount = useSelector(getSelectedInternalAccount); - const defaultAccountsAddresses = [currentAccount?.address]; + const defaultAccountsAddresses = + requestedAccounts.length > 0 + ? requestedAccounts + : [currentAccount?.address]; const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultAccountsAddresses, ); From 56ed6930c59322b5275a94be7f75ca76a89e351f Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:00:47 +0100 Subject: [PATCH 40/41] fix: Contract Interaction - cannot read the property `text_signature` (#27686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a validation to handle cases where the 4byte response results are either undefined or an empty array. Instead of throwing an error, the code now safely handles these cases by returning undefined, preventing the TypeError from occurring. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27686?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27527 ## **Manual testing steps** 1. Go to Remix 2. Deploy the contract below 3. Trigger the triggerMe func 4. See console error ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Params { uint256 public x; uint256 public value; address public addr; bool public flag; string public text; function triggerMe( uint256 _x, uint256 _value, address _addr, bool _flag, string memory _text ) public returns (bool) { x = _x; value = _value; addr = _addr; flag = _flag; text = _text; return true; } receive() external payable { } } ``` ## **Screenshots/Recordings** [4bytes response.webm](https://github.com/user-attachments/assets/0d6d8ba9-5c43-4c65-ad34-7ea416039c6f) ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/lib/four-byte.test.ts | 27 ++++++++++++++++++++++++--- shared/lib/four-byte.ts | 4 ++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts index 2867aa2e51b7..77271c4aeba3 100644 --- a/shared/lib/four-byte.test.ts +++ b/shared/lib/four-byte.test.ts @@ -10,12 +10,14 @@ import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte'; const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10); describe('Four Byte', () => { - const fetchMock = jest.fn(); - describe('getMethodFrom4Byte', () => { - it('returns signature with earliest creation date', async () => { + const fetchMock = jest.fn(); + + beforeEach(() => { jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + it('returns signature with earliest creation date', async () => { fetchMock.mockResolvedValue({ ok: true, json: async () => FOUR_BYTE_RESPONSE, @@ -44,6 +46,25 @@ describe('Four Byte', () => { expect(await getMethodFrom4Byte(prefix)).toBeUndefined(); }, ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + ['undefined', { results: undefined }], + ['object', { results: {} }], + ['empty', { results: [] }], + ])( + 'returns `undefined` if fourByteResponse.results is %s', + async (_: string, mockResponse: { results: unknown }) => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await getMethodFrom4Byte('0x913aa952'); + + expect(result).toBeUndefined(); + }, + ); }); describe('getMethodDataAsync', () => { diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts index e28f4d4c0c5c..c6b9da22e617 100644 --- a/shared/lib/four-byte.ts +++ b/shared/lib/four-byte.ts @@ -34,6 +34,10 @@ export async function getMethodFrom4Byte( functionName: 'getMethodFrom4Byte', })) as FourByteResponse; + if (!fourByteResponse.results?.length) { + return undefined; + } + fourByteResponse.results.sort((a, b) => { return new Date(a.created_at).getTime() < new Date(b.created_at).getTime() ? -1 From bf87d720cb6c2f98487368dd8df81da078a7d2e7 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 16 Oct 2024 20:24:01 +0530 Subject: [PATCH 41/41] feat: Adding typed sign support for NFT permit (#27796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding support for NFT permit signature request. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27396 ## **Manual testing steps** 1. Submit an NFT permit request 2. Check the confirmation page that appears ## **Screenshots/Recordings** Screenshot 2024-10-11 at 7 43 59 PM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/data/confirmations/typed_sign.ts | 15 ++++++ .../components/confirm/title/title.test.tsx | 20 ++++++- .../components/confirm/title/title.tsx | 47 ++++++++++++---- ui/pages/confirmations/constants/index.ts | 5 ++ .../hooks/useTypedSignSignatureInfo.test.js | 27 ++++++++++ .../hooks/useTypedSignSignatureInfo.ts | 53 +++++++++++++++++++ 6 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js create mode 100644 ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index f02705a2540b..7be24a1389c6 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -183,6 +183,21 @@ export const permitSignatureMsg = { }, } as SignatureRequestType; +export const permitNFTSignatureMsg = { + id: 'c5067710-87cf-11ef-916c-71f266571322', + status: 'unapproved', + time: 1728651190529, + type: 'eth_signTypedData', + msgParams: { + data: '{"domain":{"name":"Uniswap V3 Positions NFT-V1","version":"1","chainId":1,"verifyingContract":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88"},"types":{"Permit":[{"name":"spender","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","message":{"spender":"0x00000000Ede6d8D217c60f93191C060747324bca","tokenId":"3606393","nonce":"0","deadline":"1734995006"}}', + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', + requestId: 2874791875, + origin: 'https://metamask.github.io', + }, +} as SignatureRequestType; + export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', securityAlertResponse: { diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index 3c03343c2afb..3d4d6672940d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -11,7 +11,10 @@ import { getMockTypedSignConfirmStateForRequest, } from '../../../../../../test/data/confirmations/helper'; import { unapprovedPersonalSignMsg } from '../../../../../../test/data/confirmations/personal_sign'; -import { permitSignatureMsg } from '../../../../../../test/data/confirmations/typed_sign'; +import { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../test/data/confirmations/typed_sign'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { tEn } from '../../../../../../test/lib/i18n-helpers'; import { @@ -71,6 +74,21 @@ describe('ConfirmTitle', () => { ).toBeInTheDocument(); }); + it('should render the title and description for a NFT permit signature', () => { + const mockStore = configureMockStore([])( + getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg), + ); + const { getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(getByText('Withdrawal request')).toBeInTheDocument(); + expect( + getByText('This site wants permission to withdraw your NFTs'), + ).toBeInTheDocument(); + }); + it('should render the title and description for typed signature', () => { const mockStore = configureMockStore([])(getMockTypedSignConfirmState()); const { getByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 2645feed8a41..969e9c05518d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -3,6 +3,8 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; + +import { TokenStandard } from '../../../../../../shared/constants/transaction'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; import { Box, Text } from '../../../../../components/component-library'; import { @@ -12,12 +14,11 @@ import { } from '../../../../../helpers/constants/design-system'; import useAlerts from '../../../../../hooks/useAlerts'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { TypedSignSignaturePrimaryTypes } from '../../../constants'; import { useConfirmContext } from '../../../context/confirm'; import { Confirmation, SignatureRequestType } from '../../../types/confirm'; -import { - isPermitSignatureRequest, - isSIWESignatureRequest, -} from '../../../utils'; +import { isSIWESignatureRequest } from '../../../utils'; +import { useTypedSignSignatureInfo } from '../../../hooks/useTypedSignSignatureInfo'; import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData'; import { getIsRevokeSetApprovalForAll } from '../info/utils'; @@ -51,6 +52,8 @@ function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { type IntlFunction = (str: string) => string; +// todo: getTitle and getDescription can be merged to remove code duplication. + const getTitle = ( t: IntlFunction, confirmation?: Confirmation, @@ -58,6 +61,8 @@ const getTitle = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -74,9 +79,13 @@ const getTitle = ( } return t('confirmTitleSignature'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitlePermitTokens') - : t('confirmTitleSignature'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('setApprovalForAllRedesignedTitle'); + } + return t('confirmTitlePermitTokens'); + } + return t('confirmTitleSignature'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleApproveTransaction'); @@ -104,6 +113,8 @@ const getDescription = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -120,9 +131,13 @@ const getDescription = ( } return t('confirmTitleDescSign'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitleDescPermitSignature') - : t('confirmTitleDescSign'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('confirmTitleDescApproveTransaction'); + } + return t('confirmTitleDescPermitSignature'); + } + return t('confirmTitleDescSign'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleDescApproveTransaction'); @@ -150,6 +165,10 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); + const { primaryType, tokenStandard } = useTypedSignSignatureInfo( + currentConfirmation as SignatureRequestType, + ); + const { customSpendingCap, pending: spendingCapPending } = useCurrentSpendingCap(currentConfirmation); @@ -175,6 +194,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -183,6 +204,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); @@ -195,6 +218,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -203,6 +228,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); diff --git a/ui/pages/confirmations/constants/index.ts b/ui/pages/confirmations/constants/index.ts index 38fd05b714ba..7e26ce5c6d62 100644 --- a/ui/pages/confirmations/constants/index.ts +++ b/ui/pages/confirmations/constants/index.ts @@ -9,3 +9,8 @@ export const TYPED_SIGNATURE_VERSIONS = { }; export const SPENDING_CAP_UNLIMITED_MSG = 'UNLIMITED MESSAGE'; + +export const TypedSignSignaturePrimaryTypes = { + PERMIT: 'Permit', + ORDER: 'Order', +}; diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js new file mode 100644 index 000000000000..38468749782d --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js @@ -0,0 +1,27 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { permitNFTSignatureMsg } from '../../../../test/data/confirmations/typed_sign'; +import { unapprovedPersonalSignMsg } from '../../../../test/data/confirmations/personal_sign'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; +import { useTypedSignSignatureInfo } from './useTypedSignSignatureInfo'; + +describe('useTypedSignSignatureInfo', () => { + it('should return details for primaty type and token standard', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(permitNFTSignatureMsg), + ); + expect(result.current.primaryType).toStrictEqual( + TypedSignSignaturePrimaryTypes.PERMIT, + ); + expect(result.current.tokenStandard).toStrictEqual(TokenStandard.ERC721); + }); + + it('should return empty object if confirmation is not typed sign', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(unapprovedPersonalSignMsg), + ); + expect(result.current.primaryType).toBeUndefined(); + expect(result.current.tokenStandard).toBeUndefined(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts new file mode 100644 index 000000000000..30d4e58f1525 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; + +import { + isOrderSignatureRequest, + isPermitSignatureRequest, + isSignatureTransactionType, +} from '../utils'; +import { SignatureRequestType } from '../types/confirm'; +import { parseTypedDataMessage } from '../../../../shared/modules/transaction.utils'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; + +export const useTypedSignSignatureInfo = ( + confirmation: SignatureRequestType, +) => { + const primaryType = useMemo(() => { + if ( + !confirmation || + !isSignatureTransactionType(confirmation) || + confirmation?.type !== MESSAGE_TYPE.ETH_SIGN_TYPED_DATA + ) { + return undefined; + } + if (isPermitSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.PERMIT; + } else if (isOrderSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.ORDER; + } + return undefined; + }, [confirmation]); + + // here we are using presence of tokenId in typed message data to know if its NFT permit + // we can get contract details for verifyingContract but that is async process taking longer + // and result in confirmation page content loading late + const tokenStandard = useMemo(() => { + if (primaryType !== TypedSignSignaturePrimaryTypes.PERMIT) { + return undefined; + } + const { + message: { tokenId }, + } = parseTypedDataMessage(confirmation?.msgParams?.data as string); + if (tokenId !== undefined) { + return TokenStandard.ERC721; + } + return undefined; + }, [confirmation, primaryType]); + + return { + primaryType: primaryType as keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard, + }; +};