diff --git a/.circleci/config.yml b/.circleci/config.yml index 095650aae02d..b2c5ab712973 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,11 +118,9 @@ workflows: - prep-deps - get-changed-files-with-git-diff: filters: - branches: - ignore: - - master - requires: - - prep-deps + branches: + ignore: + - master - test-deps-audit: requires: - prep-deps @@ -360,11 +358,10 @@ workflows: value: << pipeline.git.branch >> jobs: - prep-deps - - get-changed-files-with-git-diff: - requires: - - prep-deps + - get-changed-files-with-git-diff - validate-locales-only: requires: + - prep-deps - get-changed-files-with-git-diff - test-lint: requires: @@ -501,7 +498,6 @@ jobs: - run: sudo corepack enable - attach_workspace: at: . - - gh/install - run: name: Get changed files with git diff command: npx tsx .circleci/scripts/git-diff-develop.ts diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 3cf5022d4e12..43435db17418 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -1,4 +1,3 @@ -import { hasProperty } from '@metamask/utils'; import { exec as execCallback } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -6,24 +5,38 @@ import { promisify } from 'util'; const exec = promisify(execCallback); +// The CIRCLE_PR_NUMBER variable is only available on forked Pull Requests +const PR_NUMBER = + process.env.CIRCLE_PR_NUMBER || + process.env.CIRCLE_PULL_REQUEST?.split('/').pop(); + const MAIN_BRANCH = 'develop'; +const SOURCE_BRANCH = `refs/pull/${PR_NUMBER}/head`; + +const CHANGED_FILES_DIR = 'changed-files'; + +type PRInfo = { + base: { + ref: string; + }; + body: string; +}; /** - * Get the target branch for the given pull request. + * Get JSON info about the given pull request * - * @returns The name of the branch targeted by the PR. + * @returns JSON info from GitHub */ -async function getBaseRef(): Promise { - if (!process.env.CIRCLE_PULL_REQUEST) { +async function getPrInfo(): Promise { + if (!PR_NUMBER) { return null; } - // We're referencing the CIRCLE_PULL_REQUEST environment variable within the script rather than - // passing it in because this makes it easier to use Bash parameter expansion to extract the - // PR number from the URL. - const result = await exec(`gh pr view --json baseRefName "\${CIRCLE_PULL_REQUEST##*/}" --jq '.baseRefName'`); - const baseRef = result.stdout.trim(); - return baseRef; + return await ( + await fetch( + `https://api.github.com/repos/${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}/pulls/${PR_NUMBER}`, + ) + ).json(); } /** @@ -34,8 +47,10 @@ async function getBaseRef(): Promise { */ async function fetchWithDepth(depth: number): Promise { try { - await exec(`git fetch --depth ${depth} origin develop`); - await exec(`git fetch --depth ${depth} origin ${process.env.CIRCLE_BRANCH}`); + await exec(`git fetch --depth ${depth} origin "${MAIN_BRANCH}"`); + await exec( + `git fetch --depth ${depth} origin "${SOURCE_BRANCH}:${SOURCE_BRANCH}"`, + ); return true; } catch (error: unknown) { console.error(`Failed to fetch with depth ${depth}:`, error); @@ -59,18 +74,16 @@ async function fetchUntilMergeBaseFound() { await exec(`git merge-base origin/HEAD HEAD`); return; } catch (error: unknown) { - if ( - error instanceof Error && - hasProperty(error, 'code') && - error.code === 1 - ) { - console.error(`Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`); + if (error instanceof Error && 'code' in error) { + console.error( + `Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`, + ); } else { throw error; } } } - await exec(`git fetch --unshallow origin develop`); + await exec(`git fetch --unshallow origin "${MAIN_BRANCH}"`); } /** @@ -82,50 +95,64 @@ async function fetchUntilMergeBaseFound() { */ async function gitDiff(): Promise { await fetchUntilMergeBaseFound(); - const { stdout: diffResult } = await exec(`git diff --name-only origin/HEAD...${process.env.CIRCLE_BRANCH}`); + const { stdout: diffResult } = await exec( + `git diff --name-only "origin/HEAD...${SOURCE_BRANCH}"`, + ); if (!diffResult) { - throw new Error('Unable to get diff after full checkout.'); + throw new Error('Unable to get diff after full checkout.'); } return diffResult; } +function writePrBodyToFile(prBody: string) { + const prBodyPath = path.resolve(CHANGED_FILES_DIR, 'pr-body.txt'); + fs.writeFileSync(prBodyPath, prBody.trim()); + console.log(`PR body saved to ${prBodyPath}`); +} + /** - * Stores the output of git diff to a file. + * Main run function, stores the output of git diff and the body of the matching PR to a file. * - * @returns Returns a promise that resolves when the git diff output is successfully stored. + * @returns Returns a promise that resolves when the git diff output and PR body is successfully stored. */ -async function storeGitDiffOutput() { +async function storeGitDiffOutputAndPrBody() { try { // Create the directory // This is done first because our CirleCI config requires that this directory is present, // even if we want to skip this step. - const outputDir = 'changed-files'; - fs.mkdirSync(outputDir, { recursive: true }); + fs.mkdirSync(CHANGED_FILES_DIR, { recursive: true }); - console.log(`Determining whether this run is for a PR targetting ${MAIN_BRANCH}`) - if (!process.env.CIRCLE_PULL_REQUEST) { - console.log("Not a PR, skipping git diff"); + console.log( + `Determining whether this run is for a PR targeting ${MAIN_BRANCH}`, + ); + if (!PR_NUMBER) { + console.log('Not a PR, skipping git diff'); return; } - const baseRef = await getBaseRef(); - if (baseRef === null) { - console.log("Not a PR, skipping git diff"); + const prInfo = await getPrInfo(); + + const baseRef = prInfo?.base.ref; + if (!baseRef) { + console.log('Not a PR, skipping git diff'); return; } else if (baseRef !== MAIN_BRANCH) { console.log(`This is for a PR targeting '${baseRef}', skipping git diff`); + writePrBodyToFile(prInfo.body); return; } - console.log("Attempting to get git diff..."); + console.log('Attempting to get git diff...'); const diffOutput = await gitDiff(); console.log(diffOutput); // Store the output of git diff - const outputPath = path.resolve(outputDir, 'changed-files.txt'); + const outputPath = path.resolve(CHANGED_FILES_DIR, 'changed-files.txt'); fs.writeFileSync(outputPath, diffOutput.trim()); - console.log(`Git diff results saved to ${outputPath}`); + + writePrBodyToFile(prInfo.body); + process.exit(0); } catch (error: any) { console.error('An error occurred:', error.message); @@ -133,4 +160,4 @@ async function storeGitDiffOutput() { } } -storeGitDiffOutput(); +storeGitDiffOutputAndPrBody(); diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml index f246bde7eb32..fd1b0d5134e3 100644 --- a/.github/workflows/update-coverage.yml +++ b/.github/workflows/update-coverage.yml @@ -6,10 +6,6 @@ on: - cron: 0 0 * * * workflow_dispatch: -permissions: - contents: write - pull-requests: write - jobs: run-tests: name: Run tests @@ -27,6 +23,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} - name: Update coverage run: | @@ -34,8 +32,8 @@ jobs: - name: Checkout/create branch, commit, and force push run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "MetaMask Bot" + git config user.email "metamaskbot@users.noreply.github.com" git checkout -b metamaskbot/update-coverage git add coverage.json git commit -m "chore: Update coverage.json" @@ -43,6 +41,6 @@ jobs: - name: Create/update pull request env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} run: | gh pr create --title "chore: Update coverage.json" --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." --base develop --head metamaskbot/update-coverage || gh pr edit --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." diff --git a/.storybook/test-data.js b/.storybook/test-data.js index de94b69f857e..cbcebb6347ed 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -677,6 +677,11 @@ const state = { currentLocale: 'en', preferences: { showNativeTokenAsMainBalance: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }, incomingTransactionsPreferences: { [CHAIN_IDS.MAINNET]: true, diff --git a/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch b/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch new file mode 100644 index 000000000000..7a5837cd4818 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-38.2.0-40af2afaa7.patch @@ -0,0 +1,35 @@ +diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs +index e90a1b6767bc8ac54b7a4d580035cf5db6861dca..a5e0f03d2541b4e3540431ef2e6e4b60fb7ae9fe 100644 +--- a/dist/assetsUtil.cjs ++++ b/dist/assetsUtil.cjs +@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); ++function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } + exports.fetchTokenContractExchangeRates = exports.reduceInBatchesSerially = exports.divideIntoBatches = exports.ethersBigNumberToBN = exports.addUrlProtocolPrefix = exports.getFormattedIpfsUrl = exports.getIpfsCIDv1AndPath = exports.removeIpfsProtocolPrefix = exports.isTokenListSupportedForNetwork = exports.isTokenDetectionSupportedForNetwork = exports.SupportedTokenDetectionNetworks = exports.formatIconUrlWithProxy = exports.formatAggregatorNames = exports.hasNewCollectionFields = exports.compareNftMetadata = exports.TOKEN_PRICES_BATCH_SIZE = void 0; + const controller_utils_1 = require("@metamask/controller-utils"); + const utils_1 = require("@metamask/utils"); +@@ -221,7 +222,7 @@ async function getIpfsCIDv1AndPath(ipfsUrl) { + const index = url.indexOf('/'); + const cid = index !== -1 ? url.substring(0, index) : url; + const path = index !== -1 ? url.substring(index) : undefined; +- const { CID } = await import("multiformats"); ++ const { CID } = _interopRequireWildcard(require("multiformats")); + // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) + // because most cid v0s appear to be incompatible with IPFS subdomains + return { +diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs +index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..b89849c0caf7e5db3b53cf03dd5746b6b1433543 100644 +--- a/dist/token-prices-service/codefi-v2.mjs ++++ b/dist/token-prices-service/codefi-v2.mjs +@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function ( + var _CodefiTokenPricesServiceV2_tokenPricePolicy; + import { handleFetch } from "@metamask/controller-utils"; + import { hexToNumber } from "@metamask/utils"; +-import $cockatiel from "cockatiel"; +-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel; ++import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel"; + /** + * The list of currencies that can be supplied as the `vsCurrency` parameter to + * the `/spot-prices` endpoint, in lowercase form. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd79055e0c3..c5fbcff7a94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [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)) +- Added smart transactions functionality for hardware wallet users ([#26251](https://github.com/MetaMask/metamask-extension/pull/26251)) +- Added new custom UI components for Snaps developers ([#26675](https://github.com/MetaMask/metamask-extension/pull/26675)) +- Add support for footers to Snap home pages ([#26463](https://github.com/MetaMask/metamask-extension/pull/26463)) +- [FLASK] Added Account Watcher as a preinstalled snap and added it to the menu list ([#26402](https://github.com/MetaMask/metamask-extension/pull/26402)) +- [FLASK] Added footers to Snap home pages ([#26463](https://github.com/MetaMask/metamask-extension/pull/26463)) +- Added icons for IoTeX network ([#26723](https://github.com/MetaMask/metamask-extension/pull/26723)) +- Added NEAR icon for chainId 397 and 398 ([#26459](https://github.com/MetaMask/metamask-extension/pull/26459)) + + +### Changed +- Redesign contract deployment transaction screen ([#26382](https://github.com/MetaMask/metamask-extension/pull/26382)) +- Improve performance, reliability and coverage of the phishing detection feature ([#25839](https://github.com/MetaMask/metamask-extension/pull/25839)) +- Updated Moonbeam and Moonriver network and token logos ([#26677](https://github.com/MetaMask/metamask-extension/pull/26677)) +- Updated UI for add network notification window ([#25777](https://github.com/MetaMask/metamask-extension/pull/25777)) +- Update visual styling of token lists ([#26300](https://github.com/MetaMask/metamask-extension/pull/26300)) +- Update spacing on Snap home page ([#26462](https://github.com/MetaMask/metamask-extension/pull/26462)) +- [FLASK] Integrated Snaps into the redesigned confirmation pages ([#26435](https://github.com/MetaMask/metamask-extension/pull/26435)) + +### Fixed +- Fixed network change toast width in wide screen mode ([#26532](https://github.com/MetaMask/metamask-extension/pull/26532)) +- Fixed missing deadline in swaps smart transaction status screen ([#25779](https://github.com/MetaMask/metamask-extension/pull/25779)) +- Improved Snap Address component UI/UX; stop using petnames in custom Snaps UIs ([#26477](https://github.com/MetaMask/metamask-extension/pull/26477)) +- Fixed bug that could prevent the Import NFT modal from closing after importing some tokens ([#26269](https://github.com/MetaMask/metamask-extension/pull/26269)) + ## [12.3.1] ### Fixed - Fix duplicate network validation ([#27463](https://github.com/MetaMask/metamask-extension/pull/27463)) @@ -5112,7 +5139,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.3.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD +[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 [12.2.4]: https://github.com/MetaMask/metamask-extension/compare/v12.2.3...v12.2.4 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index bda0d4d894e7..8c91aec52887 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask ist mit dieser Seite verbunden, aber es sind noch keine Konten verbunden" }, - "connectedWith": { - "message": "Verbunden mit" - }, "connecting": { "message": "Verbinden" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Wenn Sie die Verbindung zwischen $1 und $2 unterbrechen, müssen Sie die Verbindung wiederherstellen, um sie erneut zu verwenden.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Alle $1 trennen", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 trennen" }, @@ -2835,10 +2824,6 @@ "message": "$1 bittet um Ihre Zustimmung zu:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Möchten Sie, dass diese Website Folgendes tut?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Das native Token dieses Netzwerks ist $1. Dieses Token wird für die Gas-Gebühr verwendet. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 6010f1939602..4f29362124bd 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Το MetaMask είναι συνδεδεμένο σε αυτόν τον ιστότοπο, αλλά δεν έχουν συνδεθεί ακόμα λογαριασμοί" }, - "connectedWith": { - "message": "Συνδέεται με" - }, "connecting": { "message": "Σύνδεση" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Αν αποσυνδέσετε τo $1 από τo $2, θα πρέπει να επανασυνδεθείτε για να τα χρησιμοποιήσετε ξανά.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Αποσύνδεση όλων των $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Αποσύνδεση $1" }, @@ -2835,10 +2824,6 @@ "message": "Το $1 ζητάει την έγκρισή σας για:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Θέλετε αυτός ο ιστότοπος να κάνει τα εξής;", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Το αρχικό token σε αυτό το δίκτυο είναι το $1. Είναι το token που χρησιμοποιείται για τα τέλη συναλλαγών.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 30c913d1de74..ecaedb3201d0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1180,9 +1180,6 @@ "connectedSnaps": { "message": "Connected Snaps" }, - "connectedWith": { - "message": "Connected with" - }, "connectedWithAccount": { "message": "$1 accounts connected", "description": "$1 represents account length" @@ -1647,14 +1644,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "If you disconnect your $1 from $2, you'll need to reconnect to use them again.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Disconnect all $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectMessage": { "message": "This will disconnect you from $1", "description": "$1 is the name of the dapp" @@ -1838,6 +1827,9 @@ "editSpendingCapDesc": { "message": "Enter the amount that you feel comfortable being spent on your behalf." }, + "editSpendingCapError": { + "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." + }, "enable": { "message": "Enable" }, @@ -3050,10 +3042,6 @@ "message": "$1 is asking for your approval to:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Do you want this site to do the following?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "The native token on this network is $1. It is the token used for gas fees. ", "description": "$1 represents the name of the native token on the current network" @@ -5245,6 +5233,16 @@ "somethingWentWrong": { "message": "Oops! Something went wrong." }, + "sortBy": { + "message": "Sort by" + }, + "sortByAlphabetically": { + "message": "Alphabetically (A-Z)" + }, + "sortByDecliningBalance": { + "message": "Declining balance ($1 high-low)", + "description": "Indicates a descending order based on token fiat balance. $1 is the preferred currency symbol" + }, "source": { "message": "Source" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 772471fdfd65..49c523b184f6 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1085,9 +1085,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask está conectado a este sitio, pero aún no hay cuentas conectadas" }, - "connectedWith": { - "message": "Conectado con" - }, "connecting": { "message": "Conectando" }, @@ -1504,14 +1501,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Si desconecta su $1 de su $2, tendrá que volver a conectarlos para usarlos nuevamente.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Desconectar todos/as $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -2832,10 +2821,6 @@ "message": "$1 solicita su aprobación para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "¿Desea que este sitio haga lo siguiente?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "El token nativo en esta red es de $1. Es el token utilizado para las tarifas de gas. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 4a537a554315..0c5015f67665 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask est connecté à ce site, mais aucun compte n’est encore connecté" }, - "connectedWith": { - "message": "Connecté avec" - }, "connecting": { "message": "Connexion…" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Si vous déconnectez vos $1 de $2, vous devrez vous reconnecter pour les utiliser à nouveau.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Déconnecter tous les $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Déconnecter $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 vous demande votre approbation pour :", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Voulez-vous que ce site fasse ce qui suit ?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Le jeton natif de ce réseau est $1. C’est le jeton utilisé pour les frais de gaz. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7fb1a04cb137..274aae47e2e3 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask इस साइट से कनेक्टेड है, लेकिन अभी तक कोई अकाउंट कनेक्ट नहीं किया गया है" }, - "connectedWith": { - "message": "से कनेक्ट किया गया" - }, "connecting": { "message": "कनेक्ट किया जा रहा है" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "अगर आप अपने $1 को $2 से डिस्कनेक्ट करते हैं, तो आपको उन्हें दोबारा इस्तेमाल करने के लिए रिकनेक्ट करना होगा।", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "सभी $1 को डिस्कनेक्ट करें", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 डिस्कनेक्ट करें" }, @@ -2835,10 +2824,6 @@ "message": "$1 निम्नलिखित के लिए आपका एप्रूवल मांग रहा है:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "क्या आप चाहते हैं कि यह साइट निम्नलिखित कार्य करे?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "इस नेटवर्क पर ओरिजिनल टोकन $1 है। यह गैस फ़ीस के लिए इस्तेमाल किया जाने वाला टोकन है।", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index be3ef95ad448..5f36af7a382d 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask terhubung ke situs ini, tetapi belum ada akun yang terhubung" }, - "connectedWith": { - "message": "Terhubung dengan" - }, "connecting": { "message": "Menghubungkan" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "Jika Anda memutus koneksi $1 dari $2, Anda harus menghubungkannya kembali agar dapat menggunakannya lagi.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Putuskan semua koneksi $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Putuskan koneksi $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 meminta persetujuan Anda untuk:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Ingin situs ini melakukan hal berikut?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Token asli di jaringan ini adalah $1. Ini merupakan token yang digunakan untuk biaya gas. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 1ffbc9f1e4eb..c8adf1ff5af9 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMaskはこのサイトに接続されていますが、まだアカウントは接続されていません" }, - "connectedWith": { - "message": "接続先" - }, "connecting": { "message": "接続中..." }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "$1と$2の接続を解除した場合、再び使用するには再度接続する必要があります。", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "すべての$1の接続を解除", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1を接続解除" }, @@ -2835,10 +2824,6 @@ "message": "$1が次の承認を求めています:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "このサイトに次のことを希望しますか?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "このネットワークのネイティブトークンは$1です。ガス代にもこのトークンが使用されます。", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index a1c79024f651..5868672bce32 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask는 이 사이트와 연결되어 있지만, 아직 연결된 계정이 없습니다" }, - "connectedWith": { - "message": "연결 대상:" - }, "connecting": { "message": "연결 중" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "$2에서 $1의 연결을 끊은 경우, 다시 사용하려면 다시 연결해야 합니다.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "모든 $1 연결 해제", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 연결 해제" }, @@ -2835,10 +2824,6 @@ "message": "$1에서 다음 승인을 요청합니다:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "이 사이트가 다음을 수행하기 원하십니까?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "이 네트워크의 네이티브 토큰은 $1입니다. 이는 가스비 지불에 사용하는 토큰입니다. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 52eb392f9d94..298f4b8b8d70 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "A MetaMask está conectada a este site, mas nenhuma conta está conectada ainda" }, - "connectedWith": { - "message": "Conectado com" - }, "connecting": { "message": "Conectando" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Se desconectar $1 de $2, você precisará reconectar para usar novamente.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Desconectar $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Desconectar $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 solicita sua aprovação para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Deseja que este site faça o seguinte?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "O token nativo dessa rede é $1. Esse é o token usado para taxas de gás.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 9f4f15461bab..999f237f73ea 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask подключен к этому сайту, но счета пока не подключены" }, - "connectedWith": { - "message": "Подключен(-а) к" - }, "connecting": { "message": "Подключение..." }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snaps" }, - "disconnectAllText": { - "message": "Если вы отключите свои $1 от $2, вам придется повторно подключиться, чтобы использовать их снова.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Отключить все $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Отключить $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 запрашивает ваше одобрение на:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Вы хотите, чтобы этот сайт делал следующее?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Нативный токен этой сети — $1. Этот токен используется для внесения платы за газ. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c2ffc42763d0..df021e9dfdad 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Konektado ang MetaMask sa site na ito, ngunit wala pang mga account ang konektado" }, - "connectedWith": { - "message": "Nakakonekta sa" - }, "connecting": { "message": "Kumokonekta" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Mga Snap" }, - "disconnectAllText": { - "message": "Kapag idiniskonekta mo ang iyong $1 mula sa $2, kailangan mong muling ikonekta para gamitin muli.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Idiskonekta ang lahat ng $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Idiskonekta $1" }, @@ -2835,10 +2824,6 @@ "message": "Ang $1 ay humihiling ng iyong pag-apruba para:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Gusto mo bang gawin ng site na ito ang mga sumusunod?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Ang native token sa network na ito ay $1. Ito ang token na ginagamit para sa mga gas fee. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 676896deaaae..ce36a61ca716 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask bu siteye bağlı ancak henüz bağlı hesap yok" }, - "connectedWith": { - "message": "Şununla bağlanıldı:" - }, "connecting": { "message": "Bağlanıyor" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap'ler" }, - "disconnectAllText": { - "message": "$1 ile $2 bağlantısını keserseniz onları tekrar kullanmak için tekrar bağlamanız gerekir.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Tüm $1 bağlantısını kes", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "$1 bağlantısını kes" }, @@ -2835,10 +2824,6 @@ "message": "$1 sizden şunun için onay istiyor:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Bu sitenin aşağıdakileri yapmasına izin vermek istiyor musunuz?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Bu ağdaki yerli token $1. Bu, gaz ücretleri için kullanılan tokendir. ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 442478665c00..5766a1789d24 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask được kết nối với trang web này, nhưng chưa có tài khoản nào được kết nối" }, - "connectedWith": { - "message": "Đã kết nối với" - }, "connecting": { "message": "Đang kết nối" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "Nếu bạn ngắt kết nối $1 khỏi $2, bạn sẽ cần kết nối lại để sử dụng lại.", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "Ngắt kết nối tất cả $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "Ngắt kết nối $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 đang yêu cầu sự chấp thuận của bạn cho:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "Bạn có muốn trang web này thực hiện những điều sau không?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "Token gốc của mạng này là $1. Token này được dùng làm phí gas.", "description": "$1 represents the name of the native token on the current network" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9f33ef4a6b35..a5e2b1175862 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1088,9 +1088,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask 已连接到此网站,但尚未连接任何账户" }, - "connectedWith": { - "message": "已连接" - }, "connecting": { "message": "连接中……" }, @@ -1507,14 +1504,6 @@ "disconnectAllSnapsText": { "message": "Snap" }, - "disconnectAllText": { - "message": "如果您将 $1 与 $2 断开连接,则需要重新连接才能再次使用。", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`, $2 represents the website hostname" - }, - "disconnectAllTitle": { - "message": "断开连接所有 $1", - "description": "$1 will map to `disconnectAllAccountsText` or `disconnectAllSnapsText`" - }, "disconnectPrompt": { "message": "断开连接 $1" }, @@ -2835,10 +2824,6 @@ "message": "$1 请求您的批准,以便:", "description": "$1 represents dapp name" }, - "nativePermissionRequestDescription": { - "message": "您希望此网站执行以下操作吗?", - "description": "Description below header used on Permission Connect screen for native permissions." - }, "nativeToken": { "message": "此网络上的原生代币为 $1。它是用于燃料费的代币。 ", "description": "$1 represents the name of the native token on the current network" diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index ef1dbe02789a..15f4fa9b7788 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -28,6 +28,10 @@ import { TransactionMetaMetricsEvent, } from '../../../shared/constants/transaction'; +///: BEGIN:ONLY_INCLUDE_IF(build-main) +import { ENVIRONMENT } from '../../../development/build/constants'; +///: END:ONLY_INCLUDE_IF + const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled'; export const overrideAnonymousEventNames = { @@ -484,8 +488,10 @@ export default class MetaMetricsController { this.setMarketingCampaignCookieId(null); } - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - this.updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId); + ///: BEGIN:ONLY_INCLUDE_IF(build-main) + if (this.environment !== ENVIRONMENT.DEVELOPMENT) { + this.updateExtensionUninstallUrl(participateInMetaMetrics, metaMetricsId); + } ///: END:ONLY_INCLUDE_IF return metaMetricsId; @@ -862,6 +868,8 @@ export default class MetaMetricsController { metamaskState.participateInMetaMetrics, [MetaMetricsUserTrait.HasMarketingConsent]: metamaskState.dataCollectionForMarketing, + [MetaMetricsUserTrait.TokenSortPreference]: + metamaskState.tokenSortConfig?.key || '', }; if (!previousUserTraits) { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 3d4845e056d0..a0505700ef01 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1122,6 +1122,11 @@ describe('MetaMetricsController', function () { }, }, }, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }); expect(traits).toStrictEqual({ @@ -1153,6 +1158,7 @@ describe('MetaMetricsController', function () { ///: BEGIN:ONLY_INCLUDE_IF(petnames) [MetaMetricsUserTrait.PetnameAddressCount]: 3, ///: END:ONLY_INCLUDE_IF + [MetaMetricsUserTrait.TokenSortPreference]: 'token-sort-key', }); }); @@ -1181,6 +1187,11 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); @@ -1208,6 +1219,11 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: false, }); @@ -1245,6 +1261,11 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); @@ -1267,6 +1288,11 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, showNativeTokenAsMainBalance: true, }); expect(updatedTraits).toStrictEqual(null); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index a158ac0024d4..eb126b176a41 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -106,6 +106,11 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + tokenSortConfig: { + key: string; + order: string; + sortCallback: string; + }; shouldShowAggregatedBalancePopover: boolean; }; @@ -237,6 +242,11 @@ export default class PreferencesController { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, // by default user should see popover; }, // ENS decentralized website resolution diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index 9f9ead59ed90..e657fe47e64f 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -274,6 +274,8 @@ export class BalancesController extends BaseController< * @param accountId - The account ID. */ async updateBalance(accountId: string) { + // NOTE: No need to track the account here, since we start tracking those when + // the "AccountsController:accountAdded" is fired. await this.#tracker.updateBalance(accountId); } @@ -311,6 +313,13 @@ export class BalancesController extends BaseController< } this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + // NOTE: Unfortunately, we cannot update the balance right away here, because + // messenger's events are running synchronously and fetching the balance is + // asynchronous. + // Updating the balance here would resume at some point but the event emitter + // will not `await` this (so we have no real control "when" the balance will + // really be updated), see: + // - https://github.com/MetaMask/core/blob/v213.0.0/packages/accounts-controller/src/AccountsController.ts#L1036-L1039 } /** diff --git a/app/scripts/lib/accounts/BalancesTracker.ts b/app/scripts/lib/accounts/BalancesTracker.ts index 48ecd6f84cca..7359bcd2f8b6 100644 --- a/app/scripts/lib/accounts/BalancesTracker.ts +++ b/app/scripts/lib/accounts/BalancesTracker.ts @@ -102,7 +102,8 @@ export class BalancesTracker { // and try to sync with the "real block time"! const info = this.#balances[accountId]; const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; - if (isOutdated) { + const hasNoBalanceYet = info.lastUpdated === 0; + if (hasNoBalanceYet || isOutdated) { await this.#updateBalance(accountId); this.#balances[accountId].lastUpdated = Date.now(); } diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts index a013373ac9f2..93925bf63a0c 100644 --- a/app/scripts/lib/manifestFlags.ts +++ b/app/scripts/lib/manifestFlags.ts @@ -11,7 +11,7 @@ export type ManifestFlags = { }; sentry?: { tracesSampleRate?: number; - doNotForceSentryForThisTest?: boolean; + forceEnable?: boolean; }; }; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index e224cb4a2b38..2f4727fdab36 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -23,7 +23,6 @@ const addEthereumChain = { getCurrentChainIdForDomain: true, getCaveat: true, requestPermittedChainsPermission: true, - getChainPermissionsFeatureFlag: true, grantPermittedChainsPermissionIncremental: true, }, }; @@ -46,7 +45,6 @@ async function addEthereumChainHandler( getCurrentChainIdForDomain, getCaveat, requestPermittedChainsPermission, - getChainPermissionsFeatureFlag, grantPermittedChainsPermissionIncremental, }, ) { @@ -67,9 +65,6 @@ async function addEthereumChainHandler( const { origin } = req; const currentChainIdForDomain = getCurrentChainIdForDomain(origin); - const currentNetworkConfiguration = getNetworkConfigurationByChainId( - currentChainIdForDomain, - ); const existingNetwork = getNetworkConfigurationByChainId(chainId); if ( @@ -198,30 +193,14 @@ async function addEthereumChainHandler( const { networkClientId } = updatedNetwork.rpcEndpoints[updatedNetwork.defaultRpcEndpointIndex]; - const requestData = { - toNetworkConfiguration: updatedNetwork, - fromNetworkConfiguration: currentNetworkConfiguration, - }; - - return switchChain( - res, - end, - origin, - chainId, - requestData, - networkClientId, - approvalFlowId, - { - isAddFlow: true, - getChainPermissionsFeatureFlag, - setActiveNetwork, - requestUserApproval, - getCaveat, - requestPermittedChainsPermission, - endApprovalFlow, - grantPermittedChainsPermissionIncremental, - }, - ); + return switchChain(res, end, chainId, networkClientId, approvalFlowId, { + isAddFlow: true, + setActiveNetwork, + endApprovalFlow, + getCaveat, + requestPermittedChainsPermission, + grantPermittedChainsPermissionIncremental, + }); } else if (approvalFlowId) { endApprovalFlow({ id: approvalFlowId }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js index f6be2deb6f08..945953cff562 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js @@ -54,14 +54,8 @@ const createMockNonInfuraConfiguration = () => ({ describe('addEthereumChainHandler', () => { const addEthereumChainHandler = addEthereumChain.implementation; - - const makeMocks = ({ - permissionedChainIds = [], - permissionsFeatureFlagIsActive, - overrides = {}, - } = {}) => { + const makeMocks = ({ permissionedChainIds = [], overrides = {} } = {}) => { return { - getChainPermissionsFeatureFlag: () => permissionsFeatureFlagIsActive, getCurrentChainIdForDomain: jest .fn() .mockReturnValue(NON_INFURA_CHAIN_ID), @@ -92,9 +86,7 @@ describe('addEthereumChainHandler', () => { describe('with `endowment:permitted-chains` permissioning inactive', () => { it('creates a new network configuration for the given chainid and switches to it if none exists', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); await addEthereumChainHandler( { origin: 'example.com', @@ -118,8 +110,7 @@ describe('addEthereumChainHandler', () => { mocks, ); - // called twice, once for the add and once for the switch - expect(mocks.requestUserApproval).toHaveBeenCalledTimes(2); + expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1); expect(mocks.addNetwork).toHaveBeenCalledTimes(1); expect(mocks.addNetwork).toHaveBeenCalledWith({ blockExplorerUrls: ['https://optimistic.etherscan.io'], @@ -141,9 +132,7 @@ describe('addEthereumChainHandler', () => { }); it('creates a new networkConfiguration when called without "blockExplorerUrls" property', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); await addEthereumChainHandler( { origin: 'example.com', @@ -172,7 +161,6 @@ describe('addEthereumChainHandler', () => { describe('if a networkConfiguration for the given chainId already exists', () => { it('updates the existing networkConfiguration with the new rpc url if it doesnt already exist', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -258,7 +246,6 @@ describe('addEthereumChainHandler', () => { }; const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -305,7 +292,6 @@ describe('addEthereumChainHandler', () => { const existingNetwork = createMockMainnetConfiguration(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { // Start on sepolia getCurrentChainIdForDomain: jest @@ -349,9 +335,7 @@ describe('addEthereumChainHandler', () => { }); it('should return error for invalid chainId', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); const mockEnd = jest.fn(); await addEthereumChainHandler( @@ -380,7 +364,6 @@ describe('addEthereumChainHandler', () => { const mocks = makeMocks({ permissionedChainIds: [], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -427,7 +410,6 @@ describe('addEthereumChainHandler', () => { it('create a new networkConfiguration and switches to it without requesting permissions, if the requested chainId has `endowment:permitted-chains` permission granted for requesting origin', async () => { const mocks = makeMocks({ permissionedChainIds: [CHAIN_IDS.MAINNET], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -465,7 +447,6 @@ describe('addEthereumChainHandler', () => { it('create a new networkConfiguration, requests permissions and switches to it, if the requested chainId does not have permittedChains permission granted for requesting origin', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive: true, permissionedChainIds: [], overrides: { getNetworkConfigurationByChainId: jest @@ -516,7 +497,6 @@ describe('addEthereumChainHandler', () => { createMockOptimismConfiguration().chainId, CHAIN_IDS.MAINNET, ], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -562,9 +542,7 @@ describe('addEthereumChainHandler', () => { }); it('should return an error if an unexpected parameter is provided', async () => { - const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, - }); + const mocks = makeMocks(); const mockEnd = jest.fn(); const unexpectedParam = 'unexpected'; @@ -604,7 +582,6 @@ describe('addEthereumChainHandler', () => { it('should handle errors during the switch network permission request', async () => { const mockError = new Error('Permission request failed'); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: true, permissionedChainIds: [], overrides: { getCurrentChainIdForDomain: jest @@ -649,7 +626,6 @@ describe('addEthereumChainHandler', () => { it('should return an error if nativeCurrency.symbol does not match an existing network with the same chainId', async () => { const mocks = makeMocks({ permissionedChainIds: [CHAIN_IDS.MAINNET], - permissionsFeatureFlagIsActive: true, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -691,7 +667,6 @@ describe('addEthereumChainHandler', () => { const CURRENT_RPC_CONFIG = createMockNonInfuraConfiguration(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getCurrentChainIdForDomain: jest .fn() diff --git a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js index 57d14eb6e6b8..080fef549564 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js @@ -1,5 +1,4 @@ import { errorCodes, ethErrors } from 'eth-rpc-errors'; -import { ApprovalType } from '@metamask/controller-utils'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -156,46 +155,34 @@ export function validateAddEthereumChainParams(params, end) { export async function switchChain( res, end, - origin, chainId, - requestData, networkClientId, approvalFlowId, { isAddFlow, - getChainPermissionsFeatureFlag, setActiveNetwork, endApprovalFlow, - requestUserApproval, getCaveat, requestPermittedChainsPermission, grantPermittedChainsPermissionIncremental, }, ) { try { - if (getChainPermissionsFeatureFlag()) { - const { value: permissionedChainIds } = - getCaveat({ - target: PermissionNames.permittedChains, - caveatType: CaveatTypes.restrictNetworkSwitching, - }) ?? {}; - - if ( - permissionedChainIds === undefined || - !permissionedChainIds.includes(chainId) - ) { - if (isAddFlow) { - await grantPermittedChainsPermissionIncremental([chainId]); - } else { - await requestPermittedChainsPermission([chainId]); - } + const { value: permissionedChainIds } = + getCaveat({ + target: PermissionNames.permittedChains, + caveatType: CaveatTypes.restrictNetworkSwitching, + }) ?? {}; + + if ( + permissionedChainIds === undefined || + !permissionedChainIds.includes(chainId) + ) { + if (isAddFlow) { + await grantPermittedChainsPermissionIncremental([chainId]); + } else { + await requestPermittedChainsPermission([chainId]); } - } else { - await requestUserApproval({ - origin, - type: ApprovalType.SwitchEthereumChain, - requestData, - }); } await setActiveNetwork(networkClientId); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts index 530b48b25164..c95b66e1a20d 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts @@ -7,7 +7,6 @@ import type { JsonRpcParams, Hex, } from '@metamask/utils'; -import { OriginString } from '@metamask/permission-controller'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { HandlerWrapper, @@ -28,7 +27,7 @@ export type ProviderStateHandlerResult = { }; export type GetProviderState = ( - origin: OriginString, + origin: string, ) => Promise; type GetProviderStateConstraint = diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js deleted file mode 100644 index e7957192cd56..000000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js +++ /dev/null @@ -1,48 +0,0 @@ -import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; - -/** - * This RPC method is called by the inpage provider whenever it detects the - * accessing of a non-existent property on our window.web3 shim. We use this - * to alert the user that they are using a legacy dapp, and will have to take - * further steps to be able to use it. - */ -const logWeb3ShimUsage = { - methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], - implementation: logWeb3ShimUsageHandler, - hookNames: { - getWeb3ShimUsageState: true, - setWeb3ShimUsageRecorded: true, - }, -}; -export default logWeb3ShimUsage; - -/** - * @typedef {object} LogWeb3ShimUsageOptions - * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim - * usage state for the given origin. - * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim - * usage for a particular origin. - */ - -/** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. - * @param {Function} _next - The json-rpc-engine 'next' callback. - * @param {Function} end - The json-rpc-engine 'end' callback. - * @param {LogWeb3ShimUsageOptions} options - */ -function logWeb3ShimUsageHandler( - req, - res, - _next, - end, - { getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, -) { - const { origin } = req; - if (getWeb3ShimUsageState(origin) === undefined) { - setWeb3ShimUsageRecorded(origin); - } - - res.result = true; - return end(); -} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts new file mode 100644 index 000000000000..d81427af8c26 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.test.ts @@ -0,0 +1,46 @@ +import type { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import { PendingJsonRpcResponse } from '@metamask/utils'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { HandlerRequestType as LogWeb3ShimUsageHandlerRequest } from './types'; +import logWeb3ShimUsage, { + GetWeb3ShimUsageState, + SetWeb3ShimUsageRecorded, +} from './log-web3-shim-usage'; + +describe('logWeb3ShimUsage', () => { + let mockEnd: JsonRpcEngineEndCallback; + let mockGetWeb3ShimUsageState: GetWeb3ShimUsageState; + let mockSetWeb3ShimUsageRecorded: SetWeb3ShimUsageRecorded; + + beforeEach(() => { + mockEnd = jest.fn(); + mockGetWeb3ShimUsageState = jest.fn().mockReturnValue(undefined); + mockSetWeb3ShimUsageRecorded = jest.fn(); + }); + + it('should call getWeb3ShimUsageState and setWeb3ShimUsageRecorded when the handler is invoked', async () => { + const req: LogWeb3ShimUsageHandlerRequest = { + origin: 'testOrigin', + params: [], + id: '22', + jsonrpc: '2.0', + method: MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE, + }; + + const res: PendingJsonRpcResponse = { + id: '22', + jsonrpc: '2.0', + result: true, + }; + + logWeb3ShimUsage.implementation(req, res, jest.fn(), mockEnd, { + getWeb3ShimUsageState: mockGetWeb3ShimUsageState, + setWeb3ShimUsageRecorded: mockSetWeb3ShimUsageRecorded, + }); + + expect(mockGetWeb3ShimUsageState).toHaveBeenCalledWith(req.origin); + expect(mockSetWeb3ShimUsageRecorded).toHaveBeenCalled(); + expect(res.result).toStrictEqual(true); + expect(mockEnd).toHaveBeenCalled(); + }); +}); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts new file mode 100644 index 000000000000..bff4215ea5aa --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.ts @@ -0,0 +1,74 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from 'json-rpc-engine'; +import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { + HandlerWrapper, + HandlerRequestType as LogWeb3ShimUsageHandlerRequest, +} from './types'; + +export type GetWeb3ShimUsageState = (origin: string) => undefined | 1 | 2; +export type SetWeb3ShimUsageRecorded = (origin: string) => void; + +export type LogWeb3ShimUsageOptions = { + getWeb3ShimUsageState: GetWeb3ShimUsageState; + setWeb3ShimUsageRecorded: SetWeb3ShimUsageRecorded; +}; +type LogWeb3ShimUsageConstraint = + { + implementation: ( + req: LogWeb3ShimUsageHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { + getWeb3ShimUsageState, + setWeb3ShimUsageRecorded, + }: LogWeb3ShimUsageOptions, + ) => void; + } & HandlerWrapper; +/** + * This RPC method is called by the inpage provider whenever it detects the + * accessing of a non-existent property on our window.web3 shim. We use this + * to alert the user that they are using a legacy dapp, and will have to take + * further steps to be able to use it. + */ +const logWeb3ShimUsage = { + methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], + implementation: logWeb3ShimUsageHandler, + hookNames: { + getWeb3ShimUsageState: true, + setWeb3ShimUsageRecorded: true, + }, +} satisfies LogWeb3ShimUsageConstraint; + +export default logWeb3ShimUsage; + +/** + * @param req - The JSON-RPC request object. + * @param res - The JSON-RPC response object. + * @param _next - The json-rpc-engine 'next' callback. + * @param end - The json-rpc-engine 'end' callback. + * @param options + * @param options.getWeb3ShimUsageState - A function that gets web3 shim + * usage state for the given origin. + * @param options.setWeb3ShimUsageRecorded - A function that records web3 shim + * usage for a particular origin. + */ +function logWeb3ShimUsageHandler( + req: LogWeb3ShimUsageHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { getWeb3ShimUsageState, setWeb3ShimUsageRecorded }: LogWeb3ShimUsageOptions, +): void { + const { origin } = req; + if (getWeb3ShimUsageState(origin) === undefined) { + setWeb3ShimUsageRecorded(origin); + } + + res.result = true; + return end(); +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js index 847cdf8abe24..f43973e4ba57 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js @@ -14,8 +14,7 @@ const switchEthereumChain = { getCaveat: true, requestPermittedChainsPermission: true, getCurrentChainIdForDomain: true, - requestUserApproval: true, - getChainPermissionsFeatureFlag: true, + grantPermittedChainsPermissionIncremental: true, }, }; @@ -32,8 +31,7 @@ async function switchEthereumChainHandler( requestPermittedChainsPermission, getCaveat, getCurrentChainIdForDomain, - requestUserApproval, - getChainPermissionsFeatureFlag, + grantPermittedChainsPermissionIncremental, }, ) { let chainId; @@ -66,27 +64,10 @@ async function switchEthereumChainHandler( ); } - const requestData = { - toNetworkConfiguration: networkConfigurationForRequestedChainId, - fromNetworkConfiguration: getNetworkConfigurationByChainId( - currentChainIdForOrigin, - ), - }; - - return switchChain( - res, - end, - origin, - chainId, - requestData, - networkClientIdToSwitchTo, - null, - { - getChainPermissionsFeatureFlag, - setActiveNetwork, - requestUserApproval, - getCaveat, - requestPermittedChainsPermission, - }, - ); + return switchChain(res, end, chainId, networkClientIdToSwitchTo, null, { + setActiveNetwork, + getCaveat, + requestPermittedChainsPermission, + grantPermittedChainsPermissionIncremental, + }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js index 30a9f9aa8f8e..be612fbc7d8e 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js @@ -6,10 +6,6 @@ import switchEthereumChain from './switch-ethereum-chain'; const NON_INFURA_CHAIN_ID = '0x123456789'; -const mockRequestUserApproval = ({ requestData }) => { - return Promise.resolve(requestData.toNetworkConfiguration); -}; - const createMockMainnetConfiguration = () => ({ chainId: CHAIN_IDS.MAINNET, defaultRpcEndpointIndex: 0, @@ -33,7 +29,6 @@ const createMockLineaMainnetConfiguration = () => ({ describe('switchEthereumChainHandler', () => { const makeMocks = ({ permissionedChainIds = [], - permissionsFeatureFlagIsActive = false, overrides = {}, mockedGetNetworkConfigurationByChainIdReturnValue = createMockMainnetConfiguration(), mockedGetCurrentChainIdForDomainReturnValue = NON_INFURA_CHAIN_ID, @@ -42,15 +37,11 @@ describe('switchEthereumChainHandler', () => { mockGetCaveat.mockReturnValue({ value: permissionedChainIds }); return { - getChainPermissionsFeatureFlag: () => permissionsFeatureFlagIsActive, getCurrentChainIdForDomain: jest .fn() .mockReturnValue(mockedGetCurrentChainIdForDomainReturnValue), setNetworkClientIdForDomain: jest.fn(), setActiveNetwork: jest.fn(), - requestUserApproval: jest - .fn() - .mockImplementation(mockRequestUserApproval), requestPermittedChainsPermission: jest.fn(), getCaveat: mockGetCaveat, getNetworkConfigurationByChainId: jest @@ -65,11 +56,8 @@ describe('switchEthereumChainHandler', () => { }); describe('with permittedChains permissioning inactive', () => { - const permissionsFeatureFlagIsActive = false; - it('should call setActiveNetwork when switching to a built-in infura network', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -95,7 +83,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is lower case', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -121,7 +108,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is upper case', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -147,7 +133,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork when switching to a custom network', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -209,14 +194,11 @@ describe('switchEthereumChainHandler', () => { }); describe('with permittedChains permissioning active', () => { - const permissionsFeatureFlagIsActive = true; - it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in `endowment:permitted-chains`', async () => { const mockrequestPermittedChainsPermission = jest .fn() .mockResolvedValue(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { requestPermittedChainsPermission: mockrequestPermittedChainsPermission, @@ -246,7 +228,6 @@ describe('switchEthereumChainHandler', () => { it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in `endowment:permitted-chains`', async () => { const mocks = makeMocks({ - permissionsFeatureFlagIsActive, permissionedChainIds: [CHAIN_IDS.MAINNET], }); const switchEthereumChainHandler = switchEthereumChain.implementation; @@ -274,7 +255,6 @@ describe('switchEthereumChainHandler', () => { .fn() .mockRejectedValue(mockError); const mocks = makeMocks({ - permissionsFeatureFlagIsActive, overrides: { requestPermittedChainsPermission: mockrequestPermittedChainsPermission, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/types.ts b/app/scripts/lib/rpc-method-middleware/handlers/types.ts index 46ceef442ec2..91fa9c0dd1cc 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/types.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/types.ts @@ -1,4 +1,3 @@ -import { OriginString } from '@metamask/permission-controller'; import { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { MessageType } from '../../../../../shared/constants/app'; @@ -9,5 +8,5 @@ export type HandlerWrapper = { export type HandlerRequestType = Required> & { - origin: OriginString; + origin: string; }; diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 14e3bc0934d8..d440578144cc 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -123,7 +123,7 @@ function getTracesSampleRate(sentryTarget) { if (flags.circleci) { // Report very frequently on develop branch, and never on other branches - // (Unless you do a [flags.sentry.tracesSampleRate: x.xx] override) + // (Unless you use a `flags = {"sentry": {"tracesSampleRate": x.xx}}` override) if (flags.circleci.branch === 'develop') { return 0.03; } @@ -238,7 +238,7 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - getManifestFlags().sentry?.doNotForceSentryForThisTest || + !getManifestFlags().sentry?.forceEnable || (process.env.IN_TEST && !SENTRY_DSN_DEV) ) { return SENTRY_DSN_FAKE; @@ -272,7 +272,7 @@ async function getMetaMetricsEnabled() { if ( METAMASK_BUILD_TYPE === 'mmi' || - (flags.circleci && !flags.sentry?.doNotForceSentryForThisTest) + (flags.circleci && flags.sentry.forceEnable) ) { return true; } @@ -302,7 +302,7 @@ async function getMetaMetricsEnabled() { function setSentryClient() { const clientOptions = getClientOptions(); - const { dsn, environment, release } = clientOptions; + const { dsn, environment, release, tracesSampleRate } = clientOptions; /** * Sentry throws on initialization as it wants to avoid polluting the global namespace and @@ -322,6 +322,7 @@ function setSentryClient() { environment, dsn, release, + tracesSampleRate, }); Sentry.registerSpanErrorInstrumentation(); diff --git a/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts b/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts deleted file mode 100644 index 98f231607dba..000000000000 --- a/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SnapId } from '@metamask/snaps-sdk'; -import { Sender } from '@metamask/keyring-api'; -import { HandlerType } from '@metamask/snaps-utils'; -import { Json, JsonRpcRequest } from '@metamask/utils'; -// This dependency is still installed as part of the `package.json`, however -// the Snap is being pre-installed only for Flask build (for the moment). -import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { handleSnapRequest } from '../../../../ui/store/actions'; - -export const BITCOIN_WALLET_SNAP_ID: SnapId = - BitcoinWalletSnap.snapId as SnapId; - -export const BITCOIN_WALLET_NAME: string = - BitcoinWalletSnap.manifest.proposedName; - -export class BitcoinWalletSnapSender implements Sender { - send = async (request: JsonRpcRequest): Promise => { - // We assume the caller of this module is aware of this. If we try to use this module - // without having the pre-installed Snap, this will likely throw an error in - // the `handleSnapRequest` action. - return (await handleSnapRequest({ - origin: 'metamask', - snapId: BITCOIN_WALLET_SNAP_ID, - handler: HandlerType.OnKeyringRequest, - request, - })) as Json; - }; -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 31dc5fef3fcd..cd899c57e179 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -232,6 +232,8 @@ import { getCurrentChainId } from '../../ui/selectors'; // eslint-disable-next-line import/no-restricted-paths import { getProviderConfig } from '../../ui/ducks/metamask/metamask'; import { endTrace, trace } from '../../shared/lib/trace'; +// eslint-disable-next-line import/no-restricted-paths +import { isSnapId } from '../../ui/helpers/utils/snaps'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -4035,6 +4037,10 @@ export default class MetamaskController extends EventEmitter { userStorageController.syncInternalAccountsWithUserStorage.bind( userStorageController, ), + deleteAccountSyncingDataFromUserStorage: + userStorageController.performDeleteStorageAllFeatureEntries.bind( + userStorageController, + ), // NotificationServicesController checkAccountsPresence: @@ -4946,6 +4952,7 @@ export default class MetamaskController extends EventEmitter { transactionParams, transactionOptions, dappRequest, + ...otherParams }) { return { internalAccounts: this.accountsController.listAccounts(), @@ -4965,6 +4972,7 @@ export default class MetamaskController extends EventEmitter { securityAlertsEnabled: this.preferencesController.store.getState()?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), + ...otherParams, }; } @@ -5739,7 +5747,7 @@ export default class MetamaskController extends EventEmitter { { origin }, { eth_accounts: {}, - ...(process.env.CHAIN_PERMISSIONS && { + ...(!isSnapId(origin) && { [PermissionNames.permittedChains]: {}, }), }, @@ -5774,10 +5782,12 @@ export default class MetamaskController extends EventEmitter { this.permissionController.requestPermissions( { origin }, { - ...(process.env.CHAIN_PERMISSIONS && - requestedPermissions[RestrictedMethods.eth_accounts] && { - [PermissionNames.permittedChains]: {}, - }), + ...(requestedPermissions[PermissionNames.eth_accounts] && { + [PermissionNames.permittedChains]: {}, + }), + ...(requestedPermissions[PermissionNames.permittedChains] && { + [PermissionNames.eth_accounts]: {}, + }), ...requestedPermissions, }, ), @@ -5813,8 +5823,6 @@ export default class MetamaskController extends EventEmitter { return undefined; }, - getChainPermissionsFeatureFlag: () => - Boolean(process.env.CHAIN_PERMISSIONS), // network configuration-related setActiveNetwork: async (networkClientId) => { await this.networkController.setActiveNetwork(networkClientId); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index d1da34c48e0e..bab66d9bc515 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -517,6 +517,51 @@ describe('MetaMaskController', () => { }); }); + describe('#getAddTransactionRequest', () => { + it('formats the transaction for submission', () => { + const transactionParams = { from: '0xa', to: '0xb' }; + const transactionOptions = { foo: true }; + const result = metamaskController.getAddTransactionRequest({ + transactionParams, + transactionOptions, + }); + expect(result).toStrictEqual({ + internalAccounts: + metamaskController.accountsController.listAccounts(), + dappRequest: undefined, + networkClientId: + metamaskController.networkController.state.selectedNetworkClientId, + selectedAccount: + metamaskController.accountsController.getAccountByAddress( + transactionParams.from, + ), + transactionController: expect.any(Object), + transactionOptions, + transactionParams, + userOperationController: expect.any(Object), + chainId: '0x1', + ppomController: expect.any(Object), + securityAlertsEnabled: expect.any(Boolean), + updateSecurityAlertResponse: expect.any(Function), + }); + }); + it('passes through any additional params to the object', () => { + const transactionParams = { from: '0xa', to: '0xb' }; + const transactionOptions = { foo: true }; + const result = metamaskController.getAddTransactionRequest({ + transactionParams, + transactionOptions, + test: '123', + }); + + expect(result).toMatchObject({ + transactionParams, + transactionOptions, + test: '123', + }); + }); + }); + describe('submitPassword', () => { it('removes any identities that do not correspond to known accounts.', async () => { const fakeAddress = '0xbad0'; 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/130.test.ts b/app/scripts/migrations/130.test.ts new file mode 100644 index 000000000000..94e00949c7a1 --- /dev/null +++ b/app/scripts/migrations/130.test.ts @@ -0,0 +1,91 @@ +import { migrate, version } from './130'; + +const oldVersion = 129; + +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 }); + }); + describe(`migration #${version}`, () => { + it('updates the preferences with a default tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: {}, + }, + }, + }; + const expectedData = { + PreferencesController: { + preferences: { + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + }, + }; + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + + it('does nothing if the preferences already has a tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: { + tokenSortConfig: { + key: 'fooKey', + order: 'foo', + sortCallback: 'fooCallback', + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing to other preferences if they exist without a tokenSortConfig', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + preferences: { + existingPreference: true, + }, + }, + }, + }; + + const expectedData = { + PreferencesController: { + preferences: { + existingPreference: true, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + }); +}); diff --git a/app/scripts/migrations/130.ts b/app/scripts/migrations/130.ts new file mode 100644 index 000000000000..ccf376ce1e7e --- /dev/null +++ b/app/scripts/migrations/130.ts @@ -0,0 +1,44 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; +export const version = 130; +/** + * This migration adds a tokenSortConfig to the user's preferences + * + * + * @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, 'PreferencesController') && + isObject(state.PreferencesController) && + hasProperty(state.PreferencesController, 'preferences') && + isObject(state.PreferencesController.preferences) && + !state.PreferencesController.preferences.tokenSortConfig + ) { + state.PreferencesController.preferences.tokenSortConfig = { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }; + } + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 296ff8077613..a72fd34c3c28 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -146,9 +146,11 @@ const migrations = [ require('./125'), require('./125.1'), require('./126'), + require('./126.1'), require('./127'), require('./128'), require('./129'), + require('./130'), ]; export default migrations; diff --git a/coverage.json b/coverage.json index f65ea343e9b3..9887e06e2db6 100644 --- a/coverage.json +++ b/coverage.json @@ -1 +1 @@ -{ "coverage": 0 } +{ "coverage": 71 } diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index c8c97ce1dd8a..ec02c2756185 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } @@ -2036,6 +2028,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index c8c97ce1dd8a..ec02c2756185 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } @@ -2036,6 +2028,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index c8c97ce1dd8a..ec02c2756185 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -698,8 +698,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -715,14 +715,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -730,7 +722,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } @@ -2036,6 +2028,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 7478c04ea3aa..7eaa06a954b0 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -790,8 +790,8 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/base-controller": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, @@ -807,14 +807,6 @@ "uuid": true } }, - "@metamask/assets-controllers>@metamask/base-controller": { - "globals": { - "setTimeout": true - }, - "packages": { - "immer": true - } - }, "@metamask/assets-controllers>@metamask/polling-controller": { "globals": { "clearTimeout": true, @@ -822,7 +814,7 @@ "setTimeout": true }, "packages": { - "@metamask/assets-controllers>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, "uuid": true } @@ -2128,6 +2120,7 @@ "Event": true, "Headers": true, "TextDecoder": true, + "TextEncoder": true, "URL": true, "URLSearchParams": true, "addEventListener": true, diff --git a/package.json b/package.json index cc317effc2ae..d3f7cf42a1dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.3.1", + "version": "12.4.0", "private": true, "repository": { "type": "git", @@ -302,7 +302,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "^37.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/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", @@ -344,7 +344,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", "@metamask/preinstalled-example-snap": "^0.1.0", - "@metamask/profile-sync-controller": "^0.9.4", + "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", @@ -367,9 +367,9 @@ "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch", "@segment/loosely-validate-event": "^2.0.0", - "@sentry/browser": "^8.19.0", - "@sentry/types": "^8.19.0", - "@sentry/utils": "^8.19.0", + "@sentry/browser": "^8.33.1", + "@sentry/types": "^8.33.1", + "@sentry/utils": "^8.33.1", "@swc/core": "1.4.11", "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", "@zxing/browser": "^0.1.4", @@ -378,6 +378,7 @@ "base32-encode": "^1.2.0", "base64-js": "^1.5.1", "bignumber.js": "^4.1.0", + "bitcoin-address-validation": "^2.2.3", "blo": "1.2.0", "bn.js": "^5.2.1", "bowser": "^2.11.0", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index d0f1cfb87cbe..8faf7c7bfb79 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -472,6 +472,10 @@ export enum MetaMetricsUserTrait { * Identified when the user selects a currency from settings */ CurrentCurrency = 'current_currency', + /** + * Identified when the user changes token sort order on asset-list + */ + TokenSortPreference = 'token_sort_preference', } /** @@ -630,6 +634,7 @@ export enum MetaMetricsEventName { TokenScreenOpened = 'Token Screen Opened', TokenAdded = 'Token Added', TokenRemoved = 'Token Removed', + TokenSortPreference = 'Token Sort Preference', NFTRemoved = 'NFT Removed', TokenDetected = 'Token Detected', TokenHidden = 'Token Hidden', diff --git a/shared/lib/accounts/bitcoin-wallet-snap.ts b/shared/lib/accounts/bitcoin-wallet-snap.ts new file mode 100644 index 000000000000..58f367b173e1 --- /dev/null +++ b/shared/lib/accounts/bitcoin-wallet-snap.ts @@ -0,0 +1,11 @@ +import { SnapId } from '@metamask/snaps-sdk'; +// This dependency is still installed as part of the `package.json`, however +// the Snap is being pre-installed only for Flask build (for the moment). +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; + +// export const BITCOIN_WALLET_SNAP_ID: SnapId = 'local:http://localhost:8080'; +export const BITCOIN_WALLET_SNAP_ID: SnapId = + BitcoinWalletSnap.snapId as SnapId; + +export const BITCOIN_WALLET_NAME: string = + BitcoinWalletSnap.manifest.proposedName; diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts index 6c59f506e721..3b982ff8aff3 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -1,22 +1,27 @@ import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; -const MAINNET_ADDRESSES = [ +const BTC_MAINNET_ADDRESSES = [ // P2WPKH 'bc1qwl8399fz829uqvqly9tcatgrgtwp3udnhxfq4k', // P2PKH '1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ', ]; -const TESTNET_ADDRESSES = [ +const BTC_TESTNET_ADDRESSES = [ // P2WPKH 'tb1q6rmsq3vlfdhjdhtkxlqtuhhlr6pmj09y6w43g8', ]; const ETH_ADDRESSES = ['0x6431726EEE67570BF6f0Cf892aE0a3988F03903F']; +const SOL_ADDRESSES = [ + '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV', + 'DpNXPNWvWoHaZ9P3WtfGCb2ZdLihW8VW1w1Ph4KDH9iG', +]; + describe('multichain', () => { // @ts-expect-error This is missing from the Mocha type definitions - it.each(MAINNET_ADDRESSES)( + it.each(BTC_MAINNET_ADDRESSES)( 'returns true if address is compatible with BTC mainnet: %s', (address: string) => { expect(isBtcMainnetAddress(address)).toBe(true); @@ -24,7 +29,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each([...TESTNET_ADDRESSES, ...ETH_ADDRESSES])( + it.each([...BTC_TESTNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( 'returns false if address is not compatible with BTC mainnet: %s', (address: string) => { expect(isBtcMainnetAddress(address)).toBe(false); @@ -32,7 +37,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each(TESTNET_ADDRESSES)( + it.each(BTC_TESTNET_ADDRESSES)( 'returns true if address is compatible with BTC testnet: %s', (address: string) => { expect(isBtcTestnetAddress(address)).toBe(true); @@ -40,7 +45,7 @@ describe('multichain', () => { ); // @ts-expect-error This is missing from the Mocha type definitions - it.each([...MAINNET_ADDRESSES, ...ETH_ADDRESSES])( + it.each([...BTC_MAINNET_ADDRESSES, ...ETH_ADDRESSES, ...SOL_ADDRESSES])( 'returns false if address is compatible with BTC testnet: %s', (address: string) => { expect(isBtcTestnetAddress(address)).toBe(false); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index fec52295eada..8ef03509541b 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,6 +1,4 @@ -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isEthAddress } from '../../app/scripts/lib/multichain/address'; +import { validate, Network } from 'bitcoin-address-validation'; /** * Returns whether an address is on the Bitcoin mainnet. @@ -14,10 +12,7 @@ import { isEthAddress } from '../../app/scripts/lib/multichain/address'; * @returns `true` if the address is on the Bitcoin mainnet, `false` otherwise. */ export function isBtcMainnetAddress(address: string): boolean { - return ( - !isEthAddress(address) && - (address.startsWith('bc1') || address.startsWith('1')) - ); + return validate(address, Network.mainnet); } /** @@ -29,5 +24,5 @@ export function isBtcMainnetAddress(address: string): boolean { * @returns `true` if the address is on the Bitcoin testnet, `false` otherwise. */ export function isBtcTestnetAddress(address: string): boolean { - return !isEthAddress(address) && !isBtcMainnetAddress(address); + return validate(address, Network.testnet); } diff --git a/shared/lib/trace.test.ts b/shared/lib/trace.test.ts index 5154a930b7f9..ff55ec0f2df0 100644 --- a/shared/lib/trace.test.ts +++ b/shared/lib/trace.test.ts @@ -1,4 +1,5 @@ import { + setMeasurement, Span, startSpan, startSpanManual, @@ -10,6 +11,7 @@ jest.mock('@sentry/browser', () => ({ withIsolationScope: jest.fn(), startSpan: jest.fn(), startSpanManual: jest.fn(), + setMeasurement: jest.fn(), })); const NAME_MOCK = TraceName.Transaction; @@ -32,7 +34,8 @@ describe('Trace', () => { const startSpanMock = jest.mocked(startSpan); const startSpanManualMock = jest.mocked(startSpanManual); const withIsolationScopeMock = jest.mocked(withIsolationScope); - const setTagsMock = jest.fn(); + const setMeasurementMock = jest.mocked(setMeasurement); + const setTagMock = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -41,13 +44,20 @@ describe('Trace', () => { startSpan: startSpanMock, startSpanManual: startSpanManualMock, withIsolationScope: withIsolationScopeMock, + setMeasurement: setMeasurementMock, }; startSpanMock.mockImplementation((_, fn) => fn({} as Span)); + startSpanManualMock.mockImplementation((_, fn) => + fn({} as Span, () => { + // Intentionally empty + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any withIsolationScopeMock.mockImplementation((fn: any) => - fn({ setTags: setTagsMock }), + fn({ setTag: setTagMock }), ); }); @@ -91,8 +101,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided', () => { @@ -117,8 +131,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided with custom start time', () => { @@ -145,8 +163,33 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); + }); + + it('supports no global Sentry object', () => { + globalThis.sentry = undefined; + + let callbackExecuted = false; + + trace( + { + name: NAME_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + startTime: 123, + }, + () => { + callbackExecuted = true; + }, + ); + + expect(callbackExecuted).toBe(true); }); }); @@ -242,5 +285,21 @@ describe('Trace', () => { expect(spanEndMock).toHaveBeenCalledTimes(0); }); + + it('supports no global Sentry object', () => { + globalThis.sentry = undefined; + + expect(() => { + trace({ + name: NAME_MOCK, + id: ID_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK, id: ID_MOCK }); + }).not.toThrow(); + }); }); }); diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 0c667a346235..5ca256371502 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -1,10 +1,13 @@ import * as Sentry from '@sentry/browser'; -import { Primitive, StartSpanOptions } from '@sentry/types'; +import { MeasurementUnit, StartSpanOptions } from '@sentry/types'; import { createModuleLogger } from '@metamask/utils'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; +/** + * The supported trace names. + */ export enum TraceName { BackgroundConnect = 'Background Connect', DeveloperTest = 'Developer Test', @@ -36,22 +39,71 @@ type PendingTrace = { startTime: number; }; +/** + * A context object to associate traces with each other and generate nested traces. + */ export type TraceContext = unknown; +/** + * A callback function that can be traced. + */ export type TraceCallback = (context?: TraceContext) => T; +/** + * A request to create a new trace. + */ export type TraceRequest = { + /** + * Custom data to associate with the trace. + */ data?: Record; + + /** + * A unique identifier when not tracing a callback. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * The parent context of the trace. + * If provided, the trace will be nested under the parent trace. + */ parentContext?: TraceContext; + + /** + * Override the start time of the trace. + */ startTime?: number; + + /** + * Custom tags to associate with the trace. + */ tags?: Record; }; +/** + * A request to end a pending trace. + */ export type EndTraceRequest = { + /** + * The unique identifier of the trace. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * Override the end time of the trace. + */ timestamp?: number; }; @@ -59,6 +111,16 @@ export function trace(request: TraceRequest, fn: TraceCallback): T; export function trace(request: TraceRequest): TraceContext; +/** + * Create a Sentry transaction to analyse the duration of a code flow. + * If a callback is provided, the transaction will be automatically ended when the callback completes. + * If the callback returns a promise, the transaction will be ended when the promise resolves or rejects. + * If no callback is provided, the transaction must be manually ended using `endTrace`. + * + * @param request - The data associated with the trace, such as the name and tags. + * @param fn - The optional callback to record the duration of. + * @returns The context of the trace, or the result of the callback if provided. + */ export function trace( request: TraceRequest, fn?: TraceCallback, @@ -70,6 +132,12 @@ export function trace( return traceCallback(request, fn); } +/** + * End a pending trace that was started without a callback. + * Does nothing if the pending trace cannot be found. + * + * @param request - The data necessary to identify and end the pending trace. + */ export function endTrace(request: EndTraceRequest) { const { name, timestamp } = request; const id = getTraceId(request); @@ -101,6 +169,10 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T { const start = Date.now(); let error: unknown; + if (span) { + initSpan(span, request); + } + return tryCatchMaybePromise( () => fn(span), (currentError) => { @@ -131,6 +203,10 @@ function startTrace(request: TraceRequest): TraceContext { span?.end(timestamp); }; + if (span) { + initSpan(span, request); + } + const pendingTrace = { end, request, startTime }; const key = getTraceKey(request); tracesByKey.set(key, pendingTrace); @@ -149,7 +225,7 @@ function startSpan( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, ) { - const { data: attributes, name, parentContext, startTime, tags } = request; + const { data: attributes, name, parentContext, startTime } = request; const parentSpan = (parentContext ?? null) as Sentry.Span | null; const spanOptions: StartSpanOptions = { @@ -161,8 +237,7 @@ function startSpan( }; return sentryWithIsolationScope((scope: Sentry.Scope) => { - scope.setTags(tags as Record); - + initScope(scope, request); return callback(spanOptions); }); } @@ -182,6 +257,40 @@ function getPerformanceTimestamp(): number { return performance.timeOrigin + performance.now(); } +/** + * Initialise the isolated Sentry scope created for each trace. + * Includes setting all non-numeric tags. + * + * @param scope - The Sentry scope to initialise. + * @param request - The trace request. + */ +function initScope(scope: Sentry.Scope, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value !== 'number') { + scope.setTag(key, value); + } + } +} + +/** + * Initialise the Sentry span created for each trace. + * Includes setting all numeric tags as measurements so they can be queried numerically in Sentry. + * + * @param _span - The Sentry span to initialise. + * @param request - The trace request. + */ +function initSpan(_span: Sentry.Span, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value === 'number') { + sentrySetMeasurement(key, value, 'none'); + } + } +} + function tryCatchMaybePromise( tryFn: () => T, catchFn: (error: unknown) => void, @@ -243,7 +352,7 @@ function sentryWithIsolationScope(callback: (scope: Sentry.Scope) => T): T { if (!actual) { const scope = { // eslint-disable-next-line no-empty-function - setTags: () => {}, + setTag: () => {}, } as unknown as Sentry.Scope; return callback(scope); @@ -251,3 +360,17 @@ function sentryWithIsolationScope(callback: (scope: Sentry.Scope) => T): T { return actual(callback); } + +function sentrySetMeasurement( + key: string, + value: number, + unit: MeasurementUnit, +) { + const actual = globalThis.sentry?.setMeasurement; + + if (!actual) { + return; + } + + actual(key, value, unit); +} diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 32a61c573500..654e915a1305 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -372,7 +372,12 @@ "showFiatInTestnets": false, "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false + "smartTransactionsOptInStatus": false, + "tokenSortConfig": { + "key": "tokenFiatAmount", + "order": "dsc", + "sortCallback": "stringNumeric" + } }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index 60e0ea378b75..eda4ef5fbf6f 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -13,7 +13,7 @@ import { regularDelayMs, } from '../helpers'; import { Driver } from '../webdriver/driver'; -import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; +import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; import { retry } from '../../../development/lib/retry'; /** @@ -201,16 +201,12 @@ export async function connectAccountToTestDapp(driver: Driver) { await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); + + await driver.switchToWindowWithUrl(DAPP_URL); } export async function disconnectFromTestDapp(driver: Driver) { @@ -225,7 +221,6 @@ export async function disconnectFromTestDapp(driver: Driver) { text: '127.0.0.1:8080', tag: 'p', }); - await driver.clickElement('[data-testid="account-list-item-menu-button"]'); await driver.clickElement({ text: 'Disconnect', tag: 'button' }); await driver.clickElement('[data-testid ="disconnect-all"]'); } diff --git a/test/e2e/accounts/create-snap-account.spec.ts b/test/e2e/accounts/create-snap-account.spec.ts deleted file mode 100644 index 2a35b4b4c805..000000000000 --- a/test/e2e/accounts/create-snap-account.spec.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { Suite } from 'mocha'; - -import FixtureBuilder from '../fixture-builder'; -import { defaultGanacheOptions, WINDOW_TITLES, withFixtures } from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { installSnapSimpleKeyring } from './common'; - -/** - * Starts the flow to create a Snap account, including unlocking the wallet, - * connecting to the test Snaps page, installing the Snap, and initiating the - * create account process on the dapp. The function ends with switching to the - * first confirmation in the extension. - * - * @param driver - The WebDriver instance used to control the browser. - * @returns A promise that resolves when the setup steps are complete. - */ -async function startCreateSnapAccountFlow(driver: Driver): Promise { - await installSnapSimpleKeyring(driver, false); - - // move back to the Snap window to test the create account flow - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // check the dapp connection status - await driver.waitForSelector({ - css: '#snapConnected', - text: 'Connected', - }); - - // create new account on dapp - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // Wait until dialog is opened before proceeding - await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); -} - -describe('Create Snap Account', function (this: Suite) { - it('create Snap account popup contains correct Snap name and snapId', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - await driver.findElement({ - css: '[data-testid="confirmation-submit-button"]', - text: 'Create', - }); - - await driver.findElement({ - css: '[data-testid="confirmation-cancel-button"]', - text: 'Cancel', - }); - - await driver.findElement({ - css: '[data-testid="create-snap-account-content-title"]', - text: 'Create account', - }); - }, - ); - }); - - it('create Snap account confirmation flow ends in approval success', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // click the create button on the confirmation modal - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // success screen should show account created with the snap suggested name - await driver.findElement({ - tag: 'h3', - text: 'Account created', - }); - await driver.findElement({ - css: '.multichain-account-list-item__account-name__button', - text: 'SSK Account', - }); - - // click the okay button - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should be created on the dapp - await driver.findElement({ - tag: 'p', - text: 'Successful request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should be created with the snap suggested name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); - - it('creates multiple Snap accounts with increasing numeric suffixes', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); - - const expectedNames = ['SSK Account', 'SSK Account 2', 'SSK Account 3']; - - for (const [index, expectedName] of expectedNames.entries()) { - // move to the dapp window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // create new account on dapp - if (index === 0) { - // Only click the div for the first snap account creation - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - } - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - // wait until dialog is opened before proceeding - await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); - - // click the create button on the confirmation modal - await driver.clickElement( - '[data-testid="confirmation-submit-button"]', - ); - - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // click the okay button on the success screen - await driver.clickElement( - '[data-testid="confirmation-submit-button"]', - ); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // verify the account is created with the expected name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: expectedName, - }); - } - }, - ); - }); - - it('create Snap account confirmation flow ends in approval success with custom name input', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // click the create button on the confirmation modal - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // Add a custom name to the account - const newAccountLabel = 'Custom name'; - await driver.fill('[placeholder="SSK Account"]', newAccountLabel); - // click the add account button on the naming modal - await driver.clickElement( - '[data-testid="submit-add-account-with-name"]', - ); - - // success screen should show account created with the custom name - await driver.findElement({ - tag: 'h3', - text: 'Account created', - }); - await driver.findElement({ - css: '.multichain-account-list-item__account-name__button', - text: newAccountLabel, - }); - - // click the okay button - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should be created on the dapp - await driver.findElement({ - tag: 'p', - text: 'Successful request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should be created with the custom name - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: newAccountLabel, - }); - }, - ); - }); - - it('create Snap account confirmation cancellation results in error in Snap', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // cancel account creation - await driver.clickElement('[data-testid="confirmation-cancel-button"]'); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should not be created in Snap - await driver.findElement({ - tag: 'p', - text: 'Error request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should not be created - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); - - it('cancelling naming Snap account results in account not created', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // start the create account flow and switch to dialog window - await startCreateSnapAccountFlow(driver); - - // confirm account creation - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // click the cancel button on the naming modal - await driver.clickElement( - '[data-testid="cancel-add-account-with-name"]', - ); - - // switch back to the test dapp/Snap window - await driver.waitAndSwitchToWindowWithTitle( - 2, - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // account should not be created in Snap - await driver.findElement({ - tag: 'p', - text: 'Error request', - }); - - // switch to extension full screen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // account should not be created - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: 'SSK Account', - }); - }, - ); - }); -}); diff --git a/test/e2e/api-specs/ConfirmationRejectionRule.ts b/test/e2e/api-specs/ConfirmationRejectionRule.ts index 503d0358c63c..3e37dcd07fd7 100644 --- a/test/e2e/api-specs/ConfirmationRejectionRule.ts +++ b/test/e2e/api-specs/ConfirmationRejectionRule.ts @@ -69,10 +69,24 @@ export class ConfirmationsRejectRule implements Rule { await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await this.driver.findClickableElements({ - text: 'Next', + text: 'Connect', tag: 'button', }); + const editButtons = await this.driver.findElements( + '[data-testid="edit"]', + ); + await editButtons[1].click(); + + await this.driver.clickElement({ + text: 'Localhost 8545', + tag: 'p', + }); + + await this.driver.clickElement( + '[data-testid="connect-more-chains-button"]', + ); + const screenshotTwo = await this.driver.driver.takeScreenshot(); call.attachments.push({ type: 'image', @@ -80,15 +94,26 @@ export class ConfirmationsRejectRule implements Rule { }); await this.driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', }); - await this.driver.clickElement({ - text: 'Confirm', - tag: 'button', + await switchToOrOpenDapp(this.driver); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: '0x539', // 1337 + }, + ], }); + await this.driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + await switchToOrOpenDapp(this.driver); } } catch (e) { diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 83b8b29a5e83..2c0dfe9a23cb 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -215,6 +215,11 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 415af23071e7..f1e9a7e5ae1d 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -77,6 +77,11 @@ function onboardingFixture() { showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index a6031a956a37..a4ac650f8f78 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -135,11 +135,10 @@ describe('Create BTC Account', function (this: Suite) { await driver.clickElement( '[data-testid="account-options-menu-button"]', ); - const lockButton = await driver.findClickableElement( - '[data-testid="global-menu-lock"]', - ); - assert.equal(await lockButton.getText(), 'Lock MetaMask'); - await lockButton.click(); + await driver.clickElement({ + css: '[data-testid="global-menu-lock"]', + text: 'Lock MetaMask', + }); await driver.clickElement({ text: 'Forgot password?', diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index cf337b84e8f5..643dcefa35ae 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -755,12 +755,19 @@ const connectToDapp = async (driver) => { }); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + const editButtons = await driver.findElements('[data-testid="edit"]'); + await editButtons[1].click(); + await driver.clickElement({ - text: 'Next', - tag: 'button', + text: 'Localhost 8545', + tag: 'p', }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); + await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); diff --git a/test/e2e/json-rpc/eth_accounts.spec.js b/test/e2e/json-rpc/eth_accounts.spec.ts similarity index 61% rename from test/e2e/json-rpc/eth_accounts.spec.js rename to test/e2e/json-rpc/eth_accounts.spec.ts index af3568a41208..149021d40a57 100644 --- a/test/e2e/json-rpc/eth_accounts.spec.js +++ b/test/e2e/json-rpc/eth_accounts.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; +import FixtureBuilder from '../fixture-builder'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; describe('eth_accounts', function () { it('executes a eth_accounts json rpc call', async function () { @@ -18,10 +17,16 @@ describe('eth_accounts', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_accounts await driver.openNewPage(`http://127.0.0.1:8080`); @@ -31,7 +36,7 @@ describe('eth_accounts', function () { method: 'eth_accounts', }); - const accounts = await driver.executeScript( + const accounts: string[] = await driver.executeScript( `return window.ethereum.request(${accountsRequest})`, ); diff --git a/test/e2e/json-rpc/eth_call.spec.js b/test/e2e/json-rpc/eth_call.spec.ts similarity index 62% rename from test/e2e/json-rpc/eth_call.spec.js rename to test/e2e/json-rpc/eth_call.spec.ts index 8b81bb2193b4..7ff1dd7489ff 100644 --- a/test/e2e/json-rpc/eth_call.spec.js +++ b/test/e2e/json-rpc/eth_call.spec.ts @@ -1,12 +1,12 @@ -const { strict: assert } = require('assert'); -const { keccak } = require('ethereumjs-util'); -const { - withFixtures, - unlockWallet, - defaultGanacheOptions, -} = require('../helpers'); -const { SMART_CONTRACTS } = require('../seeder/smart-contracts'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { keccak } from 'ethereumjs-util'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { Driver } from '../webdriver/driver'; +import FixtureBuilder from '../fixture-builder'; +import { Ganache } from '../seeder/ganache'; +import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry'; +import { SMART_CONTRACTS } from '../seeder/smart-contracts'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; describe('eth_call', function () { const smartContract = SMART_CONTRACTS.NFTS; @@ -19,11 +19,19 @@ describe('eth_call', function () { .build(), ganacheOptions: defaultGanacheOptions, smartContract, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver, _, contractRegistry }) => { + async ({ + driver, + ganacheServer, + contractRegistry, + }: { + driver: Driver; + ganacheServer?: Ganache; + contractRegistry: GanacheContractAddressRegistry; + }) => { const contract = contractRegistry.getContractAddress(smartContract); - await unlockWallet(driver); + await loginWithBalanceValidation(driver, ganacheServer); // eth_call await driver.openNewPage(`http://127.0.0.1:8080`); diff --git a/test/e2e/json-rpc/eth_chainId.spec.js b/test/e2e/json-rpc/eth_chainId.spec.js deleted file mode 100644 index ba604552db82..000000000000 --- a/test/e2e/json-rpc/eth_chainId.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - unlockWallet, - defaultGanacheOptions, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_chainId', function () { - it('returns the chain ID of the current network', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_chainId - await driver.openNewPage(`http://127.0.0.1:8080`); - const request = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - id: 0, - }); - const result = await driver.executeScript( - `return window.ethereum.request(${request})`, - ); - - assert.equal(result, '0x539'); - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_chainId.spec.ts b/test/e2e/json-rpc/eth_chainId.spec.ts new file mode 100644 index 000000000000..d4b8e4f1dbb6 --- /dev/null +++ b/test/e2e/json-rpc/eth_chainId.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; + +describe('eth_chainId', function () { + it('returns the chain ID of the current network', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_chainId + await driver.openNewPage(`http://127.0.0.1:8080`); + const request: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + id: 0, + }); + const result = (await driver.executeScript( + `return window.ethereum.request(${request})`, + )) as string; + + assert.equal(result, '0x539'); + }, + ); + }); +}); diff --git a/test/e2e/json-rpc/eth_coinbase.spec.js b/test/e2e/json-rpc/eth_coinbase.spec.ts similarity index 50% rename from test/e2e/json-rpc/eth_coinbase.spec.js rename to test/e2e/json-rpc/eth_coinbase.spec.ts index 06fc25335572..216a3e7eedeb 100644 --- a/test/e2e/json-rpc/eth_coinbase.spec.js +++ b/test/e2e/json-rpc/eth_coinbase.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - withFixtures, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_coinbase', function () { it('executes a eth_coinbase json rpc call', async function () { @@ -15,20 +14,26 @@ describe('eth_coinbase', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.title, + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_coinbase await driver.openNewPage(`http://127.0.0.1:8080`); - const coinbaseRequest = JSON.stringify({ + const coinbaseRequest: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_coinbase', }); - const coinbase = await driver.executeScript( + const coinbase: string = await driver.executeScript( `return window.ethereum.request(${coinbaseRequest})`, ); diff --git a/test/e2e/json-rpc/eth_estimateGas.spec.js b/test/e2e/json-rpc/eth_estimateGas.spec.ts similarity index 53% rename from test/e2e/json-rpc/eth_estimateGas.spec.js rename to test/e2e/json-rpc/eth_estimateGas.spec.ts index 9ef594e1254b..11e0cb2379cb 100644 --- a/test/e2e/json-rpc/eth_estimateGas.spec.js +++ b/test/e2e/json-rpc/eth_estimateGas.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_estimateGas', function () { it('executes a estimate gas json rpc call', async function () { @@ -15,15 +14,21 @@ describe('eth_estimateGas', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_estimateGas await driver.openNewPage(`http://127.0.0.1:8080`); - const estimateGas = JSON.stringify({ + const estimateGas: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_estimateGas', params: [ @@ -34,9 +39,9 @@ describe('eth_estimateGas', function () { ], }); - const estimateGasRequest = await driver.executeScript( + const estimateGasRequest: string = (await driver.executeScript( `return window.ethereum.request(${estimateGas})`, - ); + )) as string; assert.strictEqual(estimateGasRequest, '0x5208'); }, diff --git a/test/e2e/json-rpc/eth_gasPrice.spec.js b/test/e2e/json-rpc/eth_gasPrice.spec.js deleted file mode 100644 index a3c2ef76f19b..000000000000 --- a/test/e2e/json-rpc/eth_gasPrice.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_gasPrice', function () { - it('executes gas price json rpc call', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_gasPrice - await driver.openNewPage(`http://127.0.0.1:8080`); - - const gasPriceRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_gasPrice', - }); - - const gasPrice = await driver.executeScript( - `return window.ethereum.request(${gasPriceRequest})`, - ); - - assert.strictEqual(gasPrice, '0x77359400'); // 2000000000 - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_gasPrice.spec.ts b/test/e2e/json-rpc/eth_gasPrice.spec.ts new file mode 100644 index 000000000000..d9c75c29fed9 --- /dev/null +++ b/test/e2e/json-rpc/eth_gasPrice.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; + +describe('eth_gasPrice', function () { + it('executes gas price json rpc call', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_gasPrice + await driver.openNewPage(`http://127.0.0.1:8080`); + + const gasPriceRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_gasPrice', + }); + + const gasPrice: string = await driver.executeScript( + `return window.ethereum.request(${gasPriceRequest})`, + ); + + assert.strictEqual(gasPrice, '0x77359400'); // 2000000000 + }, + ); + }); +}); diff --git a/test/e2e/json-rpc/eth_newBlockFilter.spec.js b/test/e2e/json-rpc/eth_newBlockFilter.spec.ts similarity index 62% rename from test/e2e/json-rpc/eth_newBlockFilter.spec.js rename to test/e2e/json-rpc/eth_newBlockFilter.spec.ts index 1b1091f82efa..a20f0fce23c0 100644 --- a/test/e2e/json-rpc/eth_newBlockFilter.spec.js +++ b/test/e2e/json-rpc/eth_newBlockFilter.spec.ts @@ -1,13 +1,12 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_newBlockFilter', function () { - const ganacheOptions = { + const ganacheOptions: typeof defaultGanacheOptions & { blockTime: number } = { blockTime: 0.1, ...defaultGanacheOptions, }; @@ -19,10 +18,16 @@ describe('eth_newBlockFilter', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_newBlockFilter await driver.openNewPage(`http://127.0.0.1:8080`); @@ -32,9 +37,9 @@ describe('eth_newBlockFilter', function () { method: 'eth_newBlockFilter', }); - const newBlockFilter = await driver.executeScript( + const newBlockFilter = (await driver.executeScript( `return window.ethereum.request(${newBlockfilterRequest})`, - ); + )) as string; assert.strictEqual(newBlockFilter, '0x01'); @@ -52,13 +57,13 @@ describe('eth_newBlockFilter', function () { method: 'eth_getBlockByNumber', params: ['latest', false], }); - const blockByHash = await driver.executeScript( + const blockByHash = (await driver.executeScript( `return window.ethereum.request(${blockByHashRequest})`, - ); + )) as { hash: string }; - const filterChanges = await driver.executeScript( + const filterChanges = (await driver.executeScript( `return window.ethereum.request(${getFilterChangesRequest})`, - ); + )) as string[]; assert.strictEqual(filterChanges.includes(blockByHash.hash), true); @@ -69,9 +74,9 @@ describe('eth_newBlockFilter', function () { params: ['0x01'], }); - const uninstallFilter = await driver.executeScript( + const uninstallFilter = (await driver.executeScript( `return window.ethereum.request(${uninstallFilterRequest})`, - ); + )) as boolean; assert.strictEqual(uninstallFilter, true); }, diff --git a/test/e2e/json-rpc/eth_requestAccounts.spec.js b/test/e2e/json-rpc/eth_requestAccounts.spec.ts similarity index 51% rename from test/e2e/json-rpc/eth_requestAccounts.spec.js rename to test/e2e/json-rpc/eth_requestAccounts.spec.ts index 2aa510522e2b..00c043ebac51 100644 --- a/test/e2e/json-rpc/eth_requestAccounts.spec.js +++ b/test/e2e/json-rpc/eth_requestAccounts.spec.ts @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - withFixtures, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; describe('eth_requestAccounts', function () { it('executes a request accounts json rpc call', async function () { @@ -15,20 +14,26 @@ describe('eth_requestAccounts', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.title, + title: this.test?.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); // eth_requestAccounts await driver.openNewPage(`http://127.0.0.1:8080`); - const requestAccountRequest = JSON.stringify({ + const requestAccountRequest: string = JSON.stringify({ jsonrpc: '2.0', method: 'eth_requestAccounts', }); - const requestAccount = await driver.executeScript( + const requestAccount: string[] = await driver.executeScript( `return window.ethereum.request(${requestAccountRequest})`, ); diff --git a/test/e2e/json-rpc/eth_subscribe.spec.js b/test/e2e/json-rpc/eth_subscribe.spec.js deleted file mode 100644 index 701913bb1867..000000000000 --- a/test/e2e/json-rpc/eth_subscribe.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -describe('eth_subscribe', function () { - it('executes a subscription event', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.title, - }, - async ({ driver }) => { - await unlockWallet(driver); - - // eth_subscribe - await driver.openNewPage(`http://127.0.0.1:8080`); - - const subscribeRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_subscribe', - params: ['newHeads'], - }); - - const subscribe = await driver.executeScript( - `return window.ethereum.request(${subscribeRequest})`, - ); - - const subscriptionMessage = await driver.executeAsyncScript( - `const callback = arguments[arguments.length - 1];` + - `window.ethereum.on('message', (message) => callback(message))`, - ); - - assert.strictEqual(subscribe, subscriptionMessage.data.subscription); - assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); - - // eth_unsubscribe - const unsubscribeRequest = JSON.stringify({ - jsonrpc: '2.0', - method: `eth_unsubscribe`, - params: [`${subscribe}`], - }); - - const unsubscribe = await driver.executeScript( - `return window.ethereum.request(${unsubscribeRequest})`, - ); - - assert.strictEqual(unsubscribe, true); - }, - ); - }); -}); diff --git a/test/e2e/json-rpc/eth_subscribe.spec.ts b/test/e2e/json-rpc/eth_subscribe.spec.ts new file mode 100644 index 000000000000..526bf1f3a761 --- /dev/null +++ b/test/e2e/json-rpc/eth_subscribe.spec.ts @@ -0,0 +1,72 @@ +import { strict as assert } from 'assert'; +import { defaultGanacheOptions, withFixtures } from '../helpers'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; +import { Ganache } from '../seeder/ganache'; + +describe('eth_subscribe', function () { + it('executes a subscription event', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer?: Ganache; + }) => { + await loginWithBalanceValidation(driver, ganacheServer); + + // eth_subscribe + await driver.openNewPage(`http://127.0.0.1:8080`); + + const subscribeRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_subscribe', + params: ['newHeads'], + }); + + const subscribe: string = (await driver.executeScript( + `return window.ethereum.request(${subscribeRequest})`, + )) as string; + + type SubscriptionMessage = { + data: { + subscription: string; + }; + type: string; + }; + + const subscriptionMessage: SubscriptionMessage = + (await driver.executeAsyncScript( + `const callback = arguments[arguments.length - 1]; + window.ethereum.on('message', (message) => callback(message))`, + )) as SubscriptionMessage; + + assert.strictEqual(subscribe, subscriptionMessage.data.subscription); + assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); + + // eth_unsubscribe + const unsubscribeRequest: string = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_unsubscribe', + params: [subscribe], + }); + + const unsubscribe: boolean = (await driver.executeScript( + `return window.ethereum.request(${unsubscribeRequest})`, + )) as boolean; + + assert.strictEqual(unsubscribe, true); + }, + ); + }); +}); diff --git a/test/e2e/json-rpc/switchEthereumChain.spec.js b/test/e2e/json-rpc/switchEthereumChain.spec.js index 75715b6ff00b..fba06db48131 100644 --- a/test/e2e/json-rpc/switchEthereumChain.spec.js +++ b/test/e2e/json-rpc/switchEthereumChain.spec.js @@ -7,6 +7,7 @@ const { DAPP_ONE_URL, unlockWallet, switchToNotificationWindow, + WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { isManifestV3 } = require('../../../shared/modules/mv3.utils'); @@ -17,7 +18,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -74,10 +74,10 @@ describe('Switch Ethereum Chain for two dapps', function () { // Confirm switchEthereumChain await switchToNotificationWindow(driver, 4); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Switch to Dapp One await driver.switchToWindow(dappOne); @@ -107,7 +107,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -145,24 +144,39 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps - const dappOne = await openDapp(driver, undefined, DAPP_URL); + await openDapp(driver, undefined, DAPP_URL); await openDapp(driver, undefined, DAPP_ONE_URL); + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + // Initiate send transaction on Dapp two await driver.clickElement('#sendButton'); - await driver.delay(2000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findClickableElements({ + text: 'Confirm', + tag: 'button', + }); + + // Switch to Dapp One + await driver.switchToWindowWithUrl(DAPP_URL); // Switch Ethereum chain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); - // Switch to Dapp One - await driver.switchToWindow(dappOne); - assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); - // Initiate switchEthereumChain on Dapp One await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, @@ -186,10 +200,10 @@ describe('Switch Ethereum Chain for two dapps', function () { await switchToNotificationWindow(driver, 4); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); }, ); }); @@ -199,7 +213,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -237,14 +250,43 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps + const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); - await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect Dapp One + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + // Switch and connect Dapp Two + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); // switchEthereumChain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -253,13 +295,13 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // Switch to notification of switchEthereumChain - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - // Switch to dapp one + // Switch back to dapp one await driver.switchToWindow(dappOne); assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); @@ -268,15 +310,16 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(2000); // Switch to notification that should still be switchEthereumChain request but with a warning. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement({ - span: 'span', - text: 'Switching networks will cancel all pending confirmations', - }); + // THIS IS BROKEN + // await driver.findElement({ + // span: 'span', + // text: 'Switching networks will cancel all pending confirmations', + // }); // Confirm switchEthereumChain with queued pending tx - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Window handles should only be expanded mm, dapp one, dapp 2, and the offscreen document // if this is an MV3 build(3 or 4 total) @@ -294,7 +337,6 @@ describe('Switch Ethereum Chain for two dapps', function () { { dapp: true, fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTwoTestDapps() .withNetworkControllerDoubleGanache() .build(), dappOptions: { numberOfDapps: 2 }, @@ -332,14 +374,42 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // open two dapps + const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); - await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect Dapp One + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + // Switch and connect Dapp Two + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindow(dappTwo); + assert.equal(await driver.getCurrentUrl(), `${DAPP_ONE_URL}/`); // switchEthereumChain request const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x53a' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -348,13 +418,13 @@ describe('Switch Ethereum Chain for two dapps', function () { ); // Switch to notification of switchEthereumChain - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - // Switch to dapp one + // Switch back to dapp one await driver.switchToWindow(dappOne); assert.equal(await driver.getCurrentUrl(), `${DAPP_URL}/`); @@ -363,12 +433,13 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(2000); // Switch to notification that should still be switchEthereumChain request but with an warning. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement({ - span: 'span', - text: 'Switching networks will cancel all pending confirmations', - }); + // THIS IS BROKEN + // await driver.findElement({ + // span: 'span', + // text: 'Switching networks will cancel all pending confirmations', + // }); // Cancel switchEthereumChain with queued pending tx await driver.clickElement({ text: 'Cancel', tag: 'button' }); @@ -377,7 +448,7 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.delay(1000); // Switch to new pending tx notification - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Sending ETH', tag: 'span', diff --git a/test/e2e/json-rpc/wallet_requestPermissions.spec.js b/test/e2e/json-rpc/wallet_requestPermissions.spec.js index 917e30ca12fc..5484fdf73d80 100644 --- a/test/e2e/json-rpc/wallet_requestPermissions.spec.js +++ b/test/e2e/json-rpc/wallet_requestPermissions.spec.js @@ -38,12 +38,7 @@ describe('wallet_requestPermissions', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); diff --git a/test/e2e/page-objects/flows/login.flow.ts b/test/e2e/page-objects/flows/login.flow.ts index 2904b1b9bd38..87239e3f19f1 100644 --- a/test/e2e/page-objects/flows/login.flow.ts +++ b/test/e2e/page-objects/flows/login.flow.ts @@ -40,5 +40,7 @@ export const loginWithBalanceValidation = async ( // Verify the expected balance on the homepage if (ganacheServer) { await new HomePage(driver).check_ganacheBalanceIsDisplayed(ganacheServer); + } else { + await new HomePage(driver).check_expectedBalanceIsDisplayed(); } }; diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 59845138c8a2..23c050f49526 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -109,8 +109,13 @@ class HomePage { ); } + /** + * Checks if the expected balance is displayed on homepage. + * + * @param expectedBalance - The expected balance to be displayed. Defaults to '0'. + */ async check_expectedBalanceIsDisplayed( - expectedBalance: string, + expectedBalance: string = '0', ): Promise { try { await this.driver.waitForSelector({ 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 8b20f1bc3bc0..fd4ae9d1ecc1 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -19,11 +19,17 @@ class SnapSimpleKeyringPage { tag: 'h3', }; + private readonly cancelAddAccountWithNameButton = + '[data-testid="cancel-add-account-with-name"]'; + private readonly confirmAddtoMetamask = { text: 'Confirm', tag: 'button', }; + private readonly confirmationCancelButton = + '[data-testid="confirmation-cancel-button"]'; + private readonly confirmationSubmitButton = '[data-testid="confirmation-submit-button"]'; @@ -54,6 +60,11 @@ class SnapSimpleKeyringPage { private readonly createSnapAccountName = '#account-name'; + private readonly errorRequestMessage = { + text: 'Error request', + tag: 'p', + }; + private readonly installationCompleteMessage = { text: 'Installation complete', tag: 'h2', @@ -95,19 +106,41 @@ class SnapSimpleKeyringPage { console.log('Snap Simple Keyring page is loaded'); } + async cancelCreateSnapOnConfirmationScreen(): Promise { + console.log('Cancel create snap on confirmation screen'); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationCancelButton, + ); + } + + async cancelCreateSnapOnFillNameScreen(): Promise { + console.log('Cancel create snap on fill name screen'); + await this.driver.clickElementAndWaitForWindowToClose( + this.cancelAddAccountWithNameButton, + ); + } + + async confirmCreateSnapOnConfirmationScreen(): Promise { + console.log('Confirm create snap on confirmation screen'); + await this.driver.clickElement(this.confirmationSubmitButton); + } + /** * Creates a new account on the Snap Simple Keyring page and checks the account is created. + * + * @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. */ - async createNewAccount(): Promise { + async createNewAccount( + accountName: string = 'SSK Account', + isFirstAccount: boolean = true, + ): Promise { console.log('Create new account on Snap Simple Keyring page'); - await this.driver.clickElement(this.createAccountSection); - await this.driver.clickElement(this.createAccountButton); - - await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await this.driver.waitForSelector(this.createAccountMessage); - await this.driver.clickElement(this.confirmationSubmitButton); + await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); + await this.confirmCreateSnapOnConfirmationScreen(); await this.driver.waitForSelector(this.createSnapAccountName); + await this.driver.fill(this.createSnapAccountName, accountName); await this.driver.clickElement(this.submitAddAccountWithNameButton); await this.driver.waitForSelector(this.accountCreatedMessage); @@ -146,6 +179,25 @@ class SnapSimpleKeyringPage { await this.check_simpleKeyringSnapConnected(); } + /** + * Opens the create snap account confirmation screen. + * + * @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true. + */ + async openCreateSnapAccountConfirmationScreen( + isFirstAccount: boolean = true, + ): Promise { + console.log('Open create snap account confirmation screen'); + if (isFirstAccount) { + await this.driver.clickElement(this.createAccountSection); + } + await this.driver.clickElement(this.createAccountButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.createAccountMessage); + await this.driver.waitForSelector(this.confirmationCancelButton); + } + async toggleUseSyncApproval() { console.log('Toggle Use Synchronous Approval'); await this.driver.clickElement(this.useSyncApprovalToggle); @@ -158,6 +210,13 @@ class SnapSimpleKeyringPage { await this.driver.waitForSelector(this.accountSupportedMethods); } + async check_errorRequestMessageDisplayed(): Promise { + console.log( + 'Check error request message is displayed on snap simple keyring page', + ); + await this.driver.waitForSelector(this.errorRequestMessage); + } + async check_simpleKeyringSnapConnected(): Promise { console.log('Check simple keyring snap is connected'); await this.driver.waitForSelector(this.snapConnectedMessage); diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index e8d02a12e2cd..290e8b863a9e 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,5 +1,6 @@ import { execSync } from 'child_process'; import fs from 'fs'; +import { merge } from 'lodash'; import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; export const folder = `dist/${process.env.SELENIUM_BROWSER}`; @@ -8,23 +9,82 @@ function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : undefined; } -// Grab the tracesSampleRate from the git message if it's set -function getTracesSampleRateFromGitMessage(): number | undefined { +/** + * Search a string for `flags = {...}` and return ManifestFlags if it exists + * + * @param str - The string to search + * @param errorType - The type of error to log if parsing fails + * @returns The ManifestFlags object if valid, otherwise undefined + */ +function regexSearchForFlags( + str: string, + errorType: string, +): ManifestFlags | undefined { + // Search str for `flags = {...}` + const flagsMatch = str.match(/flags\s*=\s*(\{.*\})/u); + + if (flagsMatch) { + try { + // Get 1st capturing group from regex + return JSON.parse(flagsMatch[1]); + } catch (error) { + console.error( + `Error parsing flags from ${errorType}, ignoring flags\n`, + error, + ); + } + } + + return undefined; +} + +/** + * Add flags from the GitHub PR body if they are set + * + * To use this feature, add a line to your PR body like: + * `flags = {"sentry": {"tracesSampleRate": 0.1}}` + * (must be valid JSON) + * + * @param flags - The flags object to add to + */ +function addFlagsFromPrBody(flags: ManifestFlags) { + let body; + + try { + body = fs.readFileSync('changed-files/pr-body.txt', 'utf8'); + } catch (error) { + console.debug('No pr-body.txt, ignoring flags'); + return; + } + + const newFlags = regexSearchForFlags(body, 'PR body'); + + if (newFlags) { + // Use lodash merge to do a deep merge (spread operator is shallow) + merge(flags, newFlags); + } +} + +/** + * Add flags from the Git message if they are set + * + * To use this feature, add a line to your commit message like: + * `flags = {"sentry": {"tracesSampleRate": 0.1}}` + * (must be valid JSON) + * + * @param flags - The flags object to add to + */ +function addFlagsFromGitMessage(flags: ManifestFlags) { const gitMessage = execSync( `git show --format='%B' --no-patch "HEAD"`, ).toString(); - // Search gitMessage for `[flags.sentry.tracesSampleRate: 0.000 to 1.000]` - const tracesSampleRateMatch = gitMessage.match( - /\[flags\.sentry\.tracesSampleRate: (0*(\.\d+)?|1(\.0*)?)\]/u, - ); + const newFlags = regexSearchForFlags(gitMessage, 'git message'); - if (tracesSampleRateMatch) { - // Return 1st capturing group from regex - return parseFloat(tracesSampleRateMatch[1]); + if (newFlags) { + // Use lodash merge to do a deep merge (spread operator is shallow) + merge(flags, newFlags); } - - return undefined; } // Alter the manifest with CircleCI environment variables and custom flags @@ -41,12 +101,15 @@ export function setManifestFlags(flags: ManifestFlags = {}) { ), }; - const tracesSampleRate = getTracesSampleRateFromGitMessage(); + addFlagsFromPrBody(flags); + addFlagsFromGitMessage(flags); - // 0 is a valid value, so must explicitly check for undefined - if (tracesSampleRate !== undefined) { - // Add tracesSampleRate to flags.sentry (which may or may not already exist) - flags.sentry = { ...flags.sentry, tracesSampleRate }; + // Set `flags.sentry.forceEnable` to true by default + if (flags.sentry === undefined) { + flags.sentry = {}; + } + if (flags.sentry.forceEnable === undefined) { + flags.sentry.forceEnable = true; } } diff --git a/test/e2e/snaps/test-snap-txinsights-v2.spec.js b/test/e2e/snaps/test-snap-txinsights-v2.spec.js index 0b43dca40ffc..5fb56687de96 100644 --- a/test/e2e/snaps/test-snap-txinsights-v2.spec.js +++ b/test/e2e/snaps/test-snap-txinsights-v2.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -37,22 +36,18 @@ describe('Test Snap TxInsights-v2', function () { await driver.clickElement('#connecttransaction-insights'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); - 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', }); @@ -62,17 +57,9 @@ describe('Test Snap TxInsights-v2', function () { await driver.clickElement('#getAccounts'); // switch back to MetaMask window and deal with dialogs - await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.waitForSelector({ - text: 'Confirm', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', }); @@ -82,7 +69,7 @@ describe('Test Snap TxInsights-v2', function () { // switch back to MetaMask window and switch to tx insights pane await driver.delay(2000); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findClickableElement({ text: 'Confirm', @@ -140,12 +127,6 @@ describe('Test Snap TxInsights-v2', function () { tag: 'button', text: 'Activity', }); - - // wait for transaction confirmation - await driver.waitForSelector({ - css: '.transaction-status-label', - text: 'Confirmed', - }); }, ); }); diff --git a/test/e2e/snaps/test-snap-txinsights.spec.js b/test/e2e/snaps/test-snap-txinsights.spec.js index ff93a2ea910b..7f6b7a3bec46 100644 --- a/test/e2e/snaps/test-snap-txinsights.spec.js +++ b/test/e2e/snaps/test-snap-txinsights.spec.js @@ -2,7 +2,6 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -37,22 +36,18 @@ describe('Test Snap TxInsights', function () { await driver.clickElement('#connecttransaction-insights'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); - 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', }); @@ -62,17 +57,9 @@ describe('Test Snap TxInsights', function () { await driver.clickElement('#getAccounts'); // switch back to MetaMask window and deal with dialogs - await switchToNotificationWindow(driver, 2); - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.waitForSelector({ - text: 'Confirm', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', }); @@ -82,11 +69,8 @@ describe('Test Snap TxInsights', function () { // switch back to MetaMask window and switch to tx insights pane await driver.delay(2000); - await switchToNotificationWindow(driver, 2); - await driver.waitForSelector({ - text: 'Insights Example Snap', - tag: 'button', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement({ text: 'Insights Example Snap', tag: 'button', diff --git a/test/e2e/tests/account/remove-account-snap.spec.ts b/test/e2e/tests/account/create-remove-account-snap.spec.ts similarity index 93% rename from test/e2e/tests/account/remove-account-snap.spec.ts rename to test/e2e/tests/account/create-remove-account-snap.spec.ts index 2f0e2ab96a33..5d8517f66b26 100644 --- a/test/e2e/tests/account/remove-account-snap.spec.ts +++ b/test/e2e/tests/account/create-remove-account-snap.spec.ts @@ -9,8 +9,8 @@ import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring- import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; -describe('Remove Account Snap @no-mmi', function (this: Suite) { - it('disable a snap and remove it', async function () { +describe('Create and remove Snap Account @no-mmi', function (this: Suite) { + it('create snap account and remove it by removing snap', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), diff --git a/test/e2e/tests/account/create-snap-account.spec.ts b/test/e2e/tests/account/create-snap-account.spec.ts new file mode 100644 index 000000000000..387b7149c53c --- /dev/null +++ b/test/e2e/tests/account/create-snap-account.spec.ts @@ -0,0 +1,140 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Create Snap Account @no-mmi', function (this: Suite) { + it('create Snap account with custom name input ends in approval success', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + const newCustomAccountLabel = 'Custom name'; + await snapSimpleKeyringPage.createNewAccount(newCustomAccountLabel); + + // Check snap account is displayed after adding the custom snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).check_accountLabel( + newCustomAccountLabel, + ); + }, + ); + }); + + it('creates multiple Snap accounts with increasing numeric suffixes', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const expectedNames = ['SSK Account', 'SSK Account 2', 'SSK Account 3']; + + // Create multiple snap accounts on snap simple keyring page + for (const expectedName of expectedNames) { + if (expectedName === 'SSK Account') { + await snapSimpleKeyringPage.createNewAccount(expectedName, true); + } else { + await snapSimpleKeyringPage.createNewAccount(expectedName, false); + } + } + + // Check 3 created snap accounts are displayed in the account list. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + for (const expectedName of expectedNames) { + await accountListPage.check_accountDisplayedInAccountList( + expectedName, + ); + } + }, + ); + }); + + it('create Snap account canceling on confirmation screen results in error on Snap', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // cancel snap account creation on confirmation screen + await snapSimpleKeyringPage.openCreateSnapAccountConfirmationScreen(); + await snapSimpleKeyringPage.cancelCreateSnapOnConfirmationScreen(); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await snapSimpleKeyringPage.check_errorRequestMessageDisplayed(); + + // Check snap account is not displayed in account list after canceling the creation + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); + + it('create Snap account canceling on fill name screen results in error on Snap', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + + // cancel snap account creation on fill name screen + await snapSimpleKeyringPage.openCreateSnapAccountConfirmationScreen(); + await snapSimpleKeyringPage.confirmCreateSnapOnConfirmationScreen(); + await snapSimpleKeyringPage.cancelCreateSnapOnFillNameScreen(); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await snapSimpleKeyringPage.check_errorRequestMessageDisplayed(); + + // Check snap account is not displayed in account list after canceling the creation + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new HeaderNavbar(driver).openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index 053c9f40f8b7..fc8a6d0ab240 100644 --- a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -50,11 +50,10 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SIWE_BadDomain); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const rejectionResult = await driver.waitForSelector({ diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index dca5e6ba27d5..418cc4ab513d 100644 --- a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts +++ b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts @@ -74,11 +74,10 @@ describe('Confirmation Signature - Personal Sign @no-mmi', function (this: Suite }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.PersonalSign); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const rejectionResult = await driver.waitForSelector({ diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index f2c62e617899..6961f0a5eaf2 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -56,7 +56,6 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertSignatureConfirmedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], @@ -81,10 +80,9 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: SignatureType.SignTypedDataV3, ); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, @@ -141,16 +139,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle('E2E Test Dapp'); await driver.clickElement('#signTypedDataV3Verify'); - await driver.delay(500); - - const verifyResult = await driver.findElement('#signTypedDataV3Result'); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataV3VerifyResult', - ); + await driver.waitForSelector({ + css: '#signTypedDataV3Result', + text: '0x0a22f7796a2a70c8dc918e7e6eb8452c8f2999d1a1eb5ad714473d36270a40d6724472e5609948c778a07216bd082b60b6f6853d6354c731fd8ccdd3a2f4af261b', + }); - assert.equal( - await verifyResult.getText(), - '0x0a22f7796a2a70c8dc918e7e6eb8452c8f2999d1a1eb5ad714473d36270a40d6724472e5609948c778a07216bd082b60b6f6853d6354c731fd8ccdd3a2f4af261b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); + await driver.waitForSelector({ + css: '#signTypedDataV3VerifyResult', + text: publicAddress, + }); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts index ca0dbb8f9bb6..33b94be6b332 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts @@ -50,7 +50,6 @@ describe('Confirmation Signature - Sign Typed Data V4 @no-mmi', function (this: await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertAccountDetailsMetrics( driver, @@ -87,10 +86,9 @@ describe('Confirmation Signature - Sign Typed Data V4 @no-mmi', function (this: SignatureType.SignTypedDataV4, ); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts index a9a9dfd52ae9..1017d44a00dc 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts @@ -76,10 +76,9 @@ describe('Confirmation Signature - Sign Typed Data @no-mmi', function (this: Sui }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SignTypedData); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.delay(1000); await assertSignatureRejectedMetrics({ driver, diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index d69a2f6a69ac..9b87e5b4e9cc 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -218,11 +218,10 @@ export async function clickHeaderInfoBtn(driver: Driver) { } export async function assertHeaderInfoBalance(driver: Driver) { - const headerBalanceEl = await driver.findElement( - '[data-testid="confirmation-account-details-modal__account-balance"]', - ); - await driver.waitForNonEmptyElement(headerBalanceEl); - assert.equal(await headerBalanceEl.getText(), `${WALLET_ETH_BALANCE}\nETH`); + await driver.waitForSelector({ + css: '[data-testid="confirmation-account-details-modal__account-balance"]', + text: `${WALLET_ETH_BALANCE} ETH`, + }); } export async function copyAddressAndPasteWalletAddress(driver: Driver) { diff --git a/test/e2e/tests/confirmations/signatures/siwe.spec.ts b/test/e2e/tests/confirmations/signatures/siwe.spec.ts index edc3a2020862..1dd545034731 100644 --- a/test/e2e/tests/confirmations/signatures/siwe.spec.ts +++ b/test/e2e/tests/confirmations/signatures/siwe.spec.ts @@ -47,7 +47,6 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); - await driver.delay(1000); await assertVerifiedSiweMessage( driver, @@ -77,18 +76,16 @@ describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { }: TestSuiteArguments) => { await openDappAndTriggerSignature(driver, SignatureType.SIWE); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement('#siweResult'); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: '#siweResult', + text: 'Error: User rejected the request.', + }); await assertSignatureRejectedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], @@ -119,6 +116,8 @@ async function assertVerifiedSiweMessage(driver: Driver, message: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const verifySigUtil = await driver.findElement('#siweResult'); - assert.equal(await verifySigUtil.getText(), message); + await driver.waitForSelector({ + css: '#siweResult', + text: message, + }); } diff --git a/test/e2e/tests/connections/connect-with-metamask.spec.js b/test/e2e/tests/connections/connect-with-metamask.spec.js new file mode 100644 index 000000000000..5611b40346db --- /dev/null +++ b/test/e2e/tests/connections/connect-with-metamask.spec.js @@ -0,0 +1,79 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + logInWithBalanceValidation, + defaultGanacheOptions, + openDapp, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Connections page', function () { + it('should render new connections flow', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await openDapp(driver); + // Connect to dapp + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // should render new connections page + const newConnectionPage = await driver.waitForSelector({ + tag: 'h2', + text: 'Connect with MetaMask', + }); + assert.ok(newConnectionPage, 'Connection Page is defined'); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( + '[data-testid ="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'All Permissions', tag: 'div' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Got it', + tag: 'button', + }); + await driver.clickElement({ + text: '127.0.0.1:8080', + tag: 'p', + }); + const connectionsPageAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok(connectionsPageAccountInfo, 'Connections Page is defined'); + const connectionsPageNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok(connectionsPageNetworkInfo, 'Connections Page is defined'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/edit-account-flow.spec.js b/test/e2e/tests/connections/edit-account-flow.spec.js new file mode 100644 index 000000000000..7b05f439714c --- /dev/null +++ b/test/e2e/tests/connections/edit-account-flow.spec.js @@ -0,0 +1,101 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + locateAccountBalanceDOM, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +const accountLabel2 = '2nd custom name'; +const accountLabel3 = '3rd custom name'; +describe('Edit Accounts Flow', function () { + it('should be able to edit accounts', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-add-account"]', + ); + await driver.fill('[placeholder="Account 2"]', accountLabel2); + await driver.clickElement({ text: 'Add account', tag: 'button' }); + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-add-account"]', + ); + await driver.fill('[placeholder="Account 3"]', accountLabel3); + await driver.clickElement({ text: 'Add account', tag: 'button' }); + await locateAccountBalanceDOM(driver); + await driver.clickElement( + '[data-testid ="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'All Permissions', tag: 'div' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Got it', + tag: 'button', + }); + await driver.clickElement({ + text: '127.0.0.1:8080', + tag: 'p', + }); + const connectionsPageAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok(connectionsPageAccountInfo, 'Connections Page is defined'); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Ensure there are edit buttons + assert.ok(editButtons.length > 0, 'Edit buttons are available'); + + // Click the first (0th) edit button + await editButtons[0].click(); + + await driver.clickElement({ + text: '2nd custom name', + tag: 'button', + }); + await driver.clickElement({ + text: '3rd custom name', + tag: 'button', + }); + await driver.clickElement( + '[data-testid="connect-more-accounts-button"]', + ); + const updatedAccountInfo = await driver.isElementPresent({ + text: '3 accounts connected', + tag: 'span', + }); + assert.ok(updatedAccountInfo, 'Accounts List Updated'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/edit-networks-flow.spec.js b/test/e2e/tests/connections/edit-networks-flow.spec.js new file mode 100644 index 000000000000..e14e1ae325d5 --- /dev/null +++ b/test/e2e/tests/connections/edit-networks-flow.spec.js @@ -0,0 +1,85 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + locateAccountBalanceDOM, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +async function switchToNetworkByName(driver, networkName) { + await driver.clickElement('.mm-picker-network'); + await driver.clickElement(`[data-testid="${networkName}"]`); +} + +describe('Edit Networks Flow', function () { + it('should be able to edit networks', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement('[data-testid="network-display"]'); + await driver.clickElement('.mm-modal-content__dialog .toggle-button'); + await driver.clickElement( + '.mm-modal-content__dialog button[aria-label="Close"]', + ); + + // Switch to first network, whose send transaction was just confirmed + await switchToNetworkByName(driver, 'Localhost 8545'); + await locateAccountBalanceDOM(driver); + await driver.clickElement( + '[data-testid ="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'All Permissions', tag: 'div' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Got it', + tag: 'button', + }); + await driver.clickElement({ + text: '127.0.0.1:8080', + tag: 'p', + }); + const editButtons = await driver.findElements('[data-testid="edit"]'); + + // Ensure there are edit buttons + assert.ok(editButtons.length > 0, 'Edit buttons are available'); + + // Click the first (0th) edit button + await editButtons[1].click(); + + // Disconnect Mainnet + await driver.clickElement({ + text: 'Ethereum Mainnet', + tag: 'p', + }); + + await driver.clickElement('[data-testid="connect-more-chains-button"]'); + const updatedNetworkInfo = await driver.isElementPresent({ + text: '2 networks connected', + tag: 'span', + }); + assert.ok(updatedNetworkInfo, 'Networks List Updated'); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/review-permissions-page.spec.js b/test/e2e/tests/connections/review-permissions-page.spec.js new file mode 100644 index 000000000000..d411a343b2c9 --- /dev/null +++ b/test/e2e/tests/connections/review-permissions-page.spec.js @@ -0,0 +1,145 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + WINDOW_TITLES, + connectToDapp, + logInWithBalanceValidation, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Review Permissions page', function () { + it('should show connections page', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( + '[data-testid ="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'All Permissions', tag: 'div' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Got it', + tag: 'button', + }); + await driver.clickElement({ + text: '127.0.0.1:8080', + tag: 'p', + }); + const reviewPermissionsAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok( + reviewPermissionsAccountInfo, + 'Review Permissions Page is defined', + ); + const reviewPermissionsNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok( + reviewPermissionsNetworkInfo, + 'Review Permissions Page is defined', + ); + }, + ); + }); + it('should disconnect when click on Disconnect button in connections page', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test.fullTitle(), + ganacheOptions: defaultGanacheOptions, + }, + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); + await connectToDapp(driver); + + // It should render connected status for button if dapp is connected + const getConnectedStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connected', + }); + assert.ok(getConnectedStatus, 'Account is connected to Dapp'); + + // Switch to extension Tab + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( + '[data-testid ="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'All Permissions', tag: 'div' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Got it', + tag: 'button', + }); + await driver.clickElement({ + text: '127.0.0.1:8080', + tag: 'p', + }); + const reviewPermissionsAccountInfo = await driver.isElementPresent({ + text: 'See your accounts and suggest transactions', + tag: 'p', + }); + assert.ok( + reviewPermissionsAccountInfo, + 'Accounts are defined for Review Permissions Page', + ); + const reviewPermissionsNetworkInfo = await driver.isElementPresent({ + text: 'Use your enabled networks', + tag: 'p', + }); + assert.ok( + reviewPermissionsNetworkInfo, + 'Networks are defined for Review Permissions Page', + ); + await driver.clickElement({ text: 'Disconnect', tag: 'button' }); + await driver.clickElement('[data-testid ="disconnect-all"]'); + const noAccountConnected = await driver.isElementPresent({ + text: 'MetaMask isn’t connected to this site', + tag: 'p', + }); + assert.ok( + noAccountConnected, + 'Account disconected from connections page', + ); + + // Switch back to Dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Button should show Connect text if dapp is not connected + + const getConnectStatus = await driver.waitForSelector({ + css: '#connectButton', + text: 'Connect', + }); + + assert.ok( + getConnectStatus, + 'Account is not connected to Dapp and button has text connect', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/review-switch-permission-page.spec.js b/test/e2e/tests/connections/review-switch-permission-page.spec.js new file mode 100644 index 000000000000..5fe3d6d19526 --- /dev/null +++ b/test/e2e/tests/connections/review-switch-permission-page.spec.js @@ -0,0 +1,154 @@ +const { strict: assert } = require('assert'); +const FixtureBuilder = require('../../fixture-builder'); +const { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + regularDelayMs, + WINDOW_TITLES, + defaultGanacheOptions, + switchToNotificationWindow, +} = require('../../helpers'); +const { PAGES } = require('../../webdriver/driver'); + +describe('Permissions Page when Dapp Switch to an enabled and non permissioned network', function () { + it('should switch to the chain when dapp tries to switch network to an enabled network after showing updated permissions page', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerDoubleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); + + // Open Dapp One + await openDapp(driver, undefined, DAPP_URL); + + await driver.delay(regularDelayMs); + + const chainIdRequest = JSON.stringify({ + method: 'eth_chainId', + }); + + const chainIdBeforeConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + assert.equal(chainIdBeforeConnect, '0x539'); // 1337 + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); + + // Switch to second network + await driver.clickElement({ + text: 'Ethereum Mainnet', + css: 'p', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdBeforeConnectAfterManualSwitch = + await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // before connecting the chainId should change with the wallet + assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); + + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await switchToNotificationWindow(driver); + + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // should still be on the same chainId as the wallet after connecting + assert.equal(chainIdAfterConnect, '0x1'); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); + + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await switchToNotificationWindow(driver); + await driver.findClickableElements({ + text: 'Confirm', + tag: 'button', + }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterDappSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // should be on the new chainId that was requested + assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); + + // Switch network + await driver.clickElement({ + text: 'Localhost 8546', + css: 'p', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterManualSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + assert.equal(chainIdAfterManualSwitch, '0x539'); + }, + ); + }); +}); diff --git a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js index bd2b4a6b1aef..b992925ffc7a 100644 --- a/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js +++ b/test/e2e/tests/dapp-interactions/dapp-interactions.spec.js @@ -65,8 +65,7 @@ describe('Dapp interactions', function () { navigate: false, }); - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.waitForSelector({ css: '#accounts', diff --git a/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js b/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js index df98799a462d..131ebdf4ee73 100644 --- a/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js +++ b/test/e2e/tests/dapp-interactions/dapp-tx-edit.spec.js @@ -1,10 +1,9 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + logInWithBalanceValidation, openDapp, - unlockWallet, WINDOW_TITLES, + withFixtures, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const FixtureBuilder = require('../../fixture-builder'); @@ -26,32 +25,22 @@ describe('Editing confirmations of dapp initiated contract interactions', functi const contractAddress = await contractRegistry.getContractAddress( smartContract, ); - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // deploy contract await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent await driver.findClickableElement('#deployButton'); await driver.clickElement('#depositButton'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - const editTransactionButton = await driver.isElementPresentAndVisible( + await driver.assertElementNotPresent( '[data-testid="confirm-page-back-edit-button"]', ); - assert.equal( - editTransactionButton, - false, - `Edit transaction button should not be visible on a contract interaction created by a dapp`, - ); }, ); }); @@ -68,29 +57,19 @@ describe('Editing confirmations of dapp initiated contract interactions', functi title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); await openDapp(driver); await driver.clickElement('#sendButton'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Sending ETH', }); - const editTransactionButton = await driver.isElementPresentAndVisible( + await driver.assertElementNotPresent( '[data-testid="confirm-page-back-edit-button"]', ); - assert.equal( - editTransactionButton, - false, - `Edit transaction button should not be visible on a simple send transaction created by a dapp`, - ); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js index fbf11b16cd40..296e36fe4bbe 100644 --- a/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js +++ b/test/e2e/tests/dapp-interactions/encrypt-decrypt.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -46,11 +45,10 @@ async function decryptMessage(driver) { async function verifyDecryptedMessageMM(driver, message) { await driver.clickElement({ text: 'Decrypt message', tag: 'div' }); - const notificationMessage = await driver.isElementPresent({ + await driver.waitForSelector({ text: message, tag: 'div', }); - assert.equal(notificationMessage, true); await driver.clickElement({ text: 'Decrypt', tag: 'button' }); } @@ -91,10 +89,10 @@ describe('Encrypt Decrypt', function () { await decryptMessage(driver); // Account balance is converted properly - const decryptAccountBalanceLabel = await driver.findElement( - '.request-decrypt-message__balance-value', - ); - assert.equal(await decryptAccountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-decrypt-message__balance-value', + text: '25 ETH', + }); // Verify message in MetaMask Notification await verifyDecryptedMessageMM(driver, message); @@ -187,10 +185,10 @@ describe('Encrypt Decrypt', function () { text: 'Request encryption public key', }); // Account balance is converted properly - const accountBalanceLabel = await driver.findElement( - '.request-encryption-public-key__balance-value', - ); - assert.equal(await accountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-encryption-public-key__balance-value', + text: '25 ETH', + }); }, ); }); @@ -230,10 +228,10 @@ describe('Encrypt Decrypt', function () { }); // Account balance is converted properly - const accountBalanceLabel = await driver.findElement( - '.request-encryption-public-key__balance-value', - ); - assert.equal(await accountBalanceLabel.getText(), '25 ETH'); + await driver.waitForSelector({ + css: '.request-encryption-public-key__balance-value', + text: '25 ETH', + }); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/failing-contract.spec.js b/test/e2e/tests/dapp-interactions/failing-contract.spec.js index f27768fb7e4c..5770adb1a3b9 100644 --- a/test/e2e/tests/dapp-interactions/failing-contract.spec.js +++ b/test/e2e/tests/dapp-interactions/failing-contract.spec.js @@ -46,11 +46,13 @@ describe('Failing contract interaction ', function () { // display warning when transaction is expected to fail const warningText = 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.'; - const warning = await driver.findElement('.mm-banner-alert .mm-text'); + await driver.waitForSelector({ + css: '.mm-banner-alert .mm-text', + text: warningText, + }); const confirmButton = await driver.findElement( '[data-testid="page-container-footer-next"]', ); - assert.equal(await warning.getText(), warningText); assert.equal(await confirmButton.isEnabled(), false); // dismiss warning and confirm the transaction @@ -113,11 +115,13 @@ describe('Failing contract interaction on non-EIP1559 network', function () { // display warning when transaction is expected to fail const warningText = 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.'; - const warning = await driver.findElement('.mm-banner-alert .mm-text'); + await driver.waitForSelector({ + css: '.mm-banner-alert .mm-text', + text: warningText, + }); const confirmButton = await driver.findElement( '[data-testid="page-container-footer-next"]', ); - assert.equal(await warning.getText(), warningText); assert.equal(await confirmButton.isEnabled(), false); // dismiss warning and confirm the transaction diff --git a/test/e2e/tests/dapp-interactions/permissions.spec.js b/test/e2e/tests/dapp-interactions/permissions.spec.js index 029a0a0661bc..adf3b809a656 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -36,11 +36,7 @@ describe('Permissions', function () { windowHandles, ); await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', + text: 'Connect', tag: 'button', }); diff --git a/test/e2e/tests/metrics/dapp-viewed.spec.js b/test/e2e/tests/metrics/dapp-viewed.spec.js index 78214685777e..668f93e65dc5 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -69,22 +69,6 @@ async function mockPermissionApprovedEndpoint(mockServer) { }); } -async function createTwoAccounts(driver) { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: '2nd account', - }); -} - const waitForDappConnected = async (driver) => { await driver.waitForSelector({ css: '#accounts', @@ -273,57 +257,6 @@ describe('Dapp viewed Event @no-mmi', function () { ); }); - it('is sent when connecting dapp with two accounts', async function () { - async function mockSegment(mockServer) { - return [await mockedDappViewedEndpointFirstVisit(mockServer)]; - } - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withMetaMetricsController({ - metaMetricsId: validFakeMetricsId, - participateInMetaMetrics: true, - }) - .build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - // create 2nd account - await createTwoAccounts(driver); - // Connect to dapp with two accounts - await openDapp(driver); - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement( - '[data-testid="choose-account-list-operate-all-check-box"]', - ); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - - const events = await getEventPayloads(driver, mockedEndpoints); - const dappViewedEventProperties = events[0].properties; - assert.equal(dappViewedEventProperties.is_first_visit, true); - assert.equal(dappViewedEventProperties.number_of_accounts, 2); - assert.equal(dappViewedEventProperties.number_of_accounts_connected, 2); - }, - ); - }); - it('is sent when reconnect to a dapp that has been connected before', async function () { async function mockSegment(mockServer) { return [ @@ -372,28 +305,20 @@ describe('Dapp viewed Event @no-mmi', function () { text: '127.0.0.1:8080', tag: 'p', }); - await driver.clickElement( - '[data-testid ="account-list-item-menu-button"]', - ); await driver.clickElement({ text: 'Disconnect', tag: 'button', }); await driver.clickElement('[data-testid ="disconnect-all"]'); - await driver.clickElement('button[aria-label="Back"]'); - await driver.clickElement('button[aria-label="Back"]'); // validate dapp is not connected - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ - text: 'All Permissions', - tag: 'div', - }); - await driver.findElement({ - text: 'Nothing to see here', + const noAccountConnected = await driver.isElementPresent({ + text: 'MetaMask isn’t connected to this site', tag: 'p', }); + assert.ok( + noAccountConnected, + 'Account disconected from connections page', + ); // reconnect again await connectToDapp(driver); const events = await getEventPayloads(driver, mockedEndpoints); diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index fdeb4437d428..dfe77f758fcb 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -247,7 +247,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -278,7 +278,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -319,7 +319,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -365,7 +365,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -426,7 +426,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryInvariantMigrationError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -475,7 +475,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -521,7 +521,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -585,7 +585,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -621,7 +621,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -656,7 +656,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -702,7 +702,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -766,7 +766,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -810,7 +810,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -898,7 +898,7 @@ describe('Sentry errors', function () { ganacheOptions, title: this.test.fullTitle(), manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver }) => { diff --git a/test/e2e/tests/metrics/sessions.spec.ts b/test/e2e/tests/metrics/sessions.spec.ts index f1bdee4538fb..7c79e5510116 100644 --- a/test/e2e/tests/metrics/sessions.spec.ts +++ b/test/e2e/tests/metrics/sessions.spec.ts @@ -38,7 +38,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -60,7 +60,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { 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 8d8c8c1ae895..559e8a256d43 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 @@ -215,7 +215,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean", + "tokenSortConfig": "object", "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", 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 b1131ec4e7a2..2df9ee4e2f23 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 @@ -37,6 +37,7 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, 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 f40b2687316b..d22b69967027 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 @@ -80,11 +80,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -114,8 +114,9 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, 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 3c692fa59405..2dfd6ac6ef21 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 @@ -80,11 +80,11 @@ }, "NetworkController": { "selectedNetworkClientId": "string", + "networkConfigurations": "object", "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": "object", - "networkConfigurations": "object" + "providerConfig": "object" }, "OnboardingController": { "completedOnboarding": true, @@ -114,8 +114,9 @@ "smartTransactionsOptInStatus": false, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", "showMultiRpcModal": "boolean", "shouldShowAggregatedBalancePopover": "boolean" }, diff --git a/test/e2e/tests/metrics/traces.spec.ts b/test/e2e/tests/metrics/traces.spec.ts index 194f36ff73b0..9166281f90e5 100644 --- a/test/e2e/tests/metrics/traces.spec.ts +++ b/test/e2e/tests/metrics/traces.spec.ts @@ -51,7 +51,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -73,7 +73,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -95,7 +95,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -117,7 +117,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - sentry: { doNotForceSentryForThisTest: true }, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { diff --git a/test/e2e/tests/multichain/connection-page.spec.js b/test/e2e/tests/multichain/connection-page.spec.js deleted file mode 100644 index 122a83e718fa..000000000000 --- a/test/e2e/tests/multichain/connection-page.spec.js +++ /dev/null @@ -1,219 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - WINDOW_TITLES, - connectToDapp, - logInWithBalanceValidation, - locateAccountBalanceDOM, - defaultGanacheOptions, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); - -const accountLabel2 = '2nd custom name'; -const accountLabel3 = '3rd custom name'; - -describe('Connections page', function () { - it('should disconnect when click on Disconnect button in connections page', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - await connectToDapp(driver); - - // It should render connected status for button if dapp is connected - const getConnectedStatus = await driver.waitForSelector({ - css: '#connectButton', - text: 'Connected', - }); - assert.ok(getConnectedStatus, 'Account is connected to Dapp'); - - // Switch to extension Tab - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - await driver.clickElementAndWaitToDisappear({ - text: 'Got it', - tag: 'button', - }); - await driver.clickElement({ - text: '127.0.0.1:8080', - tag: 'p', - }); - await driver.clickElement('[data-testid ="connections-page"]'); - const connectionsPage = await driver.isElementPresent({ - text: '127.0.0.1:8080', - tag: 'span', - }); - assert.ok(connectionsPage, 'Connections Page is defined'); - await driver.clickElement( - '[data-testid ="account-list-item-menu-button"]', - ); - await driver.clickElement({ text: 'Disconnect', tag: 'button' }); - await driver.clickElement('[data-testid ="disconnect-all"]'); - await driver.clickElement('button[aria-label="Back"]'); - await driver.clickElement('button[aria-label="Back"]'); - // validate dapp is not connected - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - const noAccountConnected = await driver.isElementPresent({ - text: 'Nothing to see here', - tag: 'p', - }); - assert.ok( - noAccountConnected, - 'Account disconected from connections page', - ); - - // Switch back to Dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - // Button should show Connect text if dapp is not connected - - const getConnectStatus = await driver.waitForSelector({ - css: '#connectButton', - text: 'Connect', - }); - - assert.ok( - getConnectStatus, - 'Account is not connected to Dapp and button has text connect', - ); - }, - ); - }); - - it('should connect more accounts when already connected to a dapp', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - title: this.test.fullTitle(), - ganacheOptions: defaultGanacheOptions, - }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - await connectToDapp(driver); - - const account = await driver.findElement('#accounts'); - const accountAddress = await account.getText(); - - // Dapp should contain single connected account address - assert.strictEqual( - accountAddress, - '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - ); - // disconnect dapp in fullscreen view - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // Add two new accounts with custom label - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 2"]', accountLabel2); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-account"]', - ); - await driver.fill('[placeholder="Account 3"]', accountLabel3); - await driver.clickElement({ text: 'Add account', tag: 'button' }); - await locateAccountBalanceDOM(driver); - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - await driver.clickElementAndWaitToDisappear({ - text: 'Got it', - tag: 'button', - }); - await driver.clickElement({ - text: '127.0.0.1:8080', - tag: 'p', - }); - - // Connect only second account and keep third account unconnected - await driver.clickElement({ - text: 'Connect more accounts', - tag: 'button', - }); - await driver.clickElement({ - text: '2nd custom name', - tag: 'button', - }); - await driver.clickElement( - '[data-testid ="connect-more-accounts-button"]', - ); - const newAccountConnected = await driver.isElementPresent({ - text: '2nd custom name', - tag: 'button', - }); - - assert.ok(newAccountConnected, 'Connected More Account Successfully'); - // Switch back to Dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - // Find the span element that contains the account addresses - const accounts = await driver.findElement('#accounts'); - const accountAddresses = await accounts.getText(); - - // Dapp should contain both the connected account addresses - assert.strictEqual( - accountAddresses, - '0x09781764c08de8ca82e156bbf156a3ca217c7950,0x5cfe73b6021e818b776b421b1c4db2474086a7e1', - ); - }, - ); - }); - - // Skipped until issue where firefox connecting to dapp is resolved. - // it('shows that the account is connected to the dapp', async function () { - // await withFixtures( - // { - // dapp: true, - // fixtures: new FixtureBuilder().build(), - // title: this.test.fullTitle(), - // ganacheOptions: defaultGanacheOptions, - // }, - // async ({ driver, ganacheServer }) => { - // const ACCOUNT = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; - // const SHORTENED_ACCOUNT = shortenAddress(ACCOUNT); - // await logInWithBalanceValidation(driver, ganacheServer); - // await openDappConnectionsPage(driver); - // // Verify that there are no connected accounts - // await driver.assertElementNotPresent( - // '[data-testid="account-list-address"]', - // ); - - // await connectToDapp(driver); - // await openDappConnectionsPage(driver); - - // const account = await driver.findElement( - // '[data-testid="account-list-address"]', - // ); - // const accountAddress = await account.getText(); - - // // Dapp should contain single connected account address - // assert.strictEqual(accountAddress, SHORTENED_ACCOUNT); - // }, - // ); - // }); -}); diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index 70325cb5155b..dc8f38e1168c 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -369,13 +369,6 @@ describe('Custom network', function () { tag: 'button', text: 'Approve', }); - - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); }, ); }); diff --git a/test/e2e/tests/network/chain-interactions.spec.js b/test/e2e/tests/network/chain-interactions.spec.js index ba774ffecdb1..5b831ab1ba54 100644 --- a/test/e2e/tests/network/chain-interactions.spec.js +++ b/test/e2e/tests/network/chain-interactions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { generateGanacheOptions, withFixtures, @@ -14,53 +13,6 @@ describe('Chain Interactions', function () { const ganacheOptions = generateGanacheOptions({ concurrent: [{ port, chainId }], }); - it('should add the Ganache test chain and not switch the network', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await logInWithBalanceValidation(driver); - - // trigger add chain confirmation - await openDapp(driver); - await driver.clickElement('#addEthereumChain'); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - // verify chain details - const [networkName, networkUrl, chainIdElement] = - await driver.findElements('.definition-list dd'); - assert.equal(await networkName.getText(), `Localhost ${port}`); - assert.equal(await networkUrl.getText(), `http://127.0.0.1:${port}`); - assert.equal(await chainIdElement.getText(), chainId.toString()); - - // approve add chain, cancel switch chain - await driver.clickElement({ text: 'Approve', tag: 'button' }); - await driver.clickElement({ text: 'Cancel', tag: 'button' }); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // verify networks - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - - await driver.clickElement('[data-testid="network-display"]'); - const ganacheChain = await driver.findElements({ - text: `Localhost ${port}`, - tag: 'p', - }); - assert.ok(ganacheChain.length, 1); - }, - ); - }); it('should add the Ganache chain and switch the network', async function () { await withFixtures( @@ -81,7 +33,6 @@ describe('Chain Interactions', function () { // approve and switch chain await driver.clickElement({ text: 'Approve', tag: 'button' }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); // switch to extension await driver.switchToWindowWithTitle( diff --git a/test/e2e/tests/network/deprecated-networks.spec.js b/test/e2e/tests/network/deprecated-networks.spec.js index 29587f53afff..26c2388e4b51 100644 --- a/test/e2e/tests/network/deprecated-networks.spec.js +++ b/test/e2e/tests/network/deprecated-networks.spec.js @@ -92,13 +92,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = @@ -178,13 +171,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = @@ -264,13 +250,6 @@ describe('Deprecated networks', function () { text: 'Approve', }); - const switchNetworkBtn = await driver.findElement({ - tag: 'button', - text: 'Switch network', - }); - - await switchNetworkBtn.click(); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); const deprecationWarningText = 'This network is deprecated'; diff --git a/test/e2e/tests/network/switch-custom-network.spec.js b/test/e2e/tests/network/switch-custom-network.spec.js index 694a8f309f01..09dedc3a62da 100644 --- a/test/e2e/tests/network/switch-custom-network.spec.js +++ b/test/e2e/tests/network/switch-custom-network.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { withFixtures, @@ -30,9 +29,6 @@ describe('Switch ethereum chain', function () { async ({ driver }) => { await unlockWallet(driver); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await openDapp(driver); await driver.clickElement({ @@ -40,62 +36,21 @@ describe('Switch ethereum chain', function () { text: 'Add Localhost 8546', }); - await driver.waitUntilXWindowHandles(3); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Approve', }); - await driver.findElement({ - tag: 'h3', - text: 'Allow this site to switch the network?', - }); - - // Don't switch to network now, because we will click the 'Switch to Localhost 8546' button below - await driver.clickElement({ - tag: 'button', - text: 'Cancel', - }); - - await driver.waitUntilXWindowHandles(2); - - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - await driver.clickElement({ - tag: 'button', - text: 'Switch to Localhost 8546', - }); - - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, + WINDOW_TITLES.ExtensionInFullScreenView, ); - await driver.clickElement({ - tag: 'button', - text: 'Switch network', - }); - - await driver.waitUntilXWindowHandles(2); - - await driver.switchToWindow(extension); - - const currentNetworkName = await driver.findElement({ - tag: 'span', + await driver.findElement({ + css: '[data-testid="network-display"]', text: 'Localhost 8546', }); - - assert.ok( - Boolean(currentNetworkName), - 'Failed to switch to custom network', - ); }, ); }); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js index c2a86226d0c4..deb189404fa8 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-diff-network.spec.js @@ -6,11 +6,9 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -49,23 +47,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -89,23 +77,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp one send tx @@ -122,30 +100,29 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement( + await driver.waitForSelector( By.xpath("//div[normalize-space(.)='1 of 2']"), ); - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? + // TODO: Do we want to confirm here? + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findElement( + await driver.waitForSelector( By.xpath("//div[normalize-space(.)='1 of 2']"), ); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js index 994afd5b4f31..265b28d0f56d 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-extra-tx.spec.js @@ -1,16 +1,14 @@ -const { strict: assert } = require('assert'); +const { By } = require('selenium-webdriver'); const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, - openDapp, - unlockWallet, - DAPP_URL, DAPP_ONE_URL, - regularDelayMs, - WINDOW_TITLES, + DAPP_URL, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, + openDapp, + unlockWallet, + WINDOW_TITLES, + withFixtures, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -52,39 +50,35 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_URL); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, + // Ensure Dapp One is on Localhost 8546 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, ); - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); + // Should auto switch without prompt since already approved via connect - // Switch to second network - await driver.clickElement({ - text: 'Localhost 8546', - css: 'p', - }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // Wait for the first dapp's connect confirmation to disappear await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); @@ -92,79 +86,71 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp 1 send 2 tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - await driver.delay(largeDelayMs); + await driver.waitUntilXWindowHandles(4); // Dapp 2 send 2 tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); await driver.clickElement('#sendButton'); - + // We cannot wait for the dialog, since it is already opened from before await driver.delay(largeDelayMs); - // Dapp 1 send 1 tx + // Dapp 1 send 1 tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); + // We cannot switch directly, as the dialog is sometimes closed and re-opened + await driver.delay(largeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver, 4); - - let navigationElement = await driver.findElement( - '.confirm-page-container-navigation', + await driver.waitForSelector( + By.xpath("//div[normalize-space(.)='1 of 2']"), ); - let navigationText = await navigationElement.getText(); - - assert.equal(navigationText.includes('1 of 2'), true); - - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); - // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? + // TODO: Do we want to confirm here? + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); - // Wait for confirmations to close and transactions from the second dapp to open - // Large delays to wait for confirmation spam opening/closing bug. - await driver.delay(5000); + await driver.switchToWindowWithUrl(DAPP_URL); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + // We need a big delay to make sure dialog is not invalidated + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', + await driver.waitForSelector( + By.xpath("//div[normalize-space(.)='1 of 2']"), ); - navigationText = await navigationElement.getText(); - - assert.equal(navigationText.includes('1 of 2'), true); - // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', @@ -174,19 +160,17 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun // Reject All Transactions await driver.clickElement('.page-container__footer-secondary a'); - await driver.clickElement({ text: 'Reject all', tag: 'button' }); - - // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Reject all', + tag: 'button', + }); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); - - // Check correct network on confirm tx. - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Localhost 8545', - }); + // We need a big delay to make sure dialog is not invalidated + // TODO: find a better way to handle different dialog ids + await driver.delay(2000); + await driver.switchToWindowWithUrl(DAPP_URL); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); }, ); }); diff --git a/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js b/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js index d2d7cdf122c0..bd52558ec67f 100644 --- a/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js +++ b/test/e2e/tests/request-queuing/batch-txs-per-dapp-same-network.spec.js @@ -22,10 +22,10 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function { dapp: true, fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() + .withNetworkControllerTripleGanache() .withPreferencesControllerUseRequestQueueEnabled() .build(), - dappOptions: { numberOfDapps: 2 }, + dappOptions: { numberOfDapps: 3 }, ganacheOptions: { ...defaultGanacheOptions, concurrent: [ @@ -34,6 +34,11 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function chainId, ganacheOptions2: defaultGanacheOptions, }, + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, ], }, title: this.test.fullTitle(), @@ -57,17 +62,25 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_URL); + + let switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], }); + // Ensure Dapp One is on Localhost 7777 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + // Should auto switch without prompt since already approved via connect + await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); @@ -88,18 +101,26 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await switchToNotificationWindow(driver, 4); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); - // Dapp one send tx + // Ensure Dapp Two is on Localhost 8545 + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + // Should auto switch without prompt since already approved via connect + + // Dapp one send two tx await driver.switchToWindowWithUrl(DAPP_URL); await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); @@ -107,7 +128,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.delay(largeDelayMs); - // Dapp two send tx + // Dapp two send two tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); @@ -126,7 +147,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Localhost 8545', + text: 'Localhost 7777', }); // Reject All Transactions @@ -135,10 +156,11 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function await driver.clickElement({ text: 'Reject all', tag: 'button' }); // TODO: Do we want to confirm here? // Wait for confirmation to close - await driver.waitUntilXWindowHandles(3); + await driver.waitUntilXWindowHandles(4); // Wait for new confirmations queued from second dapp to open - await switchToNotificationWindow(driver, 4); + await driver.delay(largeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); navigationElement = await driver.findElement( '.confirm-page-container-navigation', @@ -151,7 +173,7 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function // Check correct network on confirm tx. await driver.findElement({ css: '[data-testid="network-display"]', - text: 'Localhost 8545', + text: 'Localhost 8546', }); }, ); diff --git a/test/e2e/tests/request-queuing/chainid-check.spec.js b/test/e2e/tests/request-queuing/chainid-check.spec.js index 850051d39c6a..1579a8ae5aa4 100644 --- a/test/e2e/tests/request-queuing/chainid-check.spec.js +++ b/test/e2e/tests/request-queuing/chainid-check.spec.js @@ -90,15 +90,8 @@ describe('Request Queueing chainId proxy sync', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -122,11 +115,11 @@ describe('Request Queueing chainId proxy sync', function () { await switchToNotificationWindow(driver); await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -240,23 +233,13 @@ describe('Request Queueing chainId proxy sync', function () { assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); // Connect to dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -267,6 +250,10 @@ describe('Request Queueing chainId proxy sync', function () { // should still be on the same chainId as the wallet after connecting assert.equal(chainIdAfterConnect, '0x1'); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x1', + }); const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', @@ -278,14 +265,13 @@ describe('Request Queueing chainId proxy sync', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); - await switchToNotificationWindow(driver); - await driver.findClickableElements({ - text: 'Switch network', + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const chainIdAfterDappSwitch = await driver.executeScript( @@ -295,6 +281,10 @@ describe('Request Queueing chainId proxy sync', function () { // should be on the new chainId that was requested assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); diff --git a/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js index 8f6bf4c616d0..d52d45701563 100644 --- a/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-send-dapp2-signTypedData.spec.js @@ -45,10 +45,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await unlockWallet(driver); await tempToggleSettingRedesignedConfirmations(driver); - // Open Dapp One + // Open and connect Dapp One await openDapp(driver, undefined, DAPP_URL); - // Connect to dapp await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); @@ -57,25 +56,14 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - // Open Dapp Two + // Open and connect to Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); - // Connect to dapp 2 await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); @@ -85,21 +73,35 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', + // Switch Dapp Two to Localhost 8546 + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + let switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x53a' }], }); + // Initiate switchEthereumChain on Dapp one + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); + + // Should auto switch without prompt since already approved via connect + + // Switch back to Dapp One await driver.switchToWindowWithUrl(DAPP_URL); // switch chain for Dapp One - const switchEthereumChainRequest = JSON.stringify({ + switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', params: [{ chainId: '0x3e8' }], @@ -109,11 +111,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.executeScript( `window.ethereum.request(${switchEthereumChainRequest})`, ); - - await driver.waitUntilXWindowHandles(4); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x3e8', + }); + // Should auto switch without prompt since already approved via connect await driver.switchToWindowWithUrl(DAPP_URL); @@ -143,7 +145,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { // Check correct network on the signTypedData confirmation. await driver.findElement({ css: '[data-testid="signature-request-network-display"]', - text: 'Localhost 8545', + text: 'Localhost 8546', }); }, ); diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js index cbfb2b23a9a7..53c763d8891f 100644 --- a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js @@ -49,20 +49,10 @@ describe('Request Queueing', function () { await switchToNotificationWindow(driver); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - // Wait for Connecting notification to close. - await driver.waitUntilXWindowHandles(2); - // Navigate to test dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js index a68884de4a4c..7a212533de4b 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-eth-request-accounts.spec.js @@ -89,15 +89,8 @@ describe('Request Queuing Dapp 1 Send Tx -> Dapp 2 Request Accounts Tx', functio await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ - text: 'Next', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); diff --git a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js index 567ddf0f619d..c330596c48f3 100644 --- a/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js +++ b/test/e2e/tests/request-queuing/dapp1-switch-dapp2-send.spec.js @@ -51,16 +51,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -86,16 +79,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -104,7 +90,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x3e8' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -114,8 +100,8 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -124,7 +110,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -207,16 +193,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -242,16 +221,9 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -260,7 +232,7 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { const switchEthereumChainRequest = JSON.stringify({ jsonrpc: '2.0', method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x3e8' }], + params: [{ chainId: '0x539' }], }); // Initiate switchEthereumChain on Dapp Two @@ -270,8 +242,8 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); await driver.switchToWindowWithUrl(DAPP_ONE_URL); diff --git a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js index 7821a005774d..d32e96e29571 100644 --- a/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js +++ b/test/e2e/tests/request-queuing/multi-dapp-sendTx-revokePermission.spec.js @@ -5,11 +5,8 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, - largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -48,23 +45,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -88,28 +75,21 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver, 4); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp 1 send tx await driver.switchToWindowWithUrl(DAPP_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x1', + }); await driver.clickElement('#sendButton'); await driver.waitUntilXWindowHandles(4); @@ -117,18 +97,31 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok // Dapp 2 send tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); - await driver.delay(largeDelayMs); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x53a', + }); await driver.clickElement('#sendButton'); + await driver.waitUntilXWindowHandles(4); // Dapp 1 revokePermissions await driver.switchToWindowWithUrl(DAPP_URL); - await driver.clickElement('#revokeAccountsPermission'); + await driver.findElement({ + css: '[id="chainId"]', + text: '0x1', + }); + await driver.assertElementNotPresent({ + css: '[id="chainId"]', + text: '0x53a', + }); // Confirmation will close then reopen - await driver.waitUntilXWindowHandles(3); + await driver.clickElement('#revokeAccountsPermission'); + // TODO: find a better way to handle different dialog ids + await driver.delay(3000); // Check correct network on confirm tx. - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="network-display"]', diff --git a/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js b/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js index 6eb0b9d14f85..38fe1d7204d2 100644 --- a/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js +++ b/test/e2e/tests/request-queuing/multiple-networks-dapps-txs.spec.js @@ -5,11 +5,9 @@ const { unlockWallet, DAPP_URL, DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, defaultGanacheOptions, largeDelayMs, - switchToNotificationWindow, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -48,23 +46,13 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await openDapp(driver, undefined, DAPP_URL); // Connect to dapp 1 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); await driver.switchToWindowWithTitle( @@ -80,31 +68,18 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu css: 'p', }); - // Wait for the first dapp's connect confirmation to disappear - await driver.waitUntilXWindowHandles(2); - // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. // Open Dapp Two await openDapp(driver, undefined, DAPP_ONE_URL); // Connect to dapp 2 - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); + await driver.clickElement({ text: 'Connect', tag: 'button' }); - await switchToNotificationWindow(driver, 4); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Confirm', + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Dapp one send tx @@ -112,7 +87,7 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await driver.delay(largeDelayMs); await driver.clickElement('#sendButton'); - await driver.delay(largeDelayMs); + await driver.waitUntilXWindowHandles(4); // Dapp two send tx await driver.switchToWindowWithUrl(DAPP_ONE_URL); @@ -128,14 +103,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu await driver.waitUntilXWindowHandles(4); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(largeDelayMs); - - // Find correct network on confirm tx - await driver.findElement({ - text: 'Localhost 8545', - tag: 'span', - }); - // Reject Transaction await driver.findClickableElement({ text: 'Reject', tag: 'button' }); await driver.clickElement( @@ -161,6 +128,11 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu // Click Unconfirmed Tx await driver.clickElement('.transaction-list-item--unconfirmed'); + await driver.assertElementNotPresent({ + tag: 'p', + text: 'Network switched to Localhost 8546', + }); + // Confirm Tx await driver.clickElement('[data-testid="page-container-footer-next"]'); diff --git a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js index a86229e2cdb1..df33600413e1 100644 --- a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js @@ -3,9 +3,7 @@ const { withFixtures, openDapp, unlockWallet, - DAPP_URL, WINDOW_TITLES, - switchToNotificationWindow, defaultGanacheOptions, } = require('../../helpers'); @@ -18,7 +16,6 @@ describe('Request Queuing SwitchChain -> SendTx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPermissionControllerConnectedToTestDapp() .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { @@ -37,14 +34,30 @@ describe('Request Queuing SwitchChain -> SendTx', function () { async ({ driver }) => { await unlockWallet(driver); - await openDapp(driver, undefined, DAPP_URL); + await openDapp(driver); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // Switch Ethereum Chain - await driver.findClickableElement('#switchEthereumChain'); - await driver.clickElement('#switchEthereumChain'); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - // Keep notification confirmation on screen - await driver.waitUntilXWindowHandles(3); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); // Navigate back to test dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -52,22 +65,23 @@ describe('Request Queuing SwitchChain -> SendTx', function () { // Dapp Send Button await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Persist Switch Ethereum Chain notifcation await driver.findClickableElements({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); + // THIS IS BROKEN // Find the cancel pending txs on the Switch Ethereum Chain notification. - await driver.findElement({ - text: 'Switching networks will cancel all pending confirmations', - tag: 'span', - }); + // await driver.findElement({ + // text: 'Switching networks will cancel all pending confirmations', + // tag: 'span', + // }); // Confirm Switch Network - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); // No confirmations, tx should be cleared await driver.waitUntilXWindowHandles(2); diff --git a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js index b84b76868303..308a9c36914b 100644 --- a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js @@ -8,6 +8,7 @@ const { withFixtures, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); +const { DAPP_URL } = require('../../constants'); describe('Request Queue SwitchChain -> WatchAsset', function () { const smartContract = SMART_CONTRACTS.HST; @@ -20,7 +21,6 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -42,17 +42,35 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { ); await logInWithBalanceValidation(driver, ganacheServer); - await openDapp(driver, contractAddress); + await openDapp(driver, contractAddress, DAPP_URL); + + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'Connect', + tag: 'button', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // Switch Ethereum Chain - await driver.clickElement('#switchEthereumChain'); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - await driver.waitUntilXWindowHandles(3); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ - text: 'Allow this site to switch the network?', - tag: 'h3', + text: 'Use your enabled networks', + tag: 'p', }); // Switch back to test dapp @@ -68,10 +86,10 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { // Confirm Switch Network await driver.findClickableElement({ - text: 'Switch network', + text: 'Confirm', tag: 'button', }); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); }, diff --git a/test/e2e/tests/request-queuing/ui.spec.js b/test/e2e/tests/request-queuing/ui.spec.js index 482b18e0e4f5..b857d4307d5b 100644 --- a/test/e2e/tests/request-queuing/ui.spec.js +++ b/test/e2e/tests/request-queuing/ui.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { Browser, until } = require('selenium-webdriver'); +const { Browser } = require('selenium-webdriver'); const { CHAIN_IDS } = require('../../../../shared/constants/network'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -16,6 +16,10 @@ const { DAPP_TWO_URL, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); +const { + PermissionNames, +} = require('../../../../app/scripts/controllers/permissions'); +const { CaveatTypes } = require('../../../../shared/constants/permissions'); // Window handle adjustments will need to be made for Non-MV3 Firefox // due to OffscreenDocument. Additionally Firefox continually bombs @@ -29,21 +33,12 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { await openDapp(driver, undefined, dappUrl); // Connect to the dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - + await driver.clickElement({ text: 'Connect', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', + text: 'Connect', tag: 'button', - css: '[data-testid="page-container-footer-next"]', }); // Switch back to the dapp @@ -52,6 +47,25 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { // Switch chains if necessary if (chainId) { await driver.delay(veryLargeDelayMs); + const getPermissionsRequest = JSON.stringify({ + method: 'wallet_getPermissions', + }); + const getPermissionsResult = await driver.executeScript( + `return window.ethereum.request(${getPermissionsRequest})`, + ); + + const permittedChains = + getPermissionsResult + ?.find( + (permission) => + permission.parentCapability === PermissionNames.permittedChains, + ) + ?.caveats.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + + const isAlreadyPermitted = permittedChains.includes(chainId); + const switchChainRequest = JSON.stringify({ method: 'wallet_switchEthereumChain', params: [{ chainId }], @@ -61,18 +75,20 @@ async function openDappAndSwitchChain(driver, dappUrl, chainId) { `window.ethereum.request(${switchChainRequest})`, ); - await driver.delay(veryLargeDelayMs); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + if (!isAlreadyPermitted) { + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.findClickableElement( - '[data-testid="confirmation-submit-button"]', - ); - await driver.clickElementAndWaitForWindowToClose( - '[data-testid="confirmation-submit-button"]', - ); + await driver.findClickableElement( + '[data-testid="page-container-footer-next"]', + ); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); - // Switch back to the dapp - await driver.switchToWindowWithUrl(dappUrl); + // Switch back to the dapp + await driver.switchToWindowWithUrl(dappUrl); + } } } @@ -183,7 +199,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -205,7 +220,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a'); @@ -249,7 +264,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -278,7 +292,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x53a'); @@ -377,7 +391,6 @@ describe('Request-queue UI changes', function () { preferences: { showTestNetworks: true }, }) .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -399,7 +412,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -451,7 +464,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), @@ -462,15 +474,13 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Ensure the dapp starts on the correct network - await driver.wait( - until.elementTextContains( - await driver.findElement('#chainId'), - '0x539', - ), - ); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); // Open the popup with shimmed activeTabOrigin await openPopupWithActiveTabOrigin(driver, DAPP_URL); @@ -482,12 +492,10 @@ describe('Request-queue UI changes', function () { await driver.switchToWindowWithUrl(DAPP_URL); // Check to make sure the dapp network changed - await driver.wait( - until.elementTextContains( - await driver.findElement('#chainId'), - '0x1', - ), - ); + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x1', + }); }, ); }); @@ -501,7 +509,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -521,7 +528,7 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open tab 2, switch to Ethereum Mainnet await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -554,7 +561,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -574,7 +580,7 @@ describe('Request-queue UI changes', function () { await unlockWallet(driver); // Open the first dapp which starts on chain '0x539 - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open tab 2, switch to Ethereum Mainnet await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -626,7 +632,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -652,7 +657,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); @@ -697,7 +702,6 @@ describe('Request-queue UI changes', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -722,7 +726,7 @@ describe('Request-queue UI changes', function () { await driver.navigate(PAGES.HOME); // Open the first dapp - await openDappAndSwitchChain(driver, DAPP_URL); + await openDappAndSwitchChain(driver, DAPP_URL, '0x539'); // Open the second dapp and switch chains await openDappAndSwitchChain(driver, DAPP_ONE_URL, '0x1'); diff --git a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js index 3c183b5a50a7..1c1baa17fb5a 100644 --- a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js @@ -94,8 +94,6 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { await switchToNotificationWindow(driver); - await driver.clickElement({ text: 'Switch network', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); /** diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index a13ef9caa2b5..535948ba1c9b 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -119,7 +119,7 @@ describe('Add existing token using search', function () { async ({ driver }) => { await unlockWallet(driver); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await driver.fill('input[placeholder="Search tokens"]', 'BAT'); await driver.clickElement({ text: 'BAT', diff --git a/test/e2e/tests/tokens/custom-token-add-approve.spec.js b/test/e2e/tests/tokens/custom-token-add-approve.spec.js index a9cf1829a808..7a59243da403 100644 --- a/test/e2e/tests/tokens/custom-token-add-approve.spec.js +++ b/test/e2e/tests/tokens/custom-token-add-approve.spec.js @@ -35,7 +35,7 @@ describe('Create token, approve token and approve token without gas', function ( ); await clickNestedButton(driver, 'Tokens'); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/custom-token-send-transfer.spec.js b/test/e2e/tests/tokens/custom-token-send-transfer.spec.js index de2aa2addcf8..40b1872011bd 100644 --- a/test/e2e/tests/tokens/custom-token-send-transfer.spec.js +++ b/test/e2e/tests/tokens/custom-token-send-transfer.spec.js @@ -136,6 +136,12 @@ describe('Transfer custom tokens @no-mmi', function () { text: '-1.5 TST', }); + // this selector helps prevent flakiness. it allows driver to wait until send transfer is "confirmed" + await driver.waitForSelector({ + text: 'Confirmed', + tag: 'div', + }); + // check token amount is correct after transaction await clickNestedButton(driver, 'Tokens'); const tokenAmount = await driver.findElement( @@ -192,6 +198,12 @@ describe('Transfer custom tokens @no-mmi', function () { text: 'Send TST', }); + // this selector helps prevent flakiness. it allows driver to wait until send transfer is "confirmed" + await driver.waitForSelector({ + text: 'Confirmed', + tag: 'div', + }); + // check token amount is correct after transaction await clickNestedButton(driver, 'Tokens'); const tokenAmount = await driver.findElement( diff --git a/test/e2e/tests/tokens/token-details.spec.ts b/test/e2e/tests/tokens/token-details.spec.ts index 349c273c721c..0d577ab20f19 100644 --- a/test/e2e/tests/tokens/token-details.spec.ts +++ b/test/e2e/tests/tokens/token-details.spec.ts @@ -27,7 +27,7 @@ describe('Token Details', function () { }; const importToken = async (driver: Driver) => { - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-list.spec.ts b/test/e2e/tests/tokens/token-list.spec.ts index 32b5ea85e3ae..bffef04c40dd 100644 --- a/test/e2e/tests/tokens/token-list.spec.ts +++ b/test/e2e/tests/tokens/token-list.spec.ts @@ -27,7 +27,7 @@ describe('Token List', function () { }; const importToken = async (driver: Driver) => { - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); await clickNestedButton(driver, 'Custom token'); await driver.fill( '[data-testid="import-tokens-modal-custom-address"]', diff --git a/test/e2e/tests/tokens/token-sort.spec.ts b/test/e2e/tests/tokens/token-sort.spec.ts new file mode 100644 index 000000000000..e0d335ee0fd6 --- /dev/null +++ b/test/e2e/tests/tokens/token-sort.spec.ts @@ -0,0 +1,111 @@ +import { strict as assert } from 'assert'; +import { Context } from 'mocha'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import FixtureBuilder from '../../fixture-builder'; +import { + clickNestedButton, + defaultGanacheOptions, + regularDelayMs, + unlockWallet, + withFixtures, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; + +describe('Token List', function () { + const chainId = CHAIN_IDS.MAINNET; + const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; + const symbol = 'ABC'; + + const fixtures = { + fixtures: new FixtureBuilder({ inputChainId: chainId }).build(), + ganacheOptions: { + ...defaultGanacheOptions, + chainId: parseInt(chainId, 16), + }, + }; + + const importToken = async (driver: Driver) => { + await driver.clickElement({ text: 'Import', tag: 'button' }); + await clickNestedButton(driver, 'Custom token'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + tokenAddress, + ); + await driver.waitForSelector('p.mm-box--color-error-default'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-symbol"]', + symbol, + ); + await driver.delay(2000); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); + await driver.findElement({ text: 'Token imported', tag: 'h6' }); + }; + + it('should sort alphabetically and by decreasing balance', async function () { + await withFixtures( + { + ...fixtures, + title: (this as Context).test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await importToken(driver); + + const tokenListBeforeSorting = await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + const tokenSymbolsBeforeSorting = await Promise.all( + tokenListBeforeSorting.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + + assert.ok(tokenSymbolsBeforeSorting[0].includes('Ethereum')); + + await await driver.clickElement( + '[data-testid="sort-by-popover-toggle"]', + ); + await await driver.clickElement('[data-testid="sortByAlphabetically"]'); + + await driver.delay(regularDelayMs); + const tokenListAfterSortingAlphabetically = await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + const tokenListSymbolsAfterSortingAlphabetically = await Promise.all( + tokenListAfterSortingAlphabetically.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + + assert.ok( + tokenListSymbolsAfterSortingAlphabetically[0].includes('ABC'), + ); + + await await driver.clickElement( + '[data-testid="sort-by-popover-toggle"]', + ); + await await driver.clickElement( + '[data-testid="sortByDecliningBalance"]', + ); + + await driver.delay(regularDelayMs); + const tokenListBeforeSortingByDecliningBalance = + await driver.findElements( + '[data-testid="multichain-token-list-button"]', + ); + + const tokenListAfterSortingByDecliningBalance = await Promise.all( + tokenListBeforeSortingByDecliningBalance.map(async (tokenElement) => { + return tokenElement.getText(); + }), + ); + assert.ok( + tokenListAfterSortingByDecliningBalance[0].includes('Ethereum'), + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/transaction/change-assets.spec.js b/test/e2e/tests/transaction/change-assets.spec.js index 11bb7489a829..7ce971fd8d80 100644 --- a/test/e2e/tests/transaction/change-assets.spec.js +++ b/test/e2e/tests/transaction/change-assets.spec.js @@ -342,7 +342,7 @@ describe('Change assets', function () { // Make sure gas is updated by resetting amount and hex data // Note: this is needed until the race condition is fixed on the wallet level (issue #25243) - await driver.fill('[data-testid="currency-input"]', '2'); + await driver.fill('[data-testid="currency-input"]', '2.000042'); await hexDataLocator.fill('0x'); await hexDataLocator.fill(''); diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index a9f65cad0714..900c49731594 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -53,6 +53,8 @@ @import 'srp-input/srp-input'; @import 'snaps/snap-privacy-warning/index'; @import 'tab-bar/index'; +@import 'assets/asset-list/asset-list-control-bar/index'; +@import 'assets/asset-list/sort-control/index'; @import 'assets/token-cell/token-cell'; @import 'assets/token-list-display/token-list-display'; @import 'transaction-activity-log/index'; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx new file mode 100644 index 000000000000..696c3ca7c89f --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx @@ -0,0 +1,99 @@ +import React, { useRef, useState } from 'react'; +import { + Box, + ButtonBase, + ButtonBaseSize, + IconName, + Popover, + PopoverPosition, +} from '../../../../component-library'; +import SortControl from '../sort-control'; +import { + BackgroundColor, + BorderColor, + BorderStyle, + Display, + JustifyContent, + TextColor, +} from '../../../../../helpers/constants/design-system'; +import ImportControl from '../import-control'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../../../../app/scripts/lib/util'; +import { + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, +} from '../../../../../../shared/constants/app'; + +type AssetListControlBarProps = { + showTokensLinks?: boolean; +}; + +const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => { + const t = useI18nContext(); + const controlBarRef = useRef(null); // Create a ref + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const windowType = getEnvironmentType(); + const isFullScreen = + windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP; + + const handleOpenPopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + return ( + + + {t('sortBy')} + + + + + + + ); +}; + +export default AssetListControlBar; diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss new file mode 100644 index 000000000000..3ed7ae082766 --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss @@ -0,0 +1,8 @@ +.asset-list-control-bar { + padding-top: 8px; + padding-bottom: 8px; + + &__button:hover { + background-color: var(--color-background-hover); + } +} diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts b/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts new file mode 100644 index 000000000000..c9eff91c6fcf --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.ts @@ -0,0 +1 @@ +export { default } from './asset-list-control-bar'; diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index a84ec99037f9..5cfeb6803875 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,22 +1,15 @@ import React, { useContext, useState } from 'react'; import { useSelector } from 'react-redux'; import TokenList from '../token-list'; -import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; +import { PRIMARY } from '../../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - getShouldHideZeroBalanceTokens, getSelectedAccount, - getPreferences, } from '../../../../selectors'; import { - getMultichainCurrentNetwork, - getMultichainNativeCurrency, getMultichainIsEvm, - getMultichainShouldShowFiat, - getMultichainCurrencyImage, - getMultichainIsMainnet, getMultichainSelectedAccountCachedBalance, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMultichainIsBitcoin, @@ -32,14 +25,10 @@ import { import DetectedToken from '../../detected-token/detected-token'; import { DetectedTokensBanner, - TokenListItem, ImportTokenLink, ReceiveModal, } from '../../../multichain'; -import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; -import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { @@ -48,42 +37,30 @@ import { } from '../../../multichain/ramps-card/ramps-card'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF +import AssetListControlBar from './asset-list-control-bar'; +import NativeToken from './native-token'; export type TokenWithBalance = { address: string; symbol: string; - string: string; + string?: string; image: string; + secondary?: string; + tokenFiatAmount?: string; + isNative?: boolean; }; -type AssetListProps = { +export type AssetListProps = { onClickAsset: (arg: string) => void; - showTokensLinks: boolean; + showTokensLinks?: boolean; }; const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const nativeCurrency = useSelector(getMultichainNativeCurrency); - const showFiat = useSelector(getMultichainShouldShowFiat); - const isMainnet = useSelector(getMultichainIsMainnet); - const { showNativeTokenAsMainBalance } = useSelector(getPreferences); - const { chainId, ticker, type, rpcUrl } = useSelector( - getMultichainCurrentNetwork, - ); - const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( - chainId, - ticker, - type, - rpcUrl, - ); + const selectedAccount = useSelector(getSelectedAccount); const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const balance = useSelector(getMultichainSelectedAccountCachedBalance); - const balanceIsLoading = !balance; - const selectedAccount = useSelector(getSelectedAccount); - const shouldHideZeroBalanceTokens = useSelector( - getShouldHideZeroBalanceTokens, - ); const { currency: primaryCurrency, @@ -92,27 +69,12 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ethNumberOfDecimals: 4, shouldCheckShowNativeToken: true, }); - const { - currency: secondaryCurrency, - numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { - ethNumberOfDecimals: 4, - shouldCheckShowNativeToken: true, - }); - const [primaryCurrencyDisplay, primaryCurrencyProperties] = - useCurrencyDisplay(balance, { - numberOfDecimals: primaryNumberOfDecimals, - currency: primaryCurrency, - }); - - const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = - useCurrencyDisplay(balance, { - numberOfDecimals: secondaryNumberOfDecimals, - currency: secondaryCurrency, - }); + const [, primaryCurrencyProperties] = useCurrencyDisplay(balance, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); - const primaryTokenImage = useSelector(getMultichainCurrencyImage); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, @@ -126,23 +88,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { setShowReceiveModal(true); }; - const accountTotalFiatBalance = useAccountTotalFiatBalance( - selectedAccount, - shouldHideZeroBalanceTokens, - ); - - const tokensWithBalances = - accountTotalFiatBalance.tokensWithBalances as TokenWithBalance[]; - - const { loading } = accountTotalFiatBalance; - - tokensWithBalances.forEach((token) => { - token.string = roundToDecimalPlacesRemovingExtraZeroes( - token.string, - 5, - ) as string; - }); - const balanceIsZero = useSelector( getMultichainSelectedAccountCachedBalanceIsZero, ); @@ -150,6 +95,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; + const isBtc = useSelector(getMultichainIsBitcoin); ///: END:ONLY_INCLUDE_IF const isEvm = useSelector(getMultichainIsEvm); @@ -157,15 +103,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBtc = useSelector(getMultichainIsBitcoin); - ///: END:ONLY_INCLUDE_IF - - let isStakeable = isMainnet && isEvm; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - isStakeable = false; - ///: END:ONLY_INCLUDE_IF - return ( <> {detectedTokens.length > 0 && @@ -176,6 +113,21 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { margin={4} /> )} + + } + onTokenClick={(tokenAddress: string) => { + onClickAsset(tokenAddress); + trackEvent({ + event: MetaMetricsEventName.TokenScreenOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: primaryCurrencyProperties.suffix, + location: 'Home', + }, + }); + }} + /> { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) shouldShowBuy ? ( @@ -192,43 +144,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { ) : null ///: END:ONLY_INCLUDE_IF } - onClickAsset(nativeCurrency)} - title={nativeCurrency} - // The primary and secondary currencies are subject to change based on the user's settings - // TODO: rename this primary/secondary concept here to be more intuitive, regardless of setting - primary={isOriginalNativeSymbol ? secondaryCurrencyDisplay : undefined} - tokenSymbol={ - showNativeTokenAsMainBalance - ? primaryCurrencyProperties.suffix - : secondaryCurrencyProperties.suffix - } - secondary={ - showFiat && isOriginalNativeSymbol - ? primaryCurrencyDisplay - : undefined - } - tokenImage={balanceIsLoading ? null : primaryTokenImage} - isOriginalTokenSymbol={isOriginalNativeSymbol} - isNativeCurrency - isStakeable={isStakeable} - showPercentage - /> - { - onClickAsset(tokenAddress); - trackEvent({ - event: MetaMetricsEventName.TokenScreenOpened, - category: MetaMetricsEventCategory.Navigation, - properties: { - token_symbol: primaryCurrencyProperties.suffix, - location: 'Home', - }, - }); - }} - /> {shouldShowTokensLinks && ( { + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + const t = useI18nContext(); + const isEvm = useSelector(getMultichainIsEvm); + // NOTE: Since we can parametrize it now, we keep the original behavior + // for EVM assets + const shouldShowTokensLinks = showTokensLinks ?? isEvm; + + return ( + { + dispatch(showImportTokensModal()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.TokenImportButtonClicked, + properties: { + location: 'HOME', + }, + }); + }} + > + {t('import')} + + ); +}; + +export default AssetListControlBar; diff --git a/ui/components/app/assets/asset-list/import-control/index.ts b/ui/components/app/assets/asset-list/import-control/index.ts new file mode 100644 index 000000000000..b871f41ae8b4 --- /dev/null +++ b/ui/components/app/assets/asset-list/import-control/index.ts @@ -0,0 +1 @@ +export { default } from './import-control'; diff --git a/ui/components/app/assets/asset-list/native-token/index.ts b/ui/components/app/assets/asset-list/native-token/index.ts new file mode 100644 index 000000000000..6feb276bed54 --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/index.ts @@ -0,0 +1 @@ +export { default } from './native-token'; diff --git a/ui/components/app/assets/asset-list/native-token/native-token.tsx b/ui/components/app/assets/asset-list/native-token/native-token.tsx new file mode 100644 index 000000000000..cf0191b3de66 --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/native-token.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { + getMultichainCurrentNetwork, + getMultichainNativeCurrency, + getMultichainIsEvm, + getMultichainCurrencyImage, + getMultichainIsMainnet, + getMultichainSelectedAccountCachedBalance, +} from '../../../../../selectors/multichain'; +import { TokenListItem } from '../../../../multichain'; +import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { AssetListProps } from '../asset-list'; +import { useNativeTokenBalance } from './use-native-token-balance'; +// import { getPreferences } from '../../../../../selectors'; + +const NativeToken = ({ onClickAsset }: AssetListProps) => { + const nativeCurrency = useSelector(getMultichainNativeCurrency); + const isMainnet = useSelector(getMultichainIsMainnet); + const { chainId, ticker, type, rpcUrl } = useSelector( + getMultichainCurrentNetwork, + ); + const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + rpcUrl, + ); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + const balanceIsLoading = !balance; + + const { string, symbol, secondary } = useNativeTokenBalance(); + + const primaryTokenImage = useSelector(getMultichainCurrencyImage); + + const isEvm = useSelector(getMultichainIsEvm); + + let isStakeable = isMainnet && isEvm; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + isStakeable = false; + ///: END:ONLY_INCLUDE_IF + + return ( + onClickAsset(nativeCurrency)} + title={nativeCurrency} + primary={string} + tokenSymbol={symbol} + secondary={secondary} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + isOriginalTokenSymbol={isOriginalNativeSymbol} + isNativeCurrency + isStakeable={isStakeable} + showPercentage + /> + ); +}; + +export default NativeToken; diff --git a/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts b/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts new file mode 100644 index 000000000000..a14e65ac572b --- /dev/null +++ b/ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts @@ -0,0 +1,94 @@ +import currencyFormatter from 'currency-formatter'; +import { useSelector } from 'react-redux'; + +import { + getMultichainCurrencyImage, + getMultichainCurrentNetwork, + getMultichainSelectedAccountCachedBalance, + getMultichainShouldShowFiat, +} from '../../../../../selectors/multichain'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol'; +import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common'; +import { useUserPreferencedCurrency } from '../../../../../hooks/useUserPreferencedCurrency'; +import { useCurrencyDisplay } from '../../../../../hooks/useCurrencyDisplay'; +import { TokenWithBalance } from '../asset-list'; + +export const useNativeTokenBalance = () => { + const showFiat = useSelector(getMultichainShouldShowFiat); + const primaryTokenImage = useSelector(getMultichainCurrencyImage); + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); + const { chainId, ticker, type, rpcUrl } = useSelector( + getMultichainCurrentNetwork, + ); + const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + rpcUrl, + ); + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + const currentCurrency = useSelector(getCurrentCurrency); + const { + currency: primaryCurrency, + numberOfDecimals: primaryNumberOfDecimals, + } = useUserPreferencedCurrency(PRIMARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); + const { + currency: secondaryCurrency, + numberOfDecimals: secondaryNumberOfDecimals, + } = useUserPreferencedCurrency(SECONDARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); + + const [primaryCurrencyDisplay, primaryCurrencyProperties] = + useCurrencyDisplay(balance, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); + + const [secondaryCurrencyDisplay, secondaryCurrencyProperties] = + useCurrencyDisplay(balance, { + numberOfDecimals: secondaryNumberOfDecimals, + currency: secondaryCurrency, + }); + + const primaryBalance = isOriginalNativeSymbol + ? secondaryCurrencyDisplay + : undefined; + + const secondaryBalance = + showFiat && isOriginalNativeSymbol ? primaryCurrencyDisplay : undefined; + + const tokenSymbol = showNativeTokenAsMainBalance + ? primaryCurrencyProperties.suffix + : secondaryCurrencyProperties.suffix; + + const unformattedTokenFiatAmount = showNativeTokenAsMainBalance + ? secondaryCurrencyDisplay.toString() + : primaryCurrencyDisplay.toString(); + + // useCurrencyDisplay passes along the symbol and formatting into the value here + // for sorting we need the raw value, without the currency and it should be decimal + // this is the easiest way to do this without extensive refactoring of useCurrencyDisplay + const tokenFiatAmount = currencyFormatter + .unformat(unformattedTokenFiatAmount, { + code: currentCurrency.toUpperCase(), + }) + .toString(); + + const nativeTokenWithBalance: TokenWithBalance = { + address: '', + symbol: tokenSymbol ?? '', + string: primaryBalance, + image: primaryTokenImage, + secondary: secondaryBalance, + tokenFiatAmount, + isNative: true, + }; + + return nativeTokenWithBalance; +}; diff --git a/ui/components/app/assets/asset-list/sort-control/index.scss b/ui/components/app/assets/asset-list/sort-control/index.scss new file mode 100644 index 000000000000..76e61c1025ae --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/index.scss @@ -0,0 +1,27 @@ +.selectable-list-item-wrapper { + position: relative; +} + +.selectable-list-item { + cursor: pointer; + padding: 16px; + + &--selected { + background: var(--color-primary-muted); + } + + &:not(.selectable-list-item--selected) { + &:hover, + &:focus-within { + background: var(--color-background-default-hover); + } + } + + &__selected-indicator { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } +} diff --git a/ui/components/app/assets/asset-list/sort-control/index.ts b/ui/components/app/assets/asset-list/sort-control/index.ts new file mode 100644 index 000000000000..7e5ecace780f --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/index.ts @@ -0,0 +1 @@ +export { default } from './sort-control'; diff --git a/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx b/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx new file mode 100644 index 000000000000..4aac598bd838 --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.test.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { useSelector } from 'react-redux'; +import { setTokenSortConfig } from '../../../../../store/actions'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import SortControl from './sort-control'; + +// Mock the sortAssets utility +jest.mock('../../util/sort', () => ({ + sortAssets: jest.fn(() => []), // mock sorting implementation +})); + +// Mock the setTokenSortConfig action creator +jest.mock('../../../../../store/actions', () => ({ + setTokenSortConfig: jest.fn(), +})); + +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useSelector: jest.fn(), + useDispatch: () => mockDispatch, + }; +}); + +const mockHandleClose = jest.fn(); + +describe('SortControl', () => { + const mockTrackEvent = jest.fn(); + + const renderComponent = () => { + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === getPreferences) { + return { + key: 'tokenFiatAmount', + sortCallback: 'stringNumeric', + order: 'dsc', + }; + } + if (selector === getCurrentCurrency) { + return 'usd'; + } + return undefined; + }); + + return renderWithProvider( + + + , + ); + }; + + beforeEach(() => { + mockDispatch.mockClear(); + mockTrackEvent.mockClear(); + (setTokenSortConfig as jest.Mock).mockClear(); + }); + + it('renders correctly', () => { + renderComponent(); + + expect(screen.getByTestId('sortByAlphabetically')).toBeInTheDocument(); + expect(screen.getByTestId('sortByDecliningBalance')).toBeInTheDocument(); + }); + + it('dispatches setTokenSortConfig with expected config, and tracks event when Alphabetically is clicked', () => { + renderComponent(); + + const alphabeticallyButton = screen.getByTestId( + 'sortByAlphabetically__button', + ); + fireEvent.click(alphabeticallyButton); + + expect(mockDispatch).toHaveBeenCalled(); + expect(setTokenSortConfig).toHaveBeenCalledWith({ + key: 'symbol', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + expect(mockTrackEvent).toHaveBeenCalledWith({ + category: 'Settings', + event: 'Token Sort Preference', + properties: { + token_sort_preference: 'symbol', + }, + }); + }); + + it('dispatches setTokenSortConfig with expected config, and tracks event when Declining balance is clicked', () => { + renderComponent(); + + const decliningBalanceButton = screen.getByTestId( + 'sortByDecliningBalance__button', + ); + fireEvent.click(decliningBalanceButton); + + expect(mockDispatch).toHaveBeenCalled(); + expect(setTokenSortConfig).toHaveBeenCalledWith({ + key: 'tokenFiatAmount', + sortCallback: 'stringNumeric', + order: 'dsc', + }); + + expect(mockTrackEvent).toHaveBeenCalledWith({ + category: 'Settings', + event: 'Token Sort Preference', + properties: { + token_sort_preference: 'tokenFiatAmount', + }, + }); + }); +}); diff --git a/ui/components/app/assets/asset-list/sort-control/sort-control.tsx b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx new file mode 100644 index 000000000000..c45a5488f1a6 --- /dev/null +++ b/ui/components/app/assets/asset-list/sort-control/sort-control.tsx @@ -0,0 +1,116 @@ +import React, { ReactNode, useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import classnames from 'classnames'; +import { Box, Text } from '../../../../component-library'; +import { SortOrder, SortingCallbacksT } from '../../util/sort'; +import { + BackgroundColor, + BorderRadius, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { setTokenSortConfig } from '../../../../../store/actions'; +import { MetaMetricsContext } from '../../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsUserTrait, +} from '../../../../../../shared/constants/metametrics'; +import { getCurrentCurrency, getPreferences } from '../../../../../selectors'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { getCurrencySymbol } from '../../../../../helpers/utils/common.util'; + +// intentionally used generic naming convention for styled selectable list item +// inspired from ui/components/multichain/network-list-item +// should probably be broken out into component library +type SelectableListItemProps = { + isSelected: boolean; + onClick?: React.MouseEventHandler; + testId?: string; + children: ReactNode; +}; + +export const SelectableListItem = ({ + isSelected, + onClick, + testId, + children, +}: SelectableListItemProps) => { + return ( + + + + {children} + + + {isSelected && ( + + )} + + ); +}; + +type SortControlProps = { + handleClose: () => void; +}; + +const SortControl = ({ handleClose }: SortControlProps) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const { tokenSortConfig } = useSelector(getPreferences); + const currentCurrency = useSelector(getCurrentCurrency); + + const dispatch = useDispatch(); + + const handleSort = ( + key: string, + sortCallback: keyof SortingCallbacksT, + order: SortOrder, + ) => { + dispatch( + setTokenSortConfig({ + key, + sortCallback, + order, + }), + ); + trackEvent({ + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.TokenSortPreference, + properties: { + [MetaMetricsUserTrait.TokenSortPreference]: key, + }, + }); + handleClose(); + }; + return ( + <> + handleSort('symbol', 'alphaNumeric', 'asc')} + testId="sortByAlphabetically" + > + {t('sortByAlphabetically')} + + handleSort('tokenFiatAmount', 'stringNumeric', 'dsc')} + testId="sortByDecliningBalance" + > + {t('sortByDecliningBalance', [getCurrencySymbol(currentCurrency)])} + + + ); +}; + +export default SortControl; diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 2cd5cb84b8ab..5f5b43d6c098 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -10,7 +10,7 @@ import { getIntlLocale } from '../../../../ducks/locale/locale'; type TokenCellProps = { address: string; symbol: string; - string: string; + string?: string; image: string; onClick?: (arg: string) => void; }; diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 194ea2762191..8a107b154fb9 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { ReactNode, useMemo } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { Box } from '../../../component-library'; @@ -8,39 +9,87 @@ import { JustifyContent, } from '../../../../helpers/constants/design-system'; import { TokenWithBalance } from '../asset-list/asset-list'; +import { sortAssets } from '../util/sort'; +import { + getPreferences, + getSelectedAccount, + getShouldHideZeroBalanceTokens, + getTokenExchangeRates, +} from '../../../../selectors'; +import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; +import { getConversionRate } from '../../../../ducks/metamask/metamask'; +import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance'; type TokenListProps = { onTokenClick: (arg: string) => void; - tokens: TokenWithBalance[]; - loading: boolean; + nativeToken: ReactNode; }; export default function TokenList({ onTokenClick, - tokens, - loading = false, + nativeToken, }: TokenListProps) { const t = useI18nContext(); + const { tokenSortConfig } = useSelector(getPreferences); + const selectedAccount = useSelector(getSelectedAccount); + const conversionRate = useSelector(getConversionRate); + const nativeTokenWithBalance = useNativeTokenBalance(); + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const contractExchangeRates = useSelector( + getTokenExchangeRates, + shallowEqual, + ); + const { tokensWithBalances, loading } = useAccountTotalFiatBalance( + selectedAccount, + shouldHideZeroBalanceTokens, + ) as { + tokensWithBalances: TokenWithBalance[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mergedRates: any; + loading: boolean; + }; - if (loading) { - return ( - - {t('loadingTokens')} - + const sortedTokens = useMemo(() => { + return sortAssets( + [nativeTokenWithBalance, ...tokensWithBalances], + tokenSortConfig, ); - } + }, [ + tokensWithBalances, + tokenSortConfig, + conversionRate, + contractExchangeRates, + ]); - return ( + return loading ? ( + + {t('loadingTokens')} + + ) : (
- {tokens.map((tokenData, index) => ( - - ))} + {sortedTokens.map((tokenData) => { + if (tokenData?.isNative) { + // we need cloneElement so that we can pass the unique key + return React.cloneElement(nativeToken as React.ReactElement, { + key: `${tokenData.symbol}-${tokenData.address}`, + }); + } + return ( + + ); + })}
); } diff --git a/ui/components/app/assets/util/sort.test.ts b/ui/components/app/assets/util/sort.test.ts new file mode 100644 index 000000000000..f4a99e31b641 --- /dev/null +++ b/ui/components/app/assets/util/sort.test.ts @@ -0,0 +1,263 @@ +import { sortAssets } from './sort'; + +type MockAsset = { + name: string; + balance: string; + createdAt: Date; + profile: { + id: string; + info?: { + category?: string; + }; + }; +}; + +const mockAssets: MockAsset[] = [ + { + name: 'Asset Z', + balance: '500', + createdAt: new Date('2023-01-01'), + profile: { id: '1', info: { category: 'gold' } }, + }, + { + name: 'Asset A', + balance: '600', + createdAt: new Date('2022-05-15'), + profile: { id: '4', info: { category: 'silver' } }, + }, + { + name: 'Asset B', + balance: '400', + createdAt: new Date('2021-07-20'), + profile: { id: '2', info: { category: 'bronze' } }, + }, +]; + +// Define the sorting tests +describe('sortAssets function - nested value handling with dates and numeric sorting', () => { + test('sorts by name in ascending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'name', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + expect(sortedById[0].name).toBe('Asset A'); + expect(sortedById[sortedById.length - 1].name).toBe('Asset Z'); + }); + + test('sorts by balance in ascending order (stringNumeric)', () => { + const sortedById = sortAssets(mockAssets, { + key: 'balance', + sortCallback: 'stringNumeric', + order: 'asc', + }); + + expect(sortedById[0].balance).toBe('400'); + expect(sortedById[sortedById.length - 1].balance).toBe('600'); + }); + + test('sorts by balance in ascending order (numeric)', () => { + const sortedById = sortAssets(mockAssets, { + key: 'balance', + sortCallback: 'numeric', + order: 'asc', + }); + + expect(sortedById[0].balance).toBe('400'); + expect(sortedById[sortedById.length - 1].balance).toBe('600'); + }); + + test('sorts by profile.id in ascending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'profile.id', + sortCallback: 'stringNumeric', + order: 'asc', + }); + + expect(sortedById[0].profile.id).toBe('1'); + expect(sortedById[sortedById.length - 1].profile.id).toBe('4'); + }); + + test('sorts by profile.id in descending order', () => { + const sortedById = sortAssets(mockAssets, { + key: 'profile.id', + sortCallback: 'stringNumeric', + order: 'dsc', + }); + + expect(sortedById[0].profile.id).toBe('4'); + expect(sortedById[sortedById.length - 1].profile.id).toBe('1'); + }); + + test('sorts by deeply nested profile.info.category in ascending order', () => { + const sortedByCategory = sortAssets(mockAssets, { + key: 'profile.info.category', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + // Expecting the assets with defined categories to be sorted first + expect(sortedByCategory[0].profile.info?.category).toBe('bronze'); + expect( + sortedByCategory[sortedByCategory.length - 1].profile.info?.category, + ).toBe('silver'); + }); + + test('sorts by createdAt (date) in ascending order', () => { + const sortedByDate = sortAssets(mockAssets, { + key: 'createdAt', + sortCallback: 'date', + order: 'asc', + }); + + expect(sortedByDate[0].createdAt).toEqual(new Date('2021-07-20')); + expect(sortedByDate[sortedByDate.length - 1].createdAt).toEqual( + new Date('2023-01-01'), + ); + }); + + test('sorts by createdAt (date) in descending order', () => { + const sortedByDate = sortAssets(mockAssets, { + key: 'createdAt', + sortCallback: 'date', + order: 'dsc', + }); + + expect(sortedByDate[0].createdAt).toEqual(new Date('2023-01-01')); + expect(sortedByDate[sortedByDate.length - 1].createdAt).toEqual( + new Date('2021-07-20'), + ); + }); + + test('handles undefined deeply nested value gracefully when sorting', () => { + const invlaidAsset = { + name: 'Asset Y', + balance: '600', + createdAt: new Date('2024-01-01'), + profile: { id: '3' }, // No category info + }; + const sortedByCategory = sortAssets([...mockAssets, invlaidAsset], { + key: 'profile.info.category', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + // Expect the undefined categories to be at the end + expect( + // @ts-expect-error // testing for undefined value + sortedByCategory[sortedByCategory.length - 1].profile.info?.category, + ).toBeUndefined(); + }); +}); + +// Utility function to generate large mock data +function generateLargeMockData(size: number): MockAsset[] { + const mockData: MockAsset[] = []; + for (let i = 0; i < size; i++) { + mockData.push({ + name: `Asset ${String.fromCharCode(65 + (i % 26))}`, + balance: `${Math.floor(Math.random() * 1000)}`, // Random balance between 0 and 999 + createdAt: new Date(Date.now() - Math.random() * 10000000000), // Random date within the past ~115 days + profile: { + id: `${i + 1}`, + info: { + category: ['gold', 'silver', 'bronze'][i % 3], // Cycles between 'gold', 'silver', 'bronze' + }, + }, + }); + } + return mockData; +} + +// Generate a large dataset for testing +const largeDataset = generateLargeMockData(10000); // 10,000 mock assets + +// Define the sorting tests for large datasets +describe('sortAssets function - large dataset handling', () => { + const MAX_EXECUTION_TIME_MS = 500; // Set max allowed execution time (in milliseconds) + + test('sorts large dataset by name in ascending order', () => { + const startTime = Date.now(); + const sortedByName = sortAssets(largeDataset, { + key: 'name', + sortCallback: 'alphaNumeric', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + expect(sortedByName[0].name).toBe('Asset A'); + expect(sortedByName[sortedByName.length - 1].name).toBe('Asset Z'); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by balance in ascending order', () => { + const startTime = Date.now(); + const sortedByBalance = sortAssets(largeDataset, { + key: 'balance', + sortCallback: 'numeric', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const balances = sortedByBalance.map((asset) => asset.balance); + expect(balances).toEqual( + balances.slice().sort((a, b) => parseInt(a, 10) - parseInt(b, 10)), + ); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by balance in descending order', () => { + const startTime = Date.now(); + const sortedByBalance = sortAssets(largeDataset, { + key: 'balance', + sortCallback: 'numeric', + order: 'dsc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const balances = sortedByBalance.map((asset) => asset.balance); + expect(balances).toEqual( + balances.slice().sort((a, b) => parseInt(b, 10) - parseInt(a, 10)), + ); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by createdAt (date) in ascending order', () => { + const startTime = Date.now(); + const sortedByDate = sortAssets(largeDataset, { + key: 'createdAt', + sortCallback: 'date', + order: 'asc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const dates = sortedByDate.map((asset) => asset.createdAt.getTime()); + expect(dates).toEqual(dates.slice().sort((a, b) => a - b)); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); + + test('sorts large dataset by createdAt (date) in descending order', () => { + const startTime = Date.now(); + const sortedByDate = sortAssets(largeDataset, { + key: 'createdAt', + sortCallback: 'date', + order: 'dsc', + }); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + const dates = sortedByDate.map((asset) => asset.createdAt.getTime()); + expect(dates).toEqual(dates.slice().sort((a, b) => b - a)); + expect(executionTime).toBeLessThan(MAX_EXECUTION_TIME_MS); + }); +}); diff --git a/ui/components/app/assets/util/sort.ts b/ui/components/app/assets/util/sort.ts new file mode 100644 index 000000000000..b24a1c8e96a9 --- /dev/null +++ b/ui/components/app/assets/util/sort.ts @@ -0,0 +1,86 @@ +import { get } from 'lodash'; + +export type SortOrder = 'asc' | 'dsc'; +export type SortCriteria = { + key: string; + order?: 'asc' | 'dsc'; + sortCallback: SortCallbackKeys; +}; + +export type SortingType = number | string | Date; +type SortCallbackKeys = keyof SortingCallbacksT; + +export type SortingCallbacksT = { + numeric: (a: number, b: number) => number; + stringNumeric: (a: string, b: string) => number; + alphaNumeric: (a: string, b: string) => number; + date: (a: Date, b: Date) => number; +}; + +// All sortingCallbacks should be asc order, sortAssets function handles asc/dsc +const sortingCallbacks: SortingCallbacksT = { + numeric: (a: number, b: number) => a - b, + stringNumeric: (a: string, b: string) => { + return ( + parseFloat(parseFloat(a).toFixed(5)) - + parseFloat(parseFloat(b).toFixed(5)) + ); + }, + alphaNumeric: (a: string, b: string) => a.localeCompare(b), + date: (a: Date, b: Date) => a.getTime() - b.getTime(), +}; + +// Utility function to access nested properties by key path +function getNestedValue(obj: T, keyPath: string): SortingType { + return get(obj, keyPath) as SortingType; +} + +export function sortAssets(array: T[], criteria: SortCriteria): T[] { + const { key, order = 'asc', sortCallback } = criteria; + + return [...array].sort((a, b) => { + const aValue = getNestedValue(a, key); + const bValue = getNestedValue(b, key); + + // Always move undefined values to the end, regardless of sort order + if (aValue === undefined) { + return 1; + } + + if (bValue === undefined) { + return -1; + } + + let comparison: number; + + switch (sortCallback) { + case 'stringNumeric': + case 'alphaNumeric': + comparison = sortingCallbacks[sortCallback]( + aValue as string, + bValue as string, + ); + break; + case 'numeric': + comparison = sortingCallbacks.numeric( + aValue as number, + bValue as number, + ); + break; + case 'date': + comparison = sortingCallbacks.date(aValue as Date, bValue as Date); + break; + default: + if (aValue < bValue) { + comparison = -1; + } else if (aValue > bValue) { + comparison = 1; + } else { + comparison = 0; + } + } + + // Modify to sort in ascending or descending order + return order === 'asc' ? comparison : -comparison; + }); +} diff --git a/ui/components/app/confirm/info/row/address.tsx b/ui/components/app/confirm/info/row/address.tsx index ec8a0c7c669d..7d28851ece92 100644 --- a/ui/components/app/confirm/info/row/address.tsx +++ b/ui/components/app/confirm/info/row/address.tsx @@ -44,7 +44,11 @@ export const ConfirmInfoRowAddress = memo( // component can support variations. See this comment for context: // // https://github.com/MetaMask/metamask-extension/pull/23487#discussion_r1525055546 isPetNamesEnabled && !isSnapUsingThis ? ( - + ) : ( <> ( <> - {process.env.CHAIN_PERMISSIONS ? ( + {networks.length > 0 ? ( - {process.env.CHAIN_PERMISSIONS - ? t('reviewPermissions') - : t('permissions')} + {t('reviewPermissions')} - {process.env.CHAIN_PERMISSIONS - ? t('nativeNetworkPermissionRequestDescription', [ - - {getURLHost(subjectMetadata.origin)} - , - ]) - : t('nativePermissionRequestDescription', [ - - {subjectMetadata.origin} - , - ])} + {t('nativeNetworkPermissionRequestDescription', [ + + {getURLHost(subjectMetadata.origin)} + , + ])} caveat.type === CaveatTypes.restrictNetworkSwitching, - )?.value; - return ( ); } diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 99051042d2dd..2e5925dbf9cf 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -9,18 +9,13 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) InternalAccount, KeyringAccountType, - KeyringClient, ///: END:ONLY_INCLUDE_IF } from '@metamask/keyring-api'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -import { CaipChainId } from '@metamask/utils'; import { BITCOIN_WALLET_NAME, BITCOIN_WALLET_SNAP_ID, - BitcoinWalletSnapSender, - // TODO: Remove restricted import - // eslint-disable-next-line import/no-restricted-paths -} from '../../../../app/scripts/lib/snap-keyring/bitcoin-wallet-snap'; +} from '../../../../shared/lib/accounts/bitcoin-wallet-snap'; ///: END:ONLY_INCLUDE_IF import { Box, @@ -97,6 +92,7 @@ import { hasCreatedBtcTestnetAccount, } from '../../../selectors/accounts'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; +import { useBitcoinWalletSnapClient } from '../../../hooks/accounts/useBitcoinWalletSnapClient'; ///: END:ONLY_INCLUDE_IF import { InternalAccountWithBalance, @@ -261,15 +257,7 @@ export const AccountListMenu = ({ hasCreatedBtcTestnetAccount, ); - const createBitcoinAccount = async (scope: CaipChainId) => { - // Client to create the account using the Bitcoin Snap - const client = new KeyringClient(new BitcoinWalletSnapSender()); - - // This will trigger the Snap account creation flow (+ account renaming) - await client.createAccount({ - scope, - }); - }; + const bitcoinWalletSnapClient = useBitcoinWalletSnapClient(); ///: END:ONLY_INCLUDE_IF const [searchQuery, setSearchQuery] = useState(''); @@ -413,10 +401,12 @@ export const AccountListMenu = ({ // The account creation + renaming is handled by the // Snap account bridge, so we need to close the current - // model + // modal onClose(); - await createBitcoinAccount(MultichainNetworks.BITCOIN); + await bitcoinWalletSnapClient.createAccount( + MultichainNetworks.BITCOIN, + ); }} data-testid="multichain-account-menu-popover-add-btc-account" > @@ -436,10 +426,10 @@ export const AccountListMenu = ({ startIconName={IconName.Add} onClick={async () => { // The account creation + renaming is handled by the Snap account bridge, so - // we need to close the current model + // we need to close the current modal onClose(); - await createBitcoinAccount( + await bitcoinWalletSnapClient.createAccount( MultichainNetworks.BITCOIN_TESTNET, ); }} diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx index 34cbed54c127..9d265657432b 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -40,7 +40,9 @@ describe('AccountOverviewBtc', () => { const { queryByTestId } = render(); expect(queryByTestId('account-overview__asset-tab')).toBeInTheDocument(); - expect(queryByTestId('import-token-button')).not.toBeInTheDocument(); + const button = queryByTestId('import-token-button'); + expect(button).toBeInTheDocument(); // Verify the button is present + expect(button).toBeDisabled(); // Verify the button is disabled // TODO: This one might be required, but we do not really handle tokens for BTC yet... expect(queryByTestId('refresh-list-button')).not.toBeInTheDocument(); }); diff --git a/ui/components/multichain/app-header/app-header-unlocked-content.tsx b/ui/components/multichain/app-header/app-header-unlocked-content.tsx index 57e0c2f2c5fc..69ffca3f71c3 100644 --- a/ui/components/multichain/app-header/app-header-unlocked-content.tsx +++ b/ui/components/multichain/app-header/app-header-unlocked-content.tsx @@ -55,10 +55,7 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { MINUTE } from '../../../../shared/constants/time'; import { NotificationsTagCounter } from '../notifications-tag-counter'; -import { - CONNECTIONS, - REVIEW_PERMISSIONS, -} from '../../../helpers/constants/routes'; +import { REVIEW_PERMISSIONS } from '../../../helpers/constants/routes'; import { MultichainNetwork } from '../../../selectors/multichain'; type AppHeaderUnlockedContentProps = { @@ -122,11 +119,7 @@ export const AppHeaderUnlockedContent = ({ }; const handleConnectionsRoute = () => { - if (process.env.CHAIN_PERMISSIONS) { - history.push(`${REVIEW_PERMISSIONS}/${encodeURIComponent(origin)}`); - } else { - history.push(`${CONNECTIONS}/${encodeURIComponent(origin)}`); - } + history.push(`${REVIEW_PERMISSIONS}/${encodeURIComponent(origin)}`); }; return ( 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 17f5357a2fbd..62ca0ed8093a 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 = ({ - type, hostname, onClick, onClose, @@ -35,17 +34,9 @@ export const DisconnectAllModal = ({ - - {process.env.CHAIN_PERMISSIONS - ? t('disconnect') - : t('disconnectAllTitle', [t(type)])} - + {t('disconnect')} - {process.env.CHAIN_PERMISSIONS ? ( - {t('disconnectAllDescription', [hostname])} - ) : ( - {t('disconnectAllText', [t(type), hostname])} - )} + {{t('disconnectAllDescription', [hostname])}} -
@@ -42,20 +28,6 @@ exports[`Import Token Link should match snapshot for mainnet chainId 1`] = `
{ const t = useI18nContext(); @@ -39,32 +34,6 @@ export const ConnectionListItem = ({ connection, onClick }) => { getPermittedChainsForSelectedTab(state, connection.origin), ); - const renderListItem = process.env.CHAIN_PERMISSIONS ? ( - - ) : ( - - } - > - - - ); - return ( { avatarSize={IconSize.Md} /> ) : ( - <>{renderListItem} + )} { alignItems={AlignItems.center} gap={1} > - {process.env.CHAIN_PERMISSIONS ? ( - - {connection.addresses.length} {t('accountsSmallCase')}  - •  - {connectedNetworks.length} {t('networksSmallCase')} - - ) : ( - <> - - {t('connectedWith')} - - - - - )} + + {connection.addresses.length} {t('accountsSmallCase')}  + •  + {connectedNetworks.length} {t('networksSmallCase')} + )} diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js index 7e9205517cd5..ffec0e4a3b28 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js @@ -37,6 +37,10 @@ describe('ConnectionListItem', () => { iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', networkIconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', networkName: 'Test Dapp Network', + addresses: [ + '0xaaaF07C80ce267F3132cE7e6048B66E6E669365B', + '0xbbbD671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1', + ], }; const { getByText, getByTestId } = renderWithProvider( @@ -70,36 +74,4 @@ describe('ConnectionListItem', () => { fireEvent.click(getByTestId('connection-list-item')); expect(onClickMock).toHaveBeenCalledTimes(1); }); - - it('renders badgewrapper correctly for non-Snap connection', () => { - const onClickMock = jest.fn(); - const mockConnection2 = { - extensionId: null, - iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', - name: 'MM Test Dapp', - origin: 'https://metamask.github.io', - subjectType: 'website', - addresses: ['0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da'], - addressToNameMap: { - '0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da': - 'Unreasonably long account name', - }, - networkIconUrl: './images/eth_logo.svg', - networkName: 'Ethereum Mainnet', - }; - const { getByTestId } = renderWithProvider( - , - store, - ); - - expect( - getByTestId('connection-list-item__avatar-network-badge'), - ).toBeInTheDocument(); - - expect( - document - .querySelector('.mm-avatar-network__network-image') - .getAttribute('src'), - ).toBe(mockConnection2.networkIconUrl); - }); }); diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index 32fbd2cbb7eb..8cdeae0ed57d 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -23,7 +23,6 @@ import { TextVariant, } from '../../../../helpers/constants/design-system'; import { - CONNECTIONS, DEFAULT_ROUTE, REVIEW_PERMISSIONS, } from '../../../../helpers/constants/routes'; @@ -34,6 +33,7 @@ import { } from '../../../../selectors'; import { ProductTour } from '../../product-tour-popover'; import { hidePermissionsTour } from '../../../../store/actions'; +import { isSnapId } from '../../../../helpers/utils/snaps'; import { ConnectionListItem } from './connection-list-item'; export const PermissionsPage = () => { @@ -54,16 +54,14 @@ export const PermissionsPage = () => { const handleConnectionClick = (connection) => { const hostName = connection.origin; const safeEncodedHost = encodeURIComponent(hostName); - if (process.env.CHAIN_PERMISSIONS) { - history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`); - } else { - history.push(`${CONNECTIONS}/${safeEncodedHost}`); - } + + history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`); }; const renderConnectionsList = (connectionList) => Object.entries(connectionList).map(([itemKey, connection]) => { - return ( + const isSnap = isSnapId(connection.origin); + return isSnap ? null : ( { startIconName={IconName.Logout} danger onClick={() => setShowDisconnectAllModal(true)} + data-test-id="disconnect-all" > {t('disconnect')} diff --git a/ui/components/multichain/ramps-card/index.scss b/ui/components/multichain/ramps-card/index.scss index d46a317e9357..2886649e7d9a 100644 --- a/ui/components/multichain/ramps-card/index.scss +++ b/ui/components/multichain/ramps-card/index.scss @@ -1,5 +1,8 @@ .ramps-card { - padding: 8px 12px; + margin-top: 8px; + margin-left: 16px; + margin-right: 16px; + padding: 12px 16px; &__cta-button { width: fit-content; diff --git a/ui/css/design-system/_colors.scss b/ui/css/design-system/_colors.scss index f8ac7cf93da9..2d948b928012 100644 --- a/ui/css/design-system/_colors.scss +++ b/ui/css/design-system/_colors.scss @@ -1,6 +1,8 @@ $color-map: ( 'background-default': --color-background-default, 'background-alternative': --color-background-alternative, + 'background-hover': --color-background-hover, + 'background-pressed': --color-background-pressed, 'text-default': --color-text-default, 'text-alternative': --color-text-alternative, 'text-muted': --color-text-muted, diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index f8d4f19389a5..8374e812b017 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -54,6 +54,8 @@ export enum Color { export enum BackgroundColor { backgroundDefault = 'background-default', backgroundAlternative = 'background-alternative', + backgroundHover = 'background-hover', + backgroundPressed = 'background-pressed', overlayDefault = 'overlay-default', overlayAlternative = 'overlay-alternative', primaryDefault = 'primary-default', diff --git a/ui/helpers/utils/common.util.js b/ui/helpers/utils/common.util.js index 06272009a2dd..1a22a131d6c3 100644 --- a/ui/helpers/utils/common.util.js +++ b/ui/helpers/utils/common.util.js @@ -1,3 +1,19 @@ export function camelCaseToCapitalize(str = '') { return str.replace(/([A-Z])/gu, ' $1').replace(/^./u, (s) => s.toUpperCase()); } + +export function getCurrencySymbol(currencyCode) { + const supportedCurrencyCodes = { + EUR: '\u20AC', + HKD: '\u0024', + JPY: '\u00A5', + PHP: '\u20B1', + RUB: '\u20BD', + SGD: '\u0024', + USD: '\u0024', + }; + if (supportedCurrencyCodes[currencyCode.toUpperCase()]) { + return supportedCurrencyCodes[currencyCode.toUpperCase()]; + } + return currencyCode.toUpperCase(); +} diff --git a/ui/helpers/utils/tags.test.ts b/ui/helpers/utils/tags.test.ts new file mode 100644 index 000000000000..eae5e90f9ea1 --- /dev/null +++ b/ui/helpers/utils/tags.test.ts @@ -0,0 +1,206 @@ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../shared/constants/app'; +import { MetaMaskReduxState } from '../../store/store'; +import { getStartupTraceTags } from './tags'; + +jest.mock('../../../app/scripts/lib/util', () => ({ + ...jest.requireActual('../../../app/scripts/lib/util'), + getEnvironmentType: jest.fn(), +})); + +const STATE_EMPTY_MOCK = { + metamask: { + allTokens: {}, + internalAccounts: { + accounts: {}, + }, + metamaskNotificationsList: [], + }, +} as unknown as MetaMaskReduxState; + +function createMockState( + metamaskState: Partial, +): MetaMaskReduxState { + return { + ...STATE_EMPTY_MOCK, + metamask: { + ...STATE_EMPTY_MOCK.metamask, + ...metamaskState, + }, + }; +} + +describe('Tags Utils', () => { + const getEnvironmentTypeMock = jest.mocked(getEnvironmentType); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('getStartupTraceTags', () => { + it('includes UI type', () => { + getEnvironmentTypeMock.mockReturnValue(ENVIRONMENT_TYPE_FULLSCREEN); + + const tags = getStartupTraceTags(STATE_EMPTY_MOCK); + + expect(tags['wallet.ui_type']).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN); + }); + + it('includes if unlocked', () => { + const state = createMockState({ isUnlocked: true }); + const tags = getStartupTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(true); + }); + + it('includes if not unlocked', () => { + const state = createMockState({ isUnlocked: false }); + const tags = getStartupTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(false); + }); + + it('includes pending approval type', () => { + const state = createMockState({ + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + } as unknown as MetaMaskReduxState['metamask']['pendingApprovals'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes first pending approval type if multiple', () => { + const state = createMockState({ + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + 2: { + type: 'personal_sign', + }, + } as unknown as MetaMaskReduxState['metamask']['pendingApprovals'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes account count', () => { + const state = createMockState({ + internalAccounts: { + accounts: { + '0x1234': {}, + '0x4321': {}, + }, + } as unknown as MetaMaskReduxState['metamask']['internalAccounts'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.account_count']).toStrictEqual(2); + }); + + it('includes nft count', () => { + const state = createMockState({ + allNfts: { + '0x1234': { + '0x1': [ + { + tokenId: '1', + }, + { + tokenId: '2', + }, + ], + '0x2': [ + { + tokenId: '3', + }, + { + tokenId: '4', + }, + ], + }, + '0x4321': { + '0x3': [ + { + tokenId: '5', + }, + ], + }, + } as unknown as MetaMaskReduxState['metamask']['allNfts'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.nft_count']).toStrictEqual(5); + }); + + it('includes notification count', () => { + const state = createMockState({ + metamaskNotificationsList: [ + {}, + {}, + {}, + ] as unknown as MetaMaskReduxState['metamask']['metamaskNotificationsList'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.notification_count']).toStrictEqual(3); + }); + + it('includes token count', () => { + const state = createMockState({ + allTokens: { + '0x1': { + '0x1234': [{}, {}], + '0x4321': [{}], + }, + '0x2': { + '0x5678': [{}], + }, + } as unknown as MetaMaskReduxState['metamask']['allTokens'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.token_count']).toStrictEqual(4); + }); + + it('includes transaction count', () => { + const state = createMockState({ + transactions: [ + { + id: 1, + chainId: '0x1', + }, + { + id: 2, + chainId: '0x1', + }, + { + id: 3, + chainId: '0x2', + }, + ] as unknown as MetaMaskReduxState['metamask']['transactions'], + }); + + const tags = getStartupTraceTags(state); + + expect(tags['wallet.transaction_count']).toStrictEqual(3); + }); + }); +}); diff --git a/ui/helpers/utils/tags.ts b/ui/helpers/utils/tags.ts new file mode 100644 index 000000000000..4a253e214d82 --- /dev/null +++ b/ui/helpers/utils/tags.ts @@ -0,0 +1,42 @@ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getEnvironmentType } from '../../../app/scripts/lib/util'; +import { getIsUnlocked } from '../../ducks/metamask/metamask'; +import { + getInternalAccounts, + getPendingApprovals, + getTransactions, + selectAllTokensFlat, +} from '../../selectors'; +import { getMetamaskNotifications } from '../../selectors/metamask-notifications/metamask-notifications'; +import { selectAllNftsFlat } from '../../selectors/nft'; +import { MetaMaskReduxState } from '../../store/store'; + +/** + * Generate the required tags for the UI startup trace. + * + * @param state - The current flattened UI state. + * @returns The tags for the startup trace. + */ +export function getStartupTraceTags(state: MetaMaskReduxState) { + const uiType = getEnvironmentType(); + const unlocked = getIsUnlocked(state) as boolean; + const accountCount = getInternalAccounts(state).length; + const nftCount = selectAllNftsFlat(state).length; + const notificationCount = getMetamaskNotifications(state).length; + const tokenCount = selectAllTokensFlat(state).length as number; + const transactionCount = getTransactions(state).length; + const pendingApprovals = getPendingApprovals(state); + const firstApprovalType = pendingApprovals?.[0]?.type; + + return { + 'wallet.account_count': accountCount, + 'wallet.nft_count': nftCount, + 'wallet.notification_count': notificationCount, + 'wallet.pending_approval': firstApprovalType, + 'wallet.token_count': tokenCount, + 'wallet.transaction_count': transactionCount, + 'wallet.unlocked': unlocked, + 'wallet.ui_type': uiType, + }; +} diff --git a/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts new file mode 100644 index 000000000000..6032a7636128 --- /dev/null +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts @@ -0,0 +1,57 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { HandlerType } from '@metamask/snaps-utils'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; +import { + handleSnapRequest, + multichainUpdateBalance, +} from '../../store/actions'; +import { useBitcoinWalletSnapClient } from './useBitcoinWalletSnapClient'; + +jest.mock('../../store/actions', () => ({ + handleSnapRequest: jest.fn(), + multichainUpdateBalance: jest.fn(), +})); + +const mockHandleSnapRequest = handleSnapRequest as jest.Mock; +const mockMultichainUpdateBalance = multichainUpdateBalance as jest.Mock; + +describe('useBitcoinWalletSnapClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockAccount = { + address: 'tb1q2hjrlnf8kmtt5dj6e49gqzy6jnpe0sj7ty50cl', + id: '11a33c6b-0d46-43f4-a401-01587d575fd0', + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, + }; + + it('dispatch a Snap keyring request to create a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockHandleSnapRequest).toHaveBeenCalledWith({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request: expect.any(Object), + }); + }); + + it('force fetches the balance after creating a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockMultichainUpdateBalance).toHaveBeenCalledWith(mockAccount.id); + }); +}); diff --git a/ui/hooks/accounts/useBitcoinWalletSnapClient.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts new file mode 100644 index 000000000000..debe911ac391 --- /dev/null +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts @@ -0,0 +1,52 @@ +import { KeyringClient, Sender } from '@metamask/keyring-api'; +import { HandlerType } from '@metamask/snaps-utils'; +import { CaipChainId, Json, JsonRpcRequest } from '@metamask/utils'; +import { useMemo } from 'react'; +import { + handleSnapRequest, + multichainUpdateBalance, +} from '../../store/actions'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; + +export class BitcoinWalletSnapSender implements Sender { + send = async (request: JsonRpcRequest): Promise => { + // We assume the caller of this module is aware of this. If we try to use this module + // without having the pre-installed Snap, this will likely throw an error in + // the `handleSnapRequest` action. + return (await handleSnapRequest({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request, + })) as Json; + }; +} + +export class BitcoinWalletSnapClient { + readonly #client: KeyringClient; + + constructor() { + this.#client = new KeyringClient(new BitcoinWalletSnapSender()); + } + + async createAccount(scope: CaipChainId) { + // This will trigger the Snap account creation flow (+ account renaming) + const account = await this.#client.createAccount({ + scope, + }); + + // NOTE: The account's balance is going to be tracked automatically on when the new account + // will be added to the Snap bridge keyring (see `BalancesController:#handleOnAccountAdded`). + // However, the balance won't be fetched right away. To workaround this, we trigger the + // fetch explicitly here (since we are already in a `async` call) and wait for it to be updated! + await multichainUpdateBalance(account.id); + } +} + +export function useBitcoinWalletSnapClient() { + const client = useMemo(() => { + return new BitcoinWalletSnapClient(); + }, []); + + return client; +} diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx index 481ad5deec9f..951cec333ade 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx +++ b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx @@ -9,6 +9,7 @@ import { useEnableProfileSyncing, useDisableProfileSyncing, useAccountSyncingEffect, + useDeleteAccountSyncingDataFromUserStorage, } from './useProfileSyncing'; const middlewares = [thunk]; @@ -22,6 +23,7 @@ jest.mock('../../store/actions', () => ({ showLoadingIndication: jest.fn(), hideLoadingIndication: jest.fn(), syncInternalAccountsWithUserStorage: jest.fn(), + deleteAccountSyncingDataFromUserStorage: jest.fn(), })); type ArrangeMocksMetamaskStateOverrides = { @@ -132,4 +134,23 @@ describe('useProfileSyncing', () => { ).not.toHaveBeenCalled(); }); }); + + it('should dispatch account sync data deletion', async () => { + const { store } = arrangeMocks(); + + const { result } = renderHook( + () => useDeleteAccountSyncingDataFromUserStorage(), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + + act(() => { + result.current.dispatchDeleteAccountSyncingDataFromUserStorage(); + }); + + expect(actions.deleteAccountSyncingDataFromUserStorage).toHaveBeenCalled(); + }); }); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing.ts index 1306e160cb5e..67899aa73927 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing.ts +++ b/ui/hooks/metamask-notifications/useProfileSyncing.ts @@ -8,6 +8,7 @@ import { setIsProfileSyncingEnabled as setIsProfileSyncingEnabledAction, hideLoadingIndication, syncInternalAccountsWithUserStorage, + deleteAccountSyncingDataFromUserStorage, } from '../../store/actions'; import { selectIsSignedIn } from '../../selectors/metamask-notifications/authentication'; @@ -176,6 +177,32 @@ export const useAccountSyncing = () => { }; }; +/** + * Custom hook to delete a user's account syncing data from user storage + */ + +export const useDeleteAccountSyncingDataFromUserStorage = () => { + const dispatch = useDispatch(); + + const [error, setError] = useState(null); + + const dispatchDeleteAccountSyncingDataFromUserStorage = useCallback(() => { + setError(null); + + try { + dispatch(deleteAccountSyncingDataFromUserStorage()); + } catch (e) { + log.error(e); + setError(e instanceof Error ? e.message : 'An unexpected error occurred'); + } + }, [dispatch]); + + return { + dispatchDeleteAccountSyncingDataFromUserStorage, + error, + }; +}; + /** * Custom hook to apply account syncing effect. */ diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index 9a3fe389b4de..b0c9b293c906 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -1,10 +1,12 @@ import { shallowEqual, useSelector } from 'react-redux'; +import { toChecksumAddress } from 'ethereumjs-util'; import { getAllTokens, getCurrentChainId, getCurrentCurrency, getMetaMaskCachedBalances, getTokenExchangeRates, + getConfirmationExchangeRates, getNativeCurrencyImage, getTokenList, } from '../selectors'; @@ -19,7 +21,7 @@ import { } from '../ducks/metamask/metamask'; import { formatCurrency } from '../helpers/utils/confirm-tx.util'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; -import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; +import { roundToDecimalPlacesRemovingExtraZeroes } from '../helpers/utils/util'; import { useTokenTracker } from './useTokenTracker'; export const useAccountTotalFiatBalance = ( @@ -34,6 +36,7 @@ export const useAccountTotalFiatBalance = ( getTokenExchangeRates, shallowEqual, ); + const confirmationExchangeRates = useSelector(getConfirmationExchangeRates); const cachedBalances = useSelector(getMetaMaskCachedBalances); const balance = cachedBalances?.[account?.address] ?? 0; @@ -59,15 +62,14 @@ export const useAccountTotalFiatBalance = ( hideZeroBalanceTokens: shouldHideZeroBalanceTokens, }); + const mergedRates = { + ...contractExchangeRates, + ...confirmationExchangeRates, + }; + // Create fiat values for token balances const tokenFiatBalances = tokensWithBalances.map((token) => { - const contractExchangeTokenKey = Object.keys(contractExchangeRates).find( - (key) => isEqualCaseInsensitive(key, token.address), - ); - const tokenExchangeRate = - (contractExchangeTokenKey && - contractExchangeRates[contractExchangeTokenKey]) ?? - 0; + const tokenExchangeRate = mergedRates[toChecksumAddress(token.address)]; const totalFiatValue = getTokenFiatAmount( tokenExchangeRate, @@ -136,6 +138,29 @@ export const useAccountTotalFiatBalance = ( ...tokenFiatBalances, ).toString(10); + // we need to append some values to tokensWithBalance for UI + // this code was ported from asset-list + tokensWithBalances.forEach((token) => { + // token.string is the balance displayed in the TokenList UI + token.string = roundToDecimalPlacesRemovingExtraZeroes(token.string, 5); + }); + + // to sort by fiat balance, we need to compute this at this level + tokensWithBalances.forEach((token) => { + const tokenExchangeRate = mergedRates[toChecksumAddress(token.address)]; + + token.tokenFiatAmount = + getTokenFiatAmount( + tokenExchangeRate, + conversionRate, + currentCurrency, + token.string, // tokenAmount + token.symbol, // tokenSymbol + false, // no currency symbol prefix + false, // no ticker symbol suffix + ) || '0'; + }); + // Fiat balance formatted in user's desired currency (ex: "$8.90") const formattedFiat = formatCurrency(totalFiatBalance, currentCurrency); @@ -160,5 +185,6 @@ export const useAccountTotalFiatBalance = ( tokensWithBalances, loading, orderedTokenList, + mergedRates, }; }; diff --git a/ui/hooks/useAccountTotalFiatBalance.test.js b/ui/hooks/useAccountTotalFiatBalance.test.js index c883eb37cc1e..9fb1227367e1 100644 --- a/ui/hooks/useAccountTotalFiatBalance.test.js +++ b/ui/hooks/useAccountTotalFiatBalance.test.js @@ -125,19 +125,25 @@ describe('useAccountTotalFiatBalance', () => { image: undefined, isERC721: undefined, decimals: 6, - string: '0.04857', + string: 0.04857, balanceError: null, + tokenFiatAmount: '0.05', }, { address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', symbol: 'YFI', balance: '1409247882142934', decimals: 18, - string: '0.001409247882142934', + string: 0.00141, balanceError: null, + tokenFiatAmount: '7.52', }, ], loading: false, + mergedRates: { + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': 3.304588, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 0.0006189, + }, orderedTokenList: [ { fiatBalance: '1.85', diff --git a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx index 888d70e1d62c..ffd664612a02 100644 --- a/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx +++ b/ui/hooks/useMultichainAccountTotalFiatBalance.test.tsx @@ -141,6 +141,10 @@ describe('useMultichainAccountTotalFiatBalance', () => { expect(result.current).toStrictEqual({ formattedFiat: '$9.41', loading: false, + mergedRates: { + '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': 3.304588, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 0.0006189, + }, totalWeiBalance: '14ba1e6a08a9ed', tokensWithBalances: mockTokenBalances, totalFiatBalance: '9.41', diff --git a/ui/index.js b/ui/index.js index 5cb576e488d6..8cf2048cba41 100644 --- a/ui/index.js +++ b/ui/index.js @@ -39,6 +39,7 @@ import { import Root from './pages'; import txHelper from './helpers/utils/tx-helper'; import { setBackgroundConnection } from './store/background-connection'; +import { getStartupTraceTags } from './helpers/utils/tags'; log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn', false); @@ -182,8 +183,14 @@ export async function setupInitialStore( async function startApp(metamaskState, backgroundConnection, opts) { const { traceContext } = opts; + const tags = getStartupTraceTags({ metamask: metamaskState }); + const store = await trace( - { name: TraceName.SetupStore, parentContext: traceContext }, + { + name: TraceName.SetupStore, + parentContext: traceContext, + tags, + }, () => setupInitialStore(metamaskState, backgroundConnection, opts.activeTab), ); diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx index 2a97577756b2..bdbe0e6fbae3 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx @@ -87,6 +87,7 @@ export const ApproveStaticSimulation = () => { diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx index e4604fb715ab..448506f17126 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.test.tsx @@ -3,7 +3,10 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { getMockApproveConfirmState } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; -import { EditSpendingCapModal } from './edit-spending-cap-modal'; +import { + countDecimalDigits, + EditSpendingCapModal, +} from './edit-spending-cap-modal'; jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), @@ -78,3 +81,26 @@ describe('', () => { expect(container).toMatchSnapshot(); }); }); + +describe('countDecimalDigits()', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { numberString: '0', expectedDecimals: 0 }, + { numberString: '100', expectedDecimals: 0 }, + { numberString: '100.123', expectedDecimals: 3 }, + { numberString: '3.141592654', expectedDecimals: 9 }, + ])( + 'should return $expectedDecimals decimals for `$numberString`', + ({ + numberString, + expectedDecimals, + }: { + numberString: string; + expectedDecimals: number; + }) => { + const actual = countDecimalDigits(numberString); + + expect(actual).toEqual(expectedDecimals); + }, + ); +}); diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index e7431457f5c2..2762e99652a5 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -32,6 +32,10 @@ import { useConfirmContext } from '../../../../../context/confirm'; import { useAssetDetails } from '../../../../../hooks/useAssetDetails'; import { useApproveTokenSimulation } from '../hooks/use-approve-token-simulation'; +export function countDecimalDigits(numberString: string) { + return numberString.split('.')[1]?.length || 0; +} + export const EditSpendingCapModal = ({ isOpenEditSpendingCapModal, setIsOpenEditSpendingCapModal, @@ -116,10 +120,14 @@ export const EditSpendingCapModal = ({ setCustomSpendingCapInputValue(formattedSpendingCap.toString()); }, [customSpendingCapInputValue, formattedSpendingCap]); + const showDecimalError = + decimals && + parseInt(decimals, 10) < countDecimalDigits(customSpendingCapInputValue); + return ( setIsOpenEditSpendingCapModal(false)} + onClose={handleCancel} isClosedOnEscapeKey isClosedOnOutsideClick className="edit-spending-cap-modal" @@ -154,6 +162,15 @@ export const EditSpendingCapModal = ({ style={{ width: '100%' }} inputProps={{ 'data-testid': 'custom-spending-cap-input' }} /> + {showDecimalError && ( + + {t('editSpendingCapError', [decimals])} + + )} diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx index 199852538f17..38ff93ba9b36 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx @@ -23,6 +23,7 @@ export const RevokeStaticSimulation = () => { @@ -36,6 +37,7 @@ export const RevokeStaticSimulation = () => { diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx index 7cc141fb64e5..64e90a7066e6 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx @@ -29,6 +29,7 @@ export const RevokeSetApprovalForAllStaticSimulation = ({ @@ -39,7 +40,11 @@ export const RevokeSetApprovalForAllStaticSimulation = ({ - + diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx index c50d10094486..177ef4080860 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx @@ -47,6 +47,7 @@ export const SetApprovalForAllStaticSimulation = () => { diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx index 25fad3020103..633191cd2638 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx @@ -116,7 +116,11 @@ const PermitSimulationValueDisplay: React.FC< - + {fiatValue && } diff --git a/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap b/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap deleted file mode 100644 index 3115caf5af16..000000000000 --- a/ui/pages/permissions-connect/__snapshots__/permissions-connect.test.tsx.snap +++ /dev/null @@ -1,236 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PermissionApprovalContainer ConnectPath renders correctly 1`] = ` -
-
-
-
-
-
- m -
-
-
-

- metamask.io -

-

- https://metamask.io -

-
-
-
- - -
-
-`; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 09befa7218cd..417a82777b36 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -19,6 +19,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { PermissionNames } from '../../../app/scripts/controllers/permissions'; +import { isSnapId } from '../../helpers/utils/snaps'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; import SnapsConnect from './snaps/snaps-connect'; @@ -328,6 +329,8 @@ export default class PermissionConnect extends Component { snapsInstallPrivacyWarningShown, } = this.state; + const isRequestingSnap = isSnapId(permissionsRequest?.metadata?.origin); + return (
{!hideTopBar && this.renderTopBar(permissionsRequestId)} @@ -339,17 +342,7 @@ export default class PermissionConnect extends Component { path={connectPath} exact render={() => - process.env.CHAIN_PERMISSIONS ? ( - - this.cancelPermissionsRequest(requestId) - } - activeTabOrigin={this.state.origin} - request={permissionsRequest} - permissionsRequestId={permissionsRequestId} - approveConnection={this.approveConnection} - /> - ) : ( + isRequestingSnap ? ( + ) : ( + + this.cancelPermissionsRequest(requestId) + } + activeTabOrigin={this.state.origin} + request={permissionsRequest} + permissionsRequestId={permissionsRequestId} + approveConnection={this.approveConnection} + /> ) } /> diff --git a/ui/pages/permissions-connect/permissions-connect.test.tsx b/ui/pages/permissions-connect/permissions-connect.test.tsx deleted file mode 100644 index 05b1120cf5d8..000000000000 --- a/ui/pages/permissions-connect/permissions-connect.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { ApprovalType } from '@metamask/controller-utils'; -import { BtcAccountType } from '@metamask/keyring-api'; -import { fireEvent } from '@testing-library/react'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import messages from '../../../app/_locales/en/messages.json'; -import { renderWithProvider } from '../../../test/lib/render-helpers'; -import mockState from '../../../test/data/mock-state.json'; -import { CONNECT_ROUTE } from '../../helpers/constants/routes'; -import { createMockInternalAccount } from '../../../test/jest/mocks'; -import { shortenAddress } from '../../helpers/utils/util'; -import PermissionApprovalContainer from './permissions-connect.container'; - -const mockPermissionRequestId = '0cbc1f26-8772-4512-8ad7-f547d6e8b72c'; - -jest.mock('../../store/actions', () => { - return { - ...jest.requireActual('../../store/actions'), - getRequestAccountTabIds: jest.fn().mockReturnValue({ - type: 'SET_REQUEST_ACCOUNT_TABS', - payload: {}, - }), - }; -}); - -const mockAccount = createMockInternalAccount({ name: 'Account 1' }); -const mockBtcAccount = createMockInternalAccount({ - name: 'BTC Account', - address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', - type: BtcAccountType.P2wpkh, -}); - -const defaultProps = { - history: { - location: { - pathname: `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - }, - }, - location: { - pathname: `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - }, - match: { - params: { - id: mockPermissionRequestId, - }, - }, -}; - -const render = ( - props = defaultProps, - type: ApprovalType = ApprovalType.WalletRequestPermissions, -) => { - let pendingPermission; - if (type === ApprovalType.WalletRequestPermissions) { - pendingPermission = { - id: mockPermissionRequestId, - origin: 'https://metamask.io', - type: ApprovalType.WalletRequestPermissions, - time: 1721376328642, - requestData: { - metadata: { - id: mockPermissionRequestId, - origin: 'https://metamask.io', - }, - permissions: { - eth_accounts: {}, - }, - }, - requestState: null, - expectsResult: false, - }; - } - - const state = { - ...mockState, - metamask: { - ...mockState.metamask, - internalAccounts: { - accounts: { - [mockAccount.id]: mockAccount, - [mockBtcAccount.id]: mockBtcAccount, - }, - selectedAccount: mockAccount.id, - }, - keyrings: [ - { - type: 'HD Key Tree', - accounts: [mockAccount.address], - }, - { - type: 'Snap Keyring', - accounts: [mockBtcAccount.address], - }, - ], - accounts: { - [mockAccount.address]: { - address: mockAccount.address, - balance: '0x0', - }, - }, - balances: { - [mockBtcAccount.id]: {}, - }, - pendingApprovals: { - [mockPermissionRequestId]: pendingPermission, - }, - }, - }; - const middlewares = [thunk]; - const mockStore = configureStore(middlewares); - const store = mockStore(state); - - return { - render: renderWithProvider( - , - store, - `${CONNECT_ROUTE}/${mockPermissionRequestId}`, - ), - store, - }; -}; - -describe('PermissionApprovalContainer', () => { - describe('ConnectPath', () => { - it('renders correctly', () => { - const { - render: { container, getByText }, - } = render(); - expect(getByText(messages.next.message)).toBeInTheDocument(); - expect(getByText(messages.cancel.message)).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the list without BTC accounts', async () => { - const { - render: { getByText, queryByText }, - } = render(); - expect( - getByText( - `${mockAccount.metadata.name} (${shortenAddress( - mockAccount.address, - )})`, - ), - ).toBeInTheDocument(); - expect( - queryByText( - `${mockBtcAccount.metadata.name} (${shortenAddress( - mockBtcAccount.address, - )})`, - ), - ).not.toBeInTheDocument(); - }); - }); - - describe('Add new account', () => { - it('displays the correct account number', async () => { - const { - render: { getByText }, - store, - } = render(); - fireEvent.click(getByText(messages.newAccount.message)); - - const dispatchedActions = store.getActions(); - - expect(dispatchedActions).toHaveLength(2); // first action is 'SET_REQUEST_ACCOUNT_TABS' - expect(dispatchedActions[1]).toStrictEqual({ - type: 'UI_MODAL_OPEN', - payload: { - name: 'NEW_ACCOUNT', - onCreateNewAccount: expect.any(Function), - newAccountNumber: 2, - }, - }); - }); - }); -}); diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 1fdbad27ed67..25c41ca37c82 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -787,7 +787,7 @@ export default class Routes extends Component { /> ) : null} - {process.env.CHAIN_PERMISSIONS && isPermittedNetworkToastOpen ? ( + {isPermittedNetworkToastOpen ? ( { announcements: {}, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), newPrivacyPolicyToastShownDate: new Date('0'), + preferences: { + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + }, }, send: { ...mockSendState.send, diff --git a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap index f8cd5cd61006..4eea2d9cf7d1 100644 --- a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap +++ b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap @@ -240,6 +240,55 @@ exports[`Develop options tab should match snapshot 1`] = `
+

+ Profile Sync +

+
+
+
+ + Account syncing + +
+ Deletes all user storage entries for the current SRP. This can help if you tested Account Syncing early on and have corrupted data. This will not remove internal accounts already created and renamed. If you want to start from scratch with only the first account and restart syncing from this point on, you will need to reinstall the extension after this action. +
+
+
+ +
+
+
+
+
+
+

diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index fa5d58406a14..a88d735a628f 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -39,6 +39,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; import ToggleRow from './developer-options-toggle-row-component'; import { SentryTest } from './sentry-test'; +import { ProfileSyncDevSettings } from './profile-sync'; /** * Settings Page for Developer Options (internal-only) @@ -260,6 +261,8 @@ const DeveloperOptionsTab = () => { {renderServiceWorkerKeepAliveToggle()} {renderEnableConfirmationsRedesignToggle()} + + ); diff --git a/ui/pages/settings/developer-options-tab/profile-sync.tsx b/ui/pages/settings/developer-options-tab/profile-sync.tsx new file mode 100644 index 000000000000..a5a4f8893f15 --- /dev/null +++ b/ui/pages/settings/developer-options-tab/profile-sync.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useState } from 'react'; + +import { + Box, + Button, + ButtonVariant, + Icon, + IconName, + IconSize, + Text, +} from '../../../components/component-library'; + +import { + IconColor, + Display, + FlexDirection, + JustifyContent, + AlignItems, +} from '../../../helpers/constants/design-system'; +import { useDeleteAccountSyncingDataFromUserStorage } from '../../../hooks/metamask-notifications/useProfileSyncing'; + +const AccountSyncDeleteDataFromUserStorage = () => { + const [hasDeletedAccountSyncEntries, setHasDeletedAccountSyncEntries] = + useState(false); + + const { dispatchDeleteAccountSyncingDataFromUserStorage } = + useDeleteAccountSyncingDataFromUserStorage(); + + const handleDeleteAccountSyncingDataFromUserStorage = + useCallback(async () => { + await dispatchDeleteAccountSyncingDataFromUserStorage(); + setHasDeletedAccountSyncEntries(true); + }, [ + dispatchDeleteAccountSyncingDataFromUserStorage, + setHasDeletedAccountSyncEntries, + ]); + + return ( +

+ +
+ Account syncing +
+ Deletes all user storage entries for the current SRP. This can help + if you tested Account Syncing early on and have corrupted data. This + will not remove internal accounts already created and renamed. If + you want to start from scratch with only the first account and + restart syncing from this point on, you will need to reinstall the + extension after this action. +
+
+ +
+ +
+
+ + +
+
+
+ ); +}; + +export const ProfileSyncDevSettings = () => { + return ( + <> + + Profile Sync + + + + ); +}; 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 7ea900c5eb59..72050df4aca9 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( diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 2ef892db3353..1148e8d86468 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -244,10 +244,13 @@ export function getMultichainNativeCurrency( : getMultichainProviderConfig(state, account).ticker; } -export function getMultichainCurrentCurrency(state: MultichainState) { +export function getMultichainCurrentCurrency( + state: MultichainState, + account?: InternalAccount, +) { const currentCurrency = getCurrentCurrency(state); - if (getMultichainIsEvm(state)) { + if (getMultichainIsEvm(state, account)) { return currentCurrency; } @@ -256,7 +259,7 @@ export function getMultichainCurrentCurrency(state: MultichainState) { // fallback to the current ticker symbol value return currentCurrency && currentCurrency.toLowerCase() === 'usd' ? 'usd' - : getMultichainProviderConfig(state).ticker; + : getMultichainProviderConfig(state, account).ticker; } export function getMultichainCurrencyImage( diff --git a/ui/selectors/nft.test.ts b/ui/selectors/nft.test.ts index 101eb4aae181..d6f4d956f020 100644 --- a/ui/selectors/nft.test.ts +++ b/ui/selectors/nft.test.ts @@ -38,6 +38,7 @@ describe('NFT Selectors', () => { [chainIdMock2]: [contractMock5], }, }, + allNfts: {}, }, }; @@ -80,6 +81,7 @@ describe('NFT Selectors', () => { [chainIdMock2]: [contractMock5], }, }, + allNfts: {}, }, }; diff --git a/ui/selectors/nft.ts b/ui/selectors/nft.ts index 8320c6258b1c..ab3836714923 100644 --- a/ui/selectors/nft.ts +++ b/ui/selectors/nft.ts @@ -1,14 +1,19 @@ -import { NftContract } from '@metamask/assets-controllers'; +import { Nft, NftContract } from '@metamask/assets-controllers'; import { createSelector } from 'reselect'; import { getMemoizedCurrentChainId } from './selectors'; -type NftState = { +export type NftState = { metamask: { allNftContracts: { [account: string]: { [chainId: string]: NftContract[]; }; }; + allNfts: { + [account: string]: { + [chainId: string]: Nft[]; + }; + }; }; }; @@ -16,6 +21,16 @@ function getNftContractsByChainByAccount(state: NftState) { return state.metamask.allNftContracts ?? {}; } +/** + * Get all NFTs owned by the user. + * + * @param state - Metamask state. + * @returns All NFTs owned by the user, keyed by chain ID then account address. + */ +function getNftsByChainByAccount(state: NftState) { + return state.metamask.allNfts ?? {}; +} + export const getNftContractsByAddressByChain = createSelector( getNftContractsByChainByAccount, (nftContractsByChainByAccount) => { @@ -53,3 +68,21 @@ export const getNftContractsByAddressOnCurrentChain = createSelector( return nftContractsByAddressByChain[currentChainId] ?? {}; }, ); + +/** + * Get a flattened list of all NFTs owned by the user. + * Includes all NFTs from all chains and accounts. + * + * @param state - Metamask state. + * @returns All NFTs owned by the user in a single array. + */ +export const selectAllNftsFlat = createSelector( + getNftsByChainByAccount, + (nftsByChainByAccount) => { + const nftsByChainArray = Object.values(nftsByChainByAccount); + return nftsByChainArray.reduce((acc, nftsByChain) => { + const nftsArrays = Object.values(nftsByChain); + return acc.concat(...nftsArrays); + }, []); + }, +); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 644924a41e3e..17e6ffc4500a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -490,6 +490,24 @@ export function getAllTokens(state) { return state.metamask.allTokens; } +/** + * Get a flattened list of all ERC-20 tokens owned by the user. + * Includes all tokens from all chains and accounts. + * + * @returns {object[]} All ERC-20 tokens owned by the user in a flat array. + */ +export const selectAllTokensFlat = createSelector( + getAllTokens, + (tokensByAccountByChain) => { + const tokensByAccountArray = Object.values(tokensByAccountByChain); + + return tokensByAccountArray.reduce((acc, tokensByAccount) => { + const tokensArray = Object.values(tokensByAccount); + return acc.concat(...tokensArray); + }, []); + }, +); + /** * Selector to return an origin to network ID map * diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 6e1e33d9531f..6f8080e516ae 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -174,3 +174,5 @@ export const HIDE_KEYRING_SNAP_REMOVAL_RESULT = export const SET_SHOW_NFT_AUTO_DETECT_MODAL_UPGRADE = 'SET_SHOW_NFT_AUTO_DETECT_MODAL_UPGRADE'; + +export const TOKEN_SORT_CRITERIA = 'TOKEN_SORT_CRITERIA'; diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index a136287f039c..8d72ce63e32d 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -2539,6 +2539,33 @@ describe('Actions', () => { }); }); + describe('deleteAccountSyncingDataFromUserStorage', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls deleteAccountSyncingDataFromUserStorage in the background', async () => { + const store = mockStore(); + + const deleteAccountSyncingDataFromUserStorageStub = sinon + .stub() + .callsFake((_, cb) => { + return cb(); + }); + + background.getApi.returns({ + deleteAccountSyncingDataFromUserStorage: + deleteAccountSyncingDataFromUserStorageStub, + }); + setBackgroundConnection(background.getApi()); + + await store.dispatch(actions.deleteAccountSyncingDataFromUserStorage()); + expect( + deleteAccountSyncingDataFromUserStorageStub.calledOnceWith('accounts'), + ).toBe(true); + }); + }); + describe('removePermittedChain', () => { afterEach(() => { sinon.restore(); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index e64d366a7c74..3dbf61ba0386 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -119,6 +119,7 @@ import { getMethodDataAsync } from '../../shared/lib/four-byte'; import { DecodedTransactionDataResponse } from '../../shared/types/transaction-decode'; import { LastInteractedConfirmationInfo } from '../pages/confirmations/types/confirm'; import { EndTraceRequest } from '../../shared/lib/trace'; +import { SortCriteria } from '../components/app/assets/util/sort'; import { CaveatTypes, EndowmentTypes, @@ -3000,6 +3001,7 @@ export function setFeatureFlag( export function setPreference( preference: string, value: boolean | string | object, + showLoading: boolan = true, ): ThunkAction< Promise, MetaMaskReduxState, @@ -3007,13 +3009,13 @@ export function setPreference( AnyAction > { return (dispatch: MetaMaskReduxDispatch) => { - dispatch(showLoadingIndication()); + showLoading && dispatch(showLoadingIndication()); return new Promise((resolve, reject) => { callBackgroundMethod( 'setPreference', [preference, value], (err, updatedPreferences) => { - dispatch(hideLoadingIndication()); + showLoading && dispatch(hideLoadingIndication()); if (err) { dispatch(displayWarning(err)); reject(err); @@ -3083,6 +3085,10 @@ export function setRedesignedConfirmationsDeveloperEnabled(value: boolean) { return setPreference('isRedesignedConfirmationsDeveloperEnabled', value); } +export function setTokenSortConfig(value: SortCriteria) { + return setPreference('tokenSortConfig', value, false); +} + export function setSmartTransactionsOptInStatus( value: boolean, ): ThunkAction { @@ -5462,6 +5468,34 @@ export function syncInternalAccountsWithUserStorage(): ThunkAction< }; } +/** + * Delete all of current user's accounts data from user storage. + * + * This function sends a request to the background script to sync accounts data and update the state accordingly. + * If the operation encounters an error, it logs the error message and rethrows the error to ensure it is handled appropriately. + * + * @returns A thunk action that, when dispatched, attempts to synchronize accounts data with user storage between devices. + */ +export function deleteAccountSyncingDataFromUserStorage(): ThunkAction< + void, + MetaMaskReduxState, + unknown, + AnyAction +> { + return async () => { + try { + const response = await submitRequestToBackground( + 'deleteAccountSyncingDataFromUserStorage', + ['accounts'], + ); + return response; + } catch (error) { + logErrorWithMessage(error); + throw error; + } + }; +} + /** * Marks MetaMask notifications as read. * diff --git a/ui/store/store.ts b/ui/store/store.ts index 6e580c137bdc..8433511380e7 100644 --- a/ui/store/store.ts +++ b/ui/store/store.ts @@ -5,6 +5,11 @@ import { ApprovalControllerState } from '@metamask/approval-controller'; import { GasEstimateType, GasFeeEstimates } from '@metamask/gas-fee-controller'; import { TransactionMeta } from '@metamask/transaction-controller'; import { InternalAccount } from '@metamask/keyring-api'; +import { + NftControllerState, + TokensControllerState, +} from '@metamask/assets-controllers'; +import { NotificationServicesControllerState } from '@metamask/notification-services-controller/notification-services'; import rootReducer from '../ducks'; import { LedgerTransportTypes } from '../../shared/constants/hardware-wallets'; import type { NetworkStatus } from '../../shared/constants/network'; @@ -45,48 +50,50 @@ export type MessagesIndexedById = { * state received from the background takes precedence over anything in the * metamask reducer. */ -type TemporaryBackgroundState = { - addressBook: { - [chainId: string]: { - name: string; - }[]; - }; - // todo: can this be deleted post network controller v20 - providerConfig: { - chainId: string; - }; - transactions: TransactionMeta[]; - ledgerTransportType: LedgerTransportTypes; - unapprovedDecryptMsgs: MessagesIndexedById; - unapprovedPersonalMsgs: MessagesIndexedById; - unapprovedTypedMessages: MessagesIndexedById; - networksMetadata: { - [NetworkClientId: string]: { - EIPS: { [eip: string]: boolean }; - status: NetworkStatus; +type TemporaryBackgroundState = NftControllerState & + NotificationServicesControllerState & + TokensControllerState & { + addressBook: { + [chainId: string]: { + name: string; + }[]; }; - }; - selectedNetworkClientId: string; - pendingApprovals: ApprovalControllerState['pendingApprovals']; - approvalFlows: ApprovalControllerState['approvalFlows']; - knownMethodData?: { - [fourBytePrefix: string]: Record; - }; - gasFeeEstimates: GasFeeEstimates; - gasEstimateType: GasEstimateType; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - custodyAccountDetails?: { [key: string]: any }; - ///: END:ONLY_INCLUDE_IF - internalAccounts: { - accounts: { - [key: string]: InternalAccount; + // todo: can this be deleted post network controller v20 + providerConfig: { + chainId: string; + }; + transactions: TransactionMeta[]; + ledgerTransportType: LedgerTransportTypes; + unapprovedDecryptMsgs: MessagesIndexedById; + unapprovedPersonalMsgs: MessagesIndexedById; + unapprovedTypedMessages: MessagesIndexedById; + networksMetadata: { + [NetworkClientId: string]: { + EIPS: { [eip: string]: boolean }; + status: NetworkStatus; + }; + }; + selectedNetworkClientId: string; + pendingApprovals: ApprovalControllerState['pendingApprovals']; + approvalFlows: ApprovalControllerState['approvalFlows']; + knownMethodData?: { + [fourBytePrefix: string]: Record; }; - selectedAccount: string; + gasFeeEstimates: GasFeeEstimates; + gasEstimateType: GasEstimateType; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + custodyAccountDetails?: { [key: string]: any }; + ///: END:ONLY_INCLUDE_IF + internalAccounts: { + accounts: { + [key: string]: InternalAccount; + }; + selectedAccount: string; + }; + keyrings: { type: string; accounts: string[] }[]; }; - keyrings: { type: string; accounts: string[] }[]; -}; type RootReducerReturnType = ReturnType; diff --git a/yarn.lock b/yarn.lock index 4cae5223a04c..4b5ad861cc3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4861,9 +4861,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^37.0.0": - version: 37.0.0 - resolution: "@metamask/assets-controllers@npm:37.0.0" +"@metamask/assets-controllers@npm:38.2.0": + version: 38.2.0 + resolution: "@metamask/assets-controllers@npm:38.2.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4871,12 +4871,12 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^6.0.2" + "@metamask/base-controller": "npm:^7.0.1" "@metamask/contract-metadata": "npm:^2.4.0" - "@metamask/controller-utils": "npm:^11.0.2" + "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^9.0.1" + "@metamask/polling-controller": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/utils": "npm:^9.1.0" "@types/bn.js": "npm:^5.1.5" @@ -4893,9 +4893,47 @@ __metadata: "@metamask/accounts-controller": ^18.0.0 "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 - "@metamask/network-controller": ^20.0.0 + "@metamask/network-controller": ^21.0.0 + "@metamask/preferences-controller": ^13.0.0 + checksum: 10/96ae724a002289e4df97bab568e0bba4d28ef18320298b12d828fc3b58c58ebc54b9f9d659c5e6402aad82088b699e52469d897dd4356e827e35b8f8cebb4483 + 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" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/polling-controller": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/utils": "npm:^9.1.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bn.js: "npm:^5.2.1" + cockatiel: "npm:^3.1.2" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^13.1.0" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/accounts-controller": ^18.0.0 + "@metamask/approval-controller": ^7.0.0 + "@metamask/keyring-controller": ^17.0.0 + "@metamask/network-controller": ^21.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/89798930cb80a134263ce82db736feebd064fe6c999ddcf41ca86fad81cfadbb9e37d1919a6384aaf6d3aa0cb520684e7b8228da3b9bc1e70e7aea174a69c4ac + checksum: 10/0ba3673bf9c87988d6c569a14512b8c9bb97db3516debfedf24cbcf38110e99afec8d9fc50cb0b627bfbc1d1a62069298e4e27278587197f67812cb38ee2c778 languageName: node linkType: hard @@ -5958,6 +5996,22 @@ __metadata: languageName: node linkType: hard +"@metamask/polling-controller@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/polling-controller@npm:10.0.1" + dependencies: + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/utils": "npm:^9.1.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/network-controller": ^21.0.0 + checksum: 10/25c11e65eeccb08a2b4b7dec21ccabb4b797907edb03a1534ebacb87d0754a3ade52aad061aad8b3ac23bfc39917c0d61b9734e32bc748c210b2997410ae45a9 + languageName: node + linkType: hard + "@metamask/polling-controller@npm:^8.0.0": version: 8.0.0 resolution: "@metamask/polling-controller@npm:8.0.0" @@ -5975,22 +6029,6 @@ __metadata: languageName: node linkType: hard -"@metamask/polling-controller@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/polling-controller@npm:9.0.1" - dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^11.0.2" - "@metamask/utils": "npm:^9.1.0" - "@types/uuid": "npm:^8.3.0" - fast-json-stable-stringify: "npm:^2.1.0" - uuid: "npm:^8.3.2" - peerDependencies: - "@metamask/network-controller": ^20.0.0 - checksum: 10/e9e8c51013290a2e4b2817ba1e0915783474f6a55fe614e20acf92bf707e300bec1fa612c8019ae9afe9635d018fb5d5b106c8027446ba12767220db91cf1ee5 - languageName: node - linkType: hard - "@metamask/post-message-stream@npm:^8.0.0, @metamask/post-message-stream@npm:^8.1.1": version: 8.1.1 resolution: "@metamask/post-message-stream@npm:8.1.1" @@ -6028,9 +6066,9 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^0.9.4": - version: 0.9.4 - resolution: "@metamask/profile-sync-controller@npm:0.9.4" +"@metamask/profile-sync-controller@npm:^0.9.7": + version: 0.9.7 + resolution: "@metamask/profile-sync-controller@npm:0.9.7" dependencies: "@metamask/base-controller": "npm:^7.0.1" "@metamask/keyring-api": "npm:^8.1.3" @@ -6046,7 +6084,7 @@ __metadata: "@metamask/accounts-controller": ^18.1.1 "@metamask/keyring-controller": ^17.2.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/86079da552eed316f2754bd899047de1d8d9d15d390c9cdee0aef66b95bea708b5c7929a8d8d946210cc0e4c52347fee971a5cf5130149d0ca60abdc85f47774 + checksum: 10/e53888533b2aae937bbe4e385dca2617c324b34e3e60af218cd98c26d514fb725f4c67b649f126e055f6a50a554817b229d37488115b98d70e8aee7b3a910bde languageName: node linkType: hard @@ -7897,7 +7935,7 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:^8.19.0": +"@sentry/browser@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/browser@npm:8.33.1" dependencies: @@ -7937,14 +7975,14 @@ __metadata: languageName: node linkType: hard -"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.19.0": +"@sentry/types@npm:8.33.1, @sentry/types@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/types@npm:8.33.1" checksum: 10/bcd7f80e84a23cb810fa5819dc85f45bd62d52b01b1f64a1b31297df21e9d1f4de8f7ea91835c5d6a7010d7dbfc8b09cd708d057d345a6ff685b7f12db41ae57 languageName: node linkType: hard -"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.19.0": +"@sentry/utils@npm:8.33.1, @sentry/utils@npm:^8.33.1": version: 8.33.1 resolution: "@sentry/utils@npm:8.33.1" dependencies: @@ -13263,6 +13301,13 @@ __metadata: languageName: node linkType: hard +"base58-js@npm:^1.0.0": + version: 1.0.5 + resolution: "base58-js@npm:1.0.5" + checksum: 10/46c1b39d3a70bca0a47d56069c74a25d547680afd0f28609c90f280f5d614f5de36db5df993fa334db24008a68ab784a72fcdaa13eb40078e03c8999915a1100 + languageName: node + linkType: hard + "base64-arraybuffer-es6@npm:^0.7.0": version: 0.7.0 resolution: "base64-arraybuffer-es6@npm:0.7.0" @@ -13447,6 +13492,17 @@ __metadata: languageName: node linkType: hard +"bitcoin-address-validation@npm:^2.2.3": + version: 2.2.3 + resolution: "bitcoin-address-validation@npm:2.2.3" + dependencies: + base58-js: "npm:^1.0.0" + bech32: "npm:^2.0.0" + sha256-uint8array: "npm:^0.10.3" + checksum: 10/01603b5edf610ecf0843ae546534313f1cffabc8e7435a3678bc9788f18a54e51302218a539794aafd49beb5be70b5d1d507eb7442cb33970fcd665592a71305 + languageName: node + linkType: hard + "bitcoin-ops@npm:^1.3.0, bitcoin-ops@npm:^1.4.1": version: 1.4.1 resolution: "bitcoin-ops@npm:1.4.1" @@ -26041,7 +26097,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": "npm:^37.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/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" @@ -26093,7 +26149,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preinstalled-example-snap": "npm:^0.1.0" - "@metamask/profile-sync-controller": "npm:^0.9.4" + "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" @@ -26125,10 +26181,10 @@ __metadata: "@popperjs/core": "npm:^2.4.0" "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch" "@segment/loosely-validate-event": "npm:^2.0.0" - "@sentry/browser": "npm:^8.19.0" + "@sentry/browser": "npm:^8.33.1" "@sentry/cli": "npm:^2.19.4" - "@sentry/types": "npm:^8.19.0" - "@sentry/utils": "npm:^8.19.0" + "@sentry/types": "npm:^8.33.1" + "@sentry/utils": "npm:^8.33.1" "@storybook/addon-a11y": "npm:^7.6.20" "@storybook/addon-actions": "npm:^7.6.20" "@storybook/addon-designs": "npm:^7.0.9" @@ -26204,6 +26260,7 @@ __metadata: base64-js: "npm:^1.5.1" bify-module-groups: "npm:^2.0.0" bignumber.js: "npm:^4.1.0" + bitcoin-address-validation: "npm:^2.2.3" blo: "npm:1.2.0" bn.js: "npm:^5.2.1" bowser: "npm:^2.11.0" @@ -32839,6 +32896,13 @@ __metadata: languageName: node linkType: hard +"sha256-uint8array@npm:^0.10.3": + version: 0.10.7 + resolution: "sha256-uint8array@npm:0.10.7" + checksum: 10/e427f9d2f9c521dea552f033d3f0c3bd641ab214d214dd41bde3c805edde393519cf982b3eee7d683b32e5f28fa23b2278d25935940e13fbe831b216a37832be + languageName: node + linkType: hard + "shallow-clone@npm:^0.1.2": version: 0.1.2 resolution: "shallow-clone@npm:0.1.2"