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/main.yml b/.github/workflows/main.yml index d14fefe82717..5d1b4d73bdab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,8 +2,15 @@ name: Main on: push: - branches: [develop, master] + branches: + - develop + - master pull_request: + types: + - opened + - reopened + - synchronize + merge_group: jobs: check-workflows: @@ -21,11 +28,25 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + sonarcloud: + name: SonarCloud + uses: ./.github/workflows/sonarcloud.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + needs: + - run-tests + all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows + - run-tests + - sonarcloud outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: @@ -37,7 +58,8 @@ jobs: name: All jobs pass if: ${{ always() }} runs-on: ubuntu-latest - needs: all-jobs-completed + needs: + - all-jobs-completed steps: - name: Check that all jobs have passed run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a0240346af64..3cb7c50e573a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,15 +1,14 @@ name: Run tests on: - push: - branches: - - develop - - master - pull_request: - types: - - opened - - reopened - - synchronize + workflow_call: + outputs: + current-coverage: + description: Current coverage + value: ${{ jobs.report-coverage.outputs.current-coverage }} + stored-coverage: + description: Stored coverage + value: ${{ jobs.report-coverage.outputs.stored-coverage }} jobs: test-unit: @@ -78,18 +77,19 @@ jobs: name: coverage-integration path: coverage/integration/coverage-integration.json - sonarcloud: - name: SonarCloud + report-coverage: + name: Report coverage runs-on: ubuntu-latest needs: - test-unit - test-webpack - test-integration + outputs: + current-coverage: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + stored-coverage: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Setup environment uses: metamask/github-tools/.github/actions/setup-environment@main @@ -108,35 +108,28 @@ jobs: name: lcov.info path: coverage/lcov.info - - name: Get Sonar coverage - id: get-sonar-coverage - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Get current coverage + id: get-current-coverage + run: | + current_coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print int($3)}') + echo "The current coverage is $current_coverage%." + echo 'CURRENT_COVERAGE='"$current_coverage" >> "$GITHUB_OUTPUT" + + - name: Get stored coverage + id: get-stored-coverage run: | - projectKey=$(grep 'sonar.projectKey=' sonar-project.properties | cut -d'=' -f2) - sonar_coverage=$(curl --silent --header "Authorization: Bearer $SONAR_TOKEN" "https://sonarcloud.io/api/measures/component?component=$projectKey&metricKeys=coverage" | jq -r '.component.measures[0].value // 0') - echo "The Sonar coverage of $projectKey is $sonar_coverage%." - echo 'SONAR_COVERAGE='"$sonar_coverage" >> "$GITHUB_OUTPUT" + stored_coverage=$(jq ".coverage" coverage.json) + echo "The stored coverage is $stored_coverage%." + echo 'STORED_COVERAGE='"$stored_coverage" >> "$GITHUB_OUTPUT" - name: Validate test coverage env: - SONAR_COVERAGE: ${{ steps.get-sonar-coverage.outputs.SONAR_COVERAGE }} + CURRENT_COVERAGE: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + STORED_COVERAGE: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} run: | - coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print $3}') - if [ -z "$coverage" ]; then - echo "::error::Could not retrieve test coverage." - exit 1 - fi - if (( $(echo "$coverage < $SONAR_COVERAGE" | bc -l) )); then - echo "::error::Quality gate failed for test coverage. Current test coverage is $coverage%, please increase coverage to at least $SONAR_COVERAGE%." + if (( $(echo "$CURRENT_COVERAGE < $STORED_COVERAGE" | bc -l) )); then + echo "::error::Quality gate failed for test coverage. Current coverage is $CURRENT_COVERAGE%, please increase coverage to at least $STORED_COVERAGE%." exit 1 else - echo "Test coverage is $coverage%. Quality gate passed." + echo "The current coverage is $CURRENT_COVERAGE%, stored coverage is $STORED_COVERAGE%. Quality gate passed." fi - - - name: SonarCloud Scan - # This is SonarSource/sonarcloud-github-action@v2.0.0 - uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000000..460d5c140462 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,30 @@ +name: SonarCloud + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: lcov.info + path: coverage + + - name: SonarCloud Scan + # This is SonarSource/sonarcloud-github-action@v2.0.0 + uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml new file mode 100644 index 000000000000..fd1b0d5134e3 --- /dev/null +++ b/.github/workflows/update-coverage.yml @@ -0,0 +1,46 @@ +name: Update coverage + +on: + schedule: + # Once per day at midnight UTC + - cron: 0 0 * * * + workflow_dispatch: + +jobs: + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + update-coverage: + if: ${{ needs.run-tests.outputs.current-coverage > needs.run-tests.outputs.stored-coverage }} + name: Update coverage + runs-on: ubuntu-latest + needs: + - run-tests + env: + CURRENT_COVERAGE: ${{ needs.run-tests.outputs.current-coverage }} + STORED_COVERAGE: ${{ needs.run-tests.outputs.stored-coverage }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + + - name: Update coverage + run: | + echo "{ \"coverage\": $CURRENT_COVERAGE }" > coverage.json + + - name: Checkout/create branch, commit, and force push + run: | + 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" + git push -f origin metamaskbot/update-coverage + + - name: Create/update pull request + env: + 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/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml index 1baef7fb4460..c8f9c190e533 100644 --- a/.github/workflows/update-lavamoat-policies.yml +++ b/.github/workflows/update-lavamoat-policies.yml @@ -201,7 +201,7 @@ jobs: run: | if [[ $HAS_CHANGES == 'true' ]] then - gh pr comment "${PR_NUMBER}" --body 'Policies updated' + echo -e 'Policies updated. \n👀 Please review the diff for suspicious new powers. \n\n🧠 Learn how: https://lavamoat.github.io/guides/policy-diff/#what-to-look-for-when-reviewing-a-policy-diff' | gh pr comment "${PR_NUMBER}" --body-file - else gh pr comment "${PR_NUMBER}" --body 'No policy changes' fi diff --git a/.metamaskrc.dist b/.metamaskrc.dist index 601105e2af44..fc2a5a831a4b 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -45,3 +45,7 @@ BLOCKAID_PUBLIC_KEY= ; Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render ; This should NEVER be enabled in production since it slows down react ; ENABLE_WHY_DID_YOU_RENDER=false + +; API key used in Etherscan requests to prevent rate limiting. +; Only applies to Mainnet and Sepolia. +; ETHERSCAN_API_KEY= diff --git a/.prettierignore b/.prettierignore index 9c4b3868464b..d8d8cfe4a15c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,8 @@ *.scss .nyc_output/**/* node_modules/**/* +# Exclude lottie json files +/app/images/animations/**/*.json /app/vendor/** /builds/**/* /coverage/**/* diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 72a9bc3b78aa..cbcebb6347ed 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -676,7 +676,12 @@ const state = { welcomeScreenSeen: false, currentLocale: 'en', preferences: { - useNativeCurrencyAsPrimaryCurrency: true, + 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 42641c2e31ae..2c93419d22fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,108 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [12.5.0] ## [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 -- fix: flaky test `Test Snap Interactive UI test interactive ui elements` ([#26792](https://github.com/MetaMask/metamask-extension/pull/26792)) -- feat: Update Polygon from `MATIC` to `POL` ([#26671](https://github.com/MetaMask/metamask-extension/pull/26671)) -- feat: implement client side malicious network request detection ([#25839](https://github.com/MetaMask/metamask-extension/pull/25839)) -- fix: Improve migration 121.1 state validation ([#26773](https://github.com/MetaMask/metamask-extension/pull/26773)) -- chore: Bump Snaps dependencies ([#26675](https://github.com/MetaMask/metamask-extension/pull/26675)) -- refactor: extract Send-specific functionality out of AssetPicker ([#26558](https://github.com/MetaMask/metamask-extension/pull/26558)) -- fix: rename migration 126 to 121.1 ([#26742](https://github.com/MetaMask/metamask-extension/pull/26742)) -- chore: Bump `storybook`, `@storybook/*` to `^7.6.20`, `storybook-dark-mode` from `^3.0.3` to `^4.0.2` ([#26703](https://github.com/MetaMask/metamask-extension/pull/26703)) -- fix: Sentry app state null data to show null as value. ([#26522](https://github.com/MetaMask/metamask-extension/pull/26522)) -- chore: Master sync ([#26737](https://github.com/MetaMask/metamask-extension/pull/26737)) -- fix: Stop using a hardcoded Snap ID for notifications ([#26739](https://github.com/MetaMask/metamask-extension/pull/26739)) -- chore: MMI Fixes passing the state to route using history.push ([#26722](https://github.com/MetaMask/metamask-extension/pull/26722)) -- Merge origin/develop into master-sync -- test: Add integration test for insufficient gas ([#26711](https://github.com/MetaMask/metamask-extension/pull/26711)) -- test: [Snaps E2E] Add changes to fix flakiness in Snaps UI Images test ([#26725](https://github.com/MetaMask/metamask-extension/pull/26725)) -- test: Add integration test for gas estimate failed alert ([#26681](https://github.com/MetaMask/metamask-extension/pull/26681)) -- fix: flaky test `Click bridge button @no-mmi loads portfolio tab from asset overview when flag is turned off` ([#26654](https://github.com/MetaMask/metamask-extension/pull/26654)) -- fix: flaky test `Navigation Signature - Different signature types initiates and queues multiple signatures and confirms` ([#26707](https://github.com/MetaMask/metamask-extension/pull/26707)) -- feat: adding context to get current confirmation in re-designed confirmation pages PR-1 ([#26587](https://github.com/MetaMask/metamask-extension/pull/26587)) -- fix: `wallet_addEthereumChain` does not attach a `result` under certain conditions ([#26726](https://github.com/MetaMask/metamask-extension/pull/26726)) -- fix: Add IOTX icon ([#26723](https://github.com/MetaMask/metamask-extension/pull/26723)) -- test: Add integration tests for network busy alert ([#26679](https://github.com/MetaMask/metamask-extension/pull/26679)) -- feat: Temporarily hide Approve redesigned pages ([#26676](https://github.com/MetaMask/metamask-extension/pull/26676)) -- perf: use an interstitial page to load `popup.html`; load scripts using `defer`ed script tags ([#26555](https://github.com/MetaMask/metamask-extension/pull/26555)) -- feat: Add metrics to track where signature rejection occurred ([#26469](https://github.com/MetaMask/metamask-extension/pull/26469)) -- chore: update @metamask/bitcoin-wallet-snap to 0.5.0 ([#26701](https://github.com/MetaMask/metamask-extension/pull/26701)) -- fix: adding missing token images ([#26708](https://github.com/MetaMask/metamask-extension/pull/26708)) -- feat: Added Edit networks screen modal ([#26097](https://github.com/MetaMask/metamask-extension/pull/26097)) -- perf: add trace for UI startup ([#26636](https://github.com/MetaMask/metamask-extension/pull/26636)) -- fix: Address design review on contract interaction and deployment red… ([#26659](https://github.com/MetaMask/metamask-extension/pull/26659)) -- test: [Snaps E2E] Add test cases for signature confirmations redesign to signature insights snaps test ([#26691](https://github.com/MetaMask/metamask-extension/pull/26691)) -- feat: updated ui for adding chain id screen ([#25777](https://github.com/MetaMask/metamask-extension/pull/25777)) -- fix: update moonbeam and moonriver network and token logos ([#26677](https://github.com/MetaMask/metamask-extension/pull/26677)) -- chore: MMI adds back the current Tx confirmation view to MMI ([#26539](https://github.com/MetaMask/metamask-extension/pull/26539)) -- fix(snaps): Use ApprovalType instead DIALOG_APPROVAL_TYPES in confirmation page ([#26655](https://github.com/MetaMask/metamask-extension/pull/26655)) -- fix: catch error for getTokenStandardAndDetails ([#26269](https://github.com/MetaMask/metamask-extension/pull/26269)) -- chore: Master sync ([#26641](https://github.com/MetaMask/metamask-extension/pull/26641)) -- chore: update gitignore ([#26642](https://github.com/MetaMask/metamask-extension/pull/26642)) -- fix: flaky test `Phishing Detection should navigate the user to PhishFort to dispute a Phishfort Block` ([#26651](https://github.com/MetaMask/metamask-extension/pull/26651)) -- fix: flaky tests `Sentry errors before initialization, after opting into metrics @no-mmi should capture UI application state`... ([#26648](https://github.com/MetaMask/metamask-extension/pull/26648)) -- fix: flaky test `Vault Decryptor Page is able to decrypt the vault us..` due to empty file load ([#26612](https://github.com/MetaMask/metamask-extension/pull/26612)) -- Merge branch 'develop' into master-sync -- fix: flaky test `Increase Token Allowance increases token spending ca..` ([#26640](https://github.com/MetaMask/metamask-extension/pull/26640)) -- chore: bump smart transactions controller ([#26644](https://github.com/MetaMask/metamask-extension/pull/26644)) -- chore: Polish multichain token list styles ([#26300](https://github.com/MetaMask/metamask-extension/pull/26300)) -- Merge origin/develop into master-sync -- feat: upgrade network controller to v20 ([#26150](https://github.com/MetaMask/metamask-extension/pull/26150)) -- docs: Add publish a release to Sentry flow steps ([#26605](https://github.com/MetaMask/metamask-extension/pull/26605)) -- chore: set bridge network allowlists from feature flags ([#26147](https://github.com/MetaMask/metamask-extension/pull/26147)) -- chore: anonymize send analytic properties #26627 ([#26628](https://github.com/MetaMask/metamask-extension/pull/26628)) -- chore: add user IDs to send page analytics ([#26600](https://github.com/MetaMask/metamask-extension/pull/26600)) -- fix: bump accounts controller and migration to fix undefined selectedAccount ([#26573](https://github.com/MetaMask/metamask-extension/pull/26573)) -- feat: Integrate Snaps into the redesigned confirmations ([#26435](https://github.com/MetaMask/metamask-extension/pull/26435)) -- refactor: Replace usages of the deprecated `setProviderType` ([#22619](https://github.com/MetaMask/metamask-extension/pull/22619)) -- refactor: Use generic helper function to initiate signatures ([#26584](https://github.com/MetaMask/metamask-extension/pull/26584)) -- test: [Snaps E2E] Update snaps dialog test to include Custom dialog type ([#26598](https://github.com/MetaMask/metamask-extension/pull/26598)) -- feat: new receive flow ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148)) -- fix: remove speed up and cancel controller validation ([#26492](https://github.com/MetaMask/metamask-extension/pull/26492)) -- fix: flaky test `Test Snap Name Lookup tests name-lookup functionalit...` ([#26583](https://github.com/MetaMask/metamask-extension/pull/26583)) -- feat: Add contract deployment redesigned transaction screen ([#26382](https://github.com/MetaMask/metamask-extension/pull/26382)) -- feat: add transaction performance metrics ([#26332](https://github.com/MetaMask/metamask-extension/pull/26332)) -- test: add tests for insufficient funds alert ([#26512](https://github.com/MetaMask/metamask-extension/pull/26512)) -- feat: account watcher e2e ([#26524](https://github.com/MetaMask/metamask-extension/pull/26524)) -- feat: update add team label workflow ([#26548](https://github.com/MetaMask/metamask-extension/pull/26548)) -- feat: Add approval static simulation ([#26514](https://github.com/MetaMask/metamask-extension/pull/26514)) -- fix: Snapshot unit tests ([#26585](https://github.com/MetaMask/metamask-extension/pull/26585)) -- chore: Rename `permittedChains` permission to `endowment:permitted-chains` ([#26534](https://github.com/MetaMask/metamask-extension/pull/26534)) -- feat: Redesign Approve confirmation ([#26464](https://github.com/MetaMask/metamask-extension/pull/26464)) -- feat: Enable hardware wallets for smart transactions, sign a transaction only once ([#26251](https://github.com/MetaMask/metamask-extension/pull/26251)) -- fix: Allowlist Snap UI card component ([#26565](https://github.com/MetaMask/metamask-extension/pull/26565)) -- fix(deps): Bump `@metamask/eth-json-rpc-middleware` to `^14.0.0`, `@metamask/transaction-controller` to `^35.1.1` ([#26143](https://github.com/MetaMask/metamask-extension/pull/26143)) -- fix: adding warning for origin on redesigned pages ([#26306](https://github.com/MetaMask/metamask-extension/pull/26306)) -- fix: track `swapAndSend` transaction type ([#26535](https://github.com/MetaMask/metamask-extension/pull/26535)) -- feat: added AccountWatcher as preinstalled snap and added to menu list ([#26402](https://github.com/MetaMask/metamask-extension/pull/26402)) -- fix: stick add team label version to commit hash ([#26540](https://github.com/MetaMask/metamask-extension/pull/26540)) -- fix: correct duplicate notifications event tracking in global menu ([#26525](https://github.com/MetaMask/metamask-extension/pull/26525)) -- feat: migrate protect intrinsics test to e2e ([#26197](https://github.com/MetaMask/metamask-extension/pull/26197)) -- fix: NetworkChangeToast width in wide screen mode ([#26532](https://github.com/MetaMask/metamask-extension/pull/26532)) -- fix: missing deadline in swaps stx status screen ([#25779](https://github.com/MetaMask/metamask-extension/pull/25779)) -- fix: Snap Address component UI/UX (Snaps custom UI) ([#26477](https://github.com/MetaMask/metamask-extension/pull/26477)) -- feat(snaps): Removed Snaps name-lookup permission code fences ([#26393](https://github.com/MetaMask/metamask-extension/pull/26393)) -- docs: Include MV2 build commands in README ([#26486](https://github.com/MetaMask/metamask-extension/pull/26486)) -- test: add `driver.clickElementAndWaitForWindowToClose` helper method ([#26449](https://github.com/MetaMask/metamask-extension/pull/26449)) -- chore: Integrate SnapInsightsController ([#26411](https://github.com/MetaMask/metamask-extension/pull/26411)) -- feat: Update @blockaid/ppom_release to release 1.5.2 ([#26494](https://github.com/MetaMask/metamask-extension/pull/26494)) -- chore: Master sync ([#26497](https://github.com/MetaMask/metamask-extension/pull/26497)) -- Merge origin/develop into master-sync -- feat(notifications): use shared libraries NotificationServicesController ([#26480](https://github.com/MetaMask/metamask-extension/pull/26480)) -- perf: add parallel fetching for the network fee dropdown ([#26489](https://github.com/MetaMask/metamask-extension/pull/26489)) -- chore: remove token and nft detection modals ([#26403](https://github.com/MetaMask/metamask-extension/pull/26403)) -- chore: Add Near Icon ([#26459](https://github.com/MetaMask/metamask-extension/pull/26459)) -- fix: Restore `responsive` e2e driver option ([#25932](https://github.com/MetaMask/metamask-extension/pull/25932)) -- chore: downgrade prettier-eslint to match prettier version ([#26145](https://github.com/MetaMask/metamask-extension/pull/26145)) -- test: Add manual scenario for upgrade testing ([#26317](https://github.com/MetaMask/metamask-extension/pull/26317)) -- build(chore): switch to `defer` since it guarantees execution order once chunked ([#26425](https://github.com/MetaMask/metamask-extension/pull/26425)) -- fix: Update send transactions with custom nonce.csv ([#26451](https://github.com/MetaMask/metamask-extension/pull/26451)) -- fix: `rpcIdentifierUtility` client side grouping before emitting CustomRPC event ([#26266](https://github.com/MetaMask/metamask-extension/pull/26266)) -- feat(notifications): use notification services push controller ([#26448](https://github.com/MetaMask/metamask-extension/pull/26448)) -- feat: Add footers to Snap home pages ([#26463](https://github.com/MetaMask/metamask-extension/pull/26463)) -- fix: Remove double padding on Snap home page ([#26462](https://github.com/MetaMask/metamask-extension/pull/26462)) -- chore(webpack): update `html-bundler-webpack-plugin` from `v3.6.5` to `v3.17.3` ([#26371](https://github.com/MetaMask/metamask-extension/pull/26371)) +- 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 @@ -154,368 +77,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the AccountListMenu to hide the back button by default, showing it only when needed ([#27152](https://github.com/MetaMask/metamask-extension/pull/27152)) ### Fixed -- Merge branch 'Version-v12.2.0' into Version-v12.3.0 -- Merge remote-tracking branch 'origin/master' into Version-v12.2.0 -- CherryPick: "fix: issue where `wallet_addEtherumChain` was incorrectly enforcing inclusion of a blockExplorerUrls property which is not required (#26938)" ([#26938](https://github.com/MetaMask/metamask-extension/pull/26938)) -- fix: update notifications events (#26807) ([#26807](https://github.com/MetaMask/metamask-extension/pull/26807)) -- Update v12.2.0 with changes from v12.1.2 ([#26895](https://github.com/MetaMask/metamask-extension/pull/26895)) -- Merge remote-tracking branch 'origin/master' into sync-v12.1.2 -- perf(cherry-pick): use an interstitial page to load `popup.html`; load scripts using `defer`ed script tags (#26555) ([#26555](https://github.com/MetaMask/metamask-extension/pull/26555)) -- v12.2.0 sync v12.1.1 ([#26842](https://github.com/MetaMask/metamask-extension/pull/26842)) -- Merge branch 'Version-v12.2.0' into v12.2.0-sync-v12.1.1 -- ci: Prevent E2E timeouts on release changes (#26846) [cherry-pick] ([#26846](https://github.com/MetaMask/metamask-extension/pull/26846)) -- Fix type error -- Fix changelog merge conflicts -- Update LavaMoat policies -- Run yarn dedupe -- Update LavaMoat policies -- Merge branch 'Version-v12.2.0' into v12.2.0-sync-v12.1.1 -- chore: MMI adds cherry pick for PR 25967 ([#26736](https://github.com/MetaMask/metamask-extension/pull/26736)) -- Merge remote-tracking branch 'origin/Version-v12.2.0' into v12.2.0-sync-v12.1.1 -- Merge remote-tracking branch 'origin/master' into v12.2.0-sync-v12.1.1 -- fix(cherry-pick): remove BTC accounts from send flow (#26271) ([#26271](https://github.com/MetaMask/metamask-extension/pull/26271)) -- feat(cherry-pick): support creation of Bitcoin testnet accounts (#25772) ([#25772](https://github.com/MetaMask/metamask-extension/pull/25772)) -- fix(cherry-pick): remove btc account from permission connect lists (#25980) ([#25980](https://github.com/MetaMask/metamask-extension/pull/25980)) -- fix(cherry-pick): remove submitRequest from dapp permission (#26319) ([#26319](https://github.com/MetaMask/metamask-extension/pull/26319)) -- chore: update @metamask/bitcoin-wallet-snap to 0.5.0 ([#26701](https://github.com/MetaMask/metamask-extension/pull/26701)) -- chore: update @metamask/bitcoin-wallet-snap to 0.4.0 ([#26229](https://github.com/MetaMask/metamask-extension/pull/26229)) -- chore: update @metamask/bitcoin-wallet-snap to 0.3.0 ([#26168](https://github.com/MetaMask/metamask-extension/pull/26168)) -- chore: update Bitcoin Snap to version 0.2.5 ([#26058](https://github.com/MetaMask/metamask-extension/pull/26058)) -- fix(multichain): use accounts{Added,Removed} to fetch/clear balances ([#25884](https://github.com/MetaMask/metamask-extension/pull/25884)) -- feat: add BTC support survey link ([#25875](https://github.com/MetaMask/metamask-extension/pull/25875)) -- Cherrypick flaky test fix 12.2.0 ([#26747](https://github.com/MetaMask/metamask-extension/pull/26747)) -- chore: cherry pick remove token and nft detection modals (#26403) ([#26403](https://github.com/MetaMask/metamask-extension/pull/26403)) -- Synchronize v12.2.0 RC with v12.1.0 ([#26729](https://github.com/MetaMask/metamask-extension/pull/26729)) -- Merge remote-tracking branch 'origin/master' into v12.2.0-sync-master -- v12.2.0 sync with v12.1.0 ([#26695](https://github.com/MetaMask/metamask-extension/pull/26695)) -- Fix Sentry state merge conflict error -- fix cherry-pick test: UX: Multichain: Add E2E for signaling network change from Netwo… ([#26704](https://github.com/MetaMask/metamask-extension/pull/26704)) -- test: Removed step from e2e tests ([#25910](https://github.com/MetaMask/metamask-extension/pull/25910)) -- Update LavaMoat policies -- Resolve changelog conflicts -- DResolve audit advisory -- Merge remote-tracking branch 'origin/Version-v12.1.0' into v12.2.0-sync-with-v12.1.0 -- Patch fix for initial connections in preinstalled Snaps ([#26602](https://github.com/MetaMask/metamask-extension/pull/26602)) -- add version in changelog/package.json files ([#25766](https://github.com/MetaMask/metamask-extension/pull/25766)) -- chore: Master sync ([#26395](https://github.com/MetaMask/metamask-extension/pull/26395)) -- chore: Bump Snaps packages ([#26086](https://github.com/MetaMask/metamask-extension/pull/26086)) -- fix: Improve AccountListMenu/Item performance ([#26379](https://github.com/MetaMask/metamask-extension/pull/26379)) -- fix: Codespaces `corepack enable` ([#25161](https://github.com/MetaMask/metamask-extension/pull/25161)) -- fix: display toast message if user quickly sends transaction on different networks ([#26114](https://github.com/MetaMask/metamask-extension/pull/26114)) -- fix: problem with origins in the Snaps permission UI ([#26422](https://github.com/MetaMask/metamask-extension/pull/26422)) -- feat: Add abstraction for Snaps permissions ([#25175](https://github.com/MetaMask/metamask-extension/pull/25175)) -- test: add transaction contract interaction integration tests ([#26272](https://github.com/MetaMask/metamask-extension/pull/26272)) -- fix: timeout and "Rerun failed tests" ([#26239](https://github.com/MetaMask/metamask-extension/pull/26239)) -- chore: migrate BridgeController to BaseController v2 ([#26109](https://github.com/MetaMask/metamask-extension/pull/26109)) -- feat: Enable why did you render ([#26339](https://github.com/MetaMask/metamask-extension/pull/26339)) -- fix: Delete invalid `SelectedNetworkController` state ([#26428](https://github.com/MetaMask/metamask-extension/pull/26428)) -- test: ensure bridge button handles clicks according to feature flags ([#25812](https://github.com/MetaMask/metamask-extension/pull/25812)) -- build(webpack): polyfill `setImmediate` ([#26398](https://github.com/MetaMask/metamask-extension/pull/26398)) -- feat: feature-flagged cross-chain swaps route [METABRIDGE-867] ([#25811](https://github.com/MetaMask/metamask-extension/pull/25811)) -- chore: Remove i18n translations from Developer Options Settings Page ([#26380](https://github.com/MetaMask/metamask-extension/pull/26380)) -- fix: Do not break application if no token details are found using getTokenStandardAndDetails ([#26324](https://github.com/MetaMask/metamask-extension/pull/26324)) -- fix: Flaky contract interaction test ([#26420](https://github.com/MetaMask/metamask-extension/pull/26420)) -- fix: Enter key on Create Account checkbox should not trigger show/hide ([#26394](https://github.com/MetaMask/metamask-extension/pull/26394)) -- fix: notifications use better events ([#26410](https://github.com/MetaMask/metamask-extension/pull/26410)) -- fix: Restore snaps-controllers version following patch ([#26412](https://github.com/MetaMask/metamask-extension/pull/26412)) -- fix: Improve hex copy button ([#26384](https://github.com/MetaMask/metamask-extension/pull/26384)) -- refactor: use core profile syncing controllers. ([#26370](https://github.com/MetaMask/metamask-extension/pull/26370)) -- test: snap account contract interaction ([#26234](https://github.com/MetaMask/metamask-extension/pull/26234)) -- feat: updated SSK version in e2e and added test for creating multiple… ([#26378](https://github.com/MetaMask/metamask-extension/pull/26378)) -- Merge origin/develop into master-sync -- chore: MMI move duck and selector to TS ([#26125](https://github.com/MetaMask/metamask-extension/pull/26125)) -- refactor(notifications): use contentful package as dev dependency ([#26381](https://github.com/MetaMask/metamask-extension/pull/26381)) -- fix: remove submitRequest from dapp permission ([#26319](https://github.com/MetaMask/metamask-extension/pull/26319)) -- feat: Add integration test for blockaid on contract interaction ([#26366](https://github.com/MetaMask/metamask-extension/pull/26366)) -- refactor: add performance tracing infrastructure ([#26044](https://github.com/MetaMask/metamask-extension/pull/26044)) -- refactor: replace deprecated mixins with Text component in slippage-buttons ([#25638](https://github.com/MetaMask/metamask-extension/pull/25638)) -- refactor: replace deprecated mixins with text component in loading-swaps-quotes ([#25553](https://github.com/MetaMask/metamask-extension/pull/25553)) -- feat: Add metrics for alerts (transactions redesign) ([#26121](https://github.com/MetaMask/metamask-extension/pull/26121)) -- fix(25350): fix flakey token importing e2e test ([#26351](https://github.com/MetaMask/metamask-extension/pull/26351)) -- fix: enable Save button on Add Contact page for address input ([#26155](https://github.com/MetaMask/metamask-extension/pull/26155)) -- test: Add test for migration 120.2 and fix docs ([#26333](https://github.com/MetaMask/metamask-extension/pull/26333)) -- chore: normalize separator in `content` on the `viewport` `meta` tag ([#26268](https://github.com/MetaMask/metamask-extension/pull/26268)) -- fix: Stop logging pipeline stream errors in the service worker if they match 'Premature close' ([#26336](https://github.com/MetaMask/metamask-extension/pull/26336)) -- build: add alternative build process to enable faster developer builds ([#22506](https://github.com/MetaMask/metamask-extension/pull/22506)) -- fix: issue where `setNetworkClientIdForDomain` was called without checking whether the origin was eligible for setting its own network ([#26323](https://github.com/MetaMask/metamask-extension/pull/26323)) -- fix: get permit and order signatures token decimals ([#26292](https://github.com/MetaMask/metamask-extension/pull/26292)) -- feat: Update Redesign Signature Permit to show ellipsis at max 15 digits ([#26227](https://github.com/MetaMask/metamask-extension/pull/26227)) -- fix: remove the ability to send to btc accounts in send page ([#26271](https://github.com/MetaMask/metamask-extension/pull/26271)) -- fix: Adding migration 125 to remove Deprecated TxController Key from state ([#26267](https://github.com/MetaMask/metamask-extension/pull/26267)) -- fix: Revert "fix: remove submitRequest from dapp permission" ([#26293](https://github.com/MetaMask/metamask-extension/pull/26293)) -- refactor: convert `icon-factory.js` to typescript ([#23823](https://github.com/MetaMask/metamask-extension/pull/23823)) -- fix(26065): remove persisted state mostRecentRetrievedState after initialization if no errors ([#26206](https://github.com/MetaMask/metamask-extension/pull/26206)) -- refactor: ENABLE_MV3 flag cleanup ([#26059](https://github.com/MetaMask/metamask-extension/pull/26059)) -- test: fix flaky test Import flow @no-mmi Import wallet using Secret Recovery Phrase ([#26275](https://github.com/MetaMask/metamask-extension/pull/26275)) -- chore: Fully remove `eth_sign` ([#24756](https://github.com/MetaMask/metamask-extension/pull/24756)) -- fix: remove submitRequest from dapp permission ([#26276](https://github.com/MetaMask/metamask-extension/pull/26276)) -- chore: Update `actions/cache` from v3 to v4 ([#26020](https://github.com/MetaMask/metamask-extension/pull/26020)) -- feat: QR-based add NGRAVE ZERO Hardware ([#25080](https://github.com/MetaMask/metamask-extension/pull/25080)) -- fix: Fix GitHub release description ([#26247](https://github.com/MetaMask/metamask-extension/pull/26247)) -- feat(btc): use new snap account flow for Bitcoin accounts ([#26183](https://github.com/MetaMask/metamask-extension/pull/26183)) -- refactor: replace deprecated mixins with text component in transaction-confirmed ([#25551](https://github.com/MetaMask/metamask-extension/pull/25551)) -- fix: improve warning in add network modal ([#26250](https://github.com/MetaMask/metamask-extension/pull/26250)) -- fix: Fix `create_release_pull_request` OOM error ([#26249](https://github.com/MetaMask/metamask-extension/pull/26249)) -- chore: Create a story for TokenCurrencyDisplay component ([#26172](https://github.com/MetaMask/metamask-extension/pull/26172)) -- chore: refactoring onboarding to remove deprecated components ([#26207](https://github.com/MetaMask/metamask-extension/pull/26207)) -- fix: Fix CircleCI `create_release_pull_request` job ([#26246](https://github.com/MetaMask/metamask-extension/pull/26246)) -- fix: flaky test `Import flow @no-mmi Import Account using json file` ([#26240](https://github.com/MetaMask/metamask-extension/pull/26240)) -- fix: sentry sessions ([#26192](https://github.com/MetaMask/metamask-extension/pull/26192)) -- test: [Page Object Model] rename process to flow ([#26228](https://github.com/MetaMask/metamask-extension/pull/26228)) -- test: header integration test for contract interaction ([#25981](https://github.com/MetaMask/metamask-extension/pull/25981)) -- chore: Pass along hashed `rpcUrl` during `CustomNetworkAdded` event ([#26203](https://github.com/MetaMask/metamask-extension/pull/26203)) -- chore: Create a story for PageContainerHeader component ([#26031](https://github.com/MetaMask/metamask-extension/pull/26031)) -- chore: Create a story for GasTiming component ([#25557](https://github.com/MetaMask/metamask-extension/pull/25557)) -- New Crowdin translations by Github Action ([#26230](https://github.com/MetaMask/metamask-extension/pull/26230)) -- chore: update @metamask/bitcoin-wallet-snap to 0.4.0 ([#26229](https://github.com/MetaMask/metamask-extension/pull/26229)) -- fix: flaky test `Sentry errors before initialization, after opting into metrics @no-mmi should send error events in background` ([#26216](https://github.com/MetaMask/metamask-extension/pull/26216)) -- chore: Create a story for NftCollectionImage component ([#26069](https://github.com/MetaMask/metamask-extension/pull/26069)) -- fix: update icons ([#26180](https://github.com/MetaMask/metamask-extension/pull/26180)) -- refactor: replace Typography with Text component in restore-vault.js ([#25636](https://github.com/MetaMask/metamask-extension/pull/25636)) -- chore: Create a story for convert-token-to-nft-modal component ([#25561](https://github.com/MetaMask/metamask-extension/pull/25561)) -- refactor: replace deprecated mixins with Text component in qr-code-view ([#25637](https://github.com/MetaMask/metamask-extension/pull/25637)) -- test: Add manual scenario for network polling scenario ([#26195](https://github.com/MetaMask/metamask-extension/pull/26195)) -- feat: Add experimental settings toggle for transactions redesign ([#26010](https://github.com/MetaMask/metamask-extension/pull/26010)) -- feat: Support Permit variants: PermitSingle, PermitBatch, PermitTransferFrom, PermitBatchTransferFrom, TradeOrder, Seaport ([#26107](https://github.com/MetaMask/metamask-extension/pull/26107)) -- feat: updated dapp permission screen ([#25703](https://github.com/MetaMask/metamask-extension/pull/25703)) -- fix: improve performance in large signature request confirmations ([#26209](https://github.com/MetaMask/metamask-extension/pull/26209)) -- refactor: remove password manager mention ([#25985](https://github.com/MetaMask/metamask-extension/pull/25985)) -- New Crowdin translations by Github Action ([#25939](https://github.com/MetaMask/metamask-extension/pull/25939)) -- chore: remove opera manifest files as they are not used ([#26200](https://github.com/MetaMask/metamask-extension/pull/26200)) -- fix(deps): bump fast-xml-parser from 4.3.4 to 4.4.1. ([#26202](https://github.com/MetaMask/metamask-extension/pull/26202)) -- test: [Snaps E2E] remove unnecessary steps from snaps UI Images test ([#25640](https://github.com/MetaMask/metamask-extension/pull/25640)) -- fix: truncate long tokenId ([#26179](https://github.com/MetaMask/metamask-extension/pull/26179)) -- chore: Add en_GB locale ([#26196](https://github.com/MetaMask/metamask-extension/pull/26196)) -- chore: upgrade to Sentry 8 ([#25999](https://github.com/MetaMask/metamask-extension/pull/25999)) -- refactor: add unlock checks for notification related controllers ([#26189](https://github.com/MetaMask/metamask-extension/pull/26189)) -- fix: interpret multipart errors correctly and allow ignore ([#26113](https://github.com/MetaMask/metamask-extension/pull/26113)) -- feat: migrate global unit tests from Mocha to Jest ([#26104](https://github.com/MetaMask/metamask-extension/pull/26104)) -- fix: node being setup twice ([#26052](https://github.com/MetaMask/metamask-extension/pull/26052)) -- fix: setupControllerConnection outstream end event listener ([#26141](https://github.com/MetaMask/metamask-extension/pull/26141)) -- fix: Address performance issues with 'Portfolio Dashboard' loading in test environment ([#26182](https://github.com/MetaMask/metamask-extension/pull/26182)) -- chore: migrating interactive-replacement-token-page to ts ([#26115](https://github.com/MetaMask/metamask-extension/pull/26115)) -- feat: (cherry-pick)(Version v12.2.0) Migration #122 set redesignedConfirmationsEnabled to true ([#26139](https://github.com/MetaMask/metamask-extension/pull/26139)) -- chore: update @metamask/bitcoin-wallet-snap to 0.3.0 ([#26168](https://github.com/MetaMask/metamask-extension/pull/26168)) -- test: fix potential api-spec test race condition when adding to task queue ([#26171](https://github.com/MetaMask/metamask-extension/pull/26171)) -- fix(user-preference-currency-display): remove unused prop ethLogoHeight ([#24517](https://github.com/MetaMask/metamask-extension/pull/24517)) -- fix: update logos for flare-mainnet and songbird ([#25560](https://github.com/MetaMask/metamask-extension/pull/25560)) -- feat: define account name during creation ([#25191](https://github.com/MetaMask/metamask-extension/pull/25191)) -- chore: MMI move custody component to TS ([#26096](https://github.com/MetaMask/metamask-extension/pull/26096)) -- chore: add portfolio ephemeral domain URL ([#26163](https://github.com/MetaMask/metamask-extension/pull/26163)) -- fix: Flaky test `4byte setting ` ([#26111](https://github.com/MetaMask/metamask-extension/pull/26111)) -- fix: PPOM blockaid update ([#26154](https://github.com/MetaMask/metamask-extension/pull/26154)) -- fix: flaky BTC e2e tests ([#26082](https://github.com/MetaMask/metamask-extension/pull/26082)) -- chore: Add extra event props ([#26123](https://github.com/MetaMask/metamask-extension/pull/26123)) -- refactor: fix event names used to track notifications ([#25521](https://github.com/MetaMask/metamask-extension/pull/25521)) -- test: [Snaps E2E] Create test for snap dialog JSX functionality ([#25493](https://github.com/MetaMask/metamask-extension/pull/25493)) -- chore: update BNB logos ([#26140](https://github.com/MetaMask/metamask-extension/pull/26140)) -- chore: cleanup `.prettierignore` file ([#24828](https://github.com/MetaMask/metamask-extension/pull/24828)) -- chore: Bump `@metamask/ens-controller` to v12 ([#26127](https://github.com/MetaMask/metamask-extension/pull/26127)) -- chore: Bump `@metamask/transaction-controller` to v34 ([#26124](https://github.com/MetaMask/metamask-extension/pull/26124)) -- chore: Create a story for Snackbar component ([#25515](https://github.com/MetaMask/metamask-extension/pull/25515)) -- fix: add new helper function for `openMenuSafe` to mitigate all ocurrences for opening menu with MMI build ([#26079](https://github.com/MetaMask/metamask-extension/pull/26079)) -- feat: make add-team-label use the reusable workflow ([#25807](https://github.com/MetaMask/metamask-extension/pull/25807)) -- chore: MMI-5301 adds enums for custody type and status ([#26006](https://github.com/MetaMask/metamask-extension/pull/26006)) -- fix: enable siwe redesign ([#26136](https://github.com/MetaMask/metamask-extension/pull/26136)) -- chore: cleanup `.prettierignore` file ([#24828](https://github.com/MetaMask/metamask-extension/pull/24828)) -- chore: Bump `@metamask/ens-controller` to v12 ([#26127](https://github.com/MetaMask/metamask-extension/pull/26127)) -- chore: Bump `@metamask/transaction-controller` to v34 ([#26124](https://github.com/MetaMask/metamask-extension/pull/26124)) -- Revert "test: Adding e2e for SIWE and re-enabling redesign for SIWE (#25831)" ([#25831](https://github.com/MetaMask/metamask-extension/pull/25831)) -- chore: Create a story for Snackbar component ([#25515](https://github.com/MetaMask/metamask-extension/pull/25515)) -- fix: add new helper function for `openMenuSafe` to mitigate all ocurrences for opening menu with MMI build ([#26079](https://github.com/MetaMask/metamask-extension/pull/26079)) -- test: Adding e2e for SIWE and re-enabling redesign for SIWE (#25831) ([#25831](https://github.com/MetaMask/metamask-extension/pull/25831)) -- feat: make add-team-label use the reusable workflow ([#25807](https://github.com/MetaMask/metamask-extension/pull/25807)) -- chore: MMI-5301 adds enums for custody type and status ([#26006](https://github.com/MetaMask/metamask-extension/pull/26006)) -- feat: Move ENABLE_CONFIRMATION_REDESIGN feature flag to the developer… ([#26095](https://github.com/MetaMask/metamask-extension/pull/26095)) -- fix: remove btc account from permission connect lists ([#25980](https://github.com/MetaMask/metamask-extension/pull/25980)) -- feat: update network list item to include start accessory and end ([#25507](https://github.com/MetaMask/metamask-extension/pull/25507)) -- chore: mmi 5305 mmi pages typescript migration ([#26081](https://github.com/MetaMask/metamask-extension/pull/26081)) -- fix: Move Snaps hooks out of code fence ([#26120](https://github.com/MetaMask/metamask-extension/pull/26120)) -- feat: Mitigate risk for distracted users on queued transactions from different dApps ([#25852](https://github.com/MetaMask/metamask-extension/pull/25852)) -- fix: lock Chrome version to 126 (#26101) ([#26101](https://github.com/MetaMask/metamask-extension/pull/26101)) -- feat: Add metrics event for advanced details section toggling ([#26083](https://github.com/MetaMask/metamask-extension/pull/26083)) -- fix: display link to privacy-policy explanation in onboarding flow ([#26038](https://github.com/MetaMask/metamask-extension/pull/26038)) -- chore: Create a story for InvalidCustomNetworkAlert component ([#25600](https://github.com/MetaMask/metamask-extension/pull/25600)) -- fix: number formatting on swap + send tx detail ([#26029](https://github.com/MetaMask/metamask-extension/pull/26029)) -- fix: Flaky test `Account Custom Name..` ([#26062](https://github.com/MetaMask/metamask-extension/pull/26062)) -- fix: snap flakiness on `installSnapSimpleKeyring` function ([#26039](https://github.com/MetaMask/metamask-extension/pull/26039)) -- fix: lock Chrome version to 126 ([#26101](https://github.com/MetaMask/metamask-extension/pull/26101)) -- fix: remove halo for tokens ([#26016](https://github.com/MetaMask/metamask-extension/pull/26016)) -- refactor: replace typography with text component in creation-successful.js ([#25552](https://github.com/MetaMask/metamask-extension/pull/25552)) -- fix: `vault decryption` broken tests due to update on window handling ([#26074](https://github.com/MetaMask/metamask-extension/pull/26074)) -- docs: Centralize Author/Team Mapping for Commit Tracking ([#25986](https://github.com/MetaMask/metamask-extension/pull/25986)) -- fix: flaky test: Check the toggle for hex data ([#25899](https://github.com/MetaMask/metamask-extension/pull/25899)) -- chore: migrated institutional ui components to ts ([#25858](https://github.com/MetaMask/metamask-extension/pull/25858)) -- chore: removed unused component ([#26000](https://github.com/MetaMask/metamask-extension/pull/26000)) -- chore: update Bitcoin Snap to version 0.2.5 ([#26058](https://github.com/MetaMask/metamask-extension/pull/26058)) -- refactor: replace Typography with Text component in metametrics.js ([#25630](https://github.com/MetaMask/metamask-extension/pull/25630)) -- refactor: replace typography with text component in review recovery phrase ([#25265](https://github.com/MetaMask/metamask-extension/pull/25265)) -- test: new switchToWindowWithTitle w/ Extension communication ([#25362](https://github.com/MetaMask/metamask-extension/pull/25362)) -- ci: Trimming the gitdiff output before writing to output file ([#26057](https://github.com/MetaMask/metamask-extension/pull/26057)) -- chore: tweak send page styling ([#25982](https://github.com/MetaMask/metamask-extension/pull/25982)) -- fix: mmi flaky tests `Reveal SRP through settings completes quiz and reveals SRP QR after wrong answers` , `Sign Typed Data Signature Request can initiate and reject a Signature Request of Sign Typed Data`, `Sign Typed Data Signature Request can queue multiple Signature Requests of Sign Typed Data and confirm` ([#26055](https://github.com/MetaMask/metamask-extension/pull/26055)) -- chore: Create a story for IconButton component ([#25277](https://github.com/MetaMask/metamask-extension/pull/25277)) -- fix: center token icon ([#26013](https://github.com/MetaMask/metamask-extension/pull/26013)) -- fix: flaky test `Import flow @no-mmi Import wallet using Secret Recovery Phrase with pasting word by word` ([#26049](https://github.com/MetaMask/metamask-extension/pull/26049)) -- fix: flaky test 25912 ([#25913](https://github.com/MetaMask/metamask-extension/pull/25913)) -- chore: add privacy query params to portfolio navigation ([#25958](https://github.com/MetaMask/metamask-extension/pull/25958)) -- fix: (cherry-pick) Remove special reject button case from api spec tests (#26048) ([#26048](https://github.com/MetaMask/metamask-extension/pull/26048)) -- chore: Temporarily disable Playwright Swaps tests ([#26050](https://github.com/MetaMask/metamask-extension/pull/26050)) -- fix: Remove special reject button case from api spec tests ([#26048](https://github.com/MetaMask/metamask-extension/pull/26048)) -- test(e2e): unlock trezor account ([#25824](https://github.com/MetaMask/metamask-extension/pull/25824)) -- fix: Flaky "Signature Approved Event" e2e test ([#26040](https://github.com/MetaMask/metamask-extension/pull/26040)) -- feat: Migration #122 set redesignedConfirmationsEnabled to true ([#25769](https://github.com/MetaMask/metamask-extension/pull/25769)) -- fix: Revert "refactor: use withKeyring method (#25435)" ([#25435](https://github.com/MetaMask/metamask-extension/pull/25435)) -- fix: :label: update the text in the popup to enable notifications ([#26026](https://github.com/MetaMask/metamask-extension/pull/26026)) -- fix: map the supported block explorers ([#25908](https://github.com/MetaMask/metamask-extension/pull/25908)) -- fix: update css for modals ([#25961](https://github.com/MetaMask/metamask-extension/pull/25961)) -- fix: Fix permssions for `update-attributions` workflow ([#26019](https://github.com/MetaMask/metamask-extension/pull/26019)) -- fix: add migration for profile syncing controller ([#26004](https://github.com/MetaMask/metamask-extension/pull/26004)) -- test: Adding e2e for SIWE and re-enabling redesign for SIWE ([#25831](https://github.com/MetaMask/metamask-extension/pull/25831)) -- test: UX: Multichain: Add E2E for signaling network change from Network menu to dapp, Autoswitching networks ([#25765](https://github.com/MetaMask/metamask-extension/pull/25765)) -- feat: Move ENABLE_CONFIRMATION_REDESIGN feature flag to the developer settings page ([#25520](https://github.com/MetaMask/metamask-extension/pull/25520)) -- fix: `yarn:start:test:flask` is broken `Lavapack is not defined` ([#25995](https://github.com/MetaMask/metamask-extension/pull/25995)) -- feat: add utility function to get supported chains from the Security Alerts API ([#25716](https://github.com/MetaMask/metamask-extension/pull/25716)) -- fix: `vault-decryption` test since the order of announcement modals changed ([#25997](https://github.com/MetaMask/metamask-extension/pull/25997)) -- fix: updated switch to this account condition ([#25609](https://github.com/MetaMask/metamask-extension/pull/25609)) -- fix: flaky test Settings Redirects to ENS domains when user inputs ENS into address bar ([#25782](https://github.com/MetaMask/metamask-extension/pull/25782)) -- chore: MMI-5248 introduce the token allowance functionality for MMI ([#25967](https://github.com/MetaMask/metamask-extension/pull/25967)) -- fix: vertically align asset image ([#25988](https://github.com/MetaMask/metamask-extension/pull/25988)) -- feat: Adding state per window in e2e, excluding null state ([#25900](https://github.com/MetaMask/metamask-extension/pull/25900)) -- fix: attribution link ([#25947](https://github.com/MetaMask/metamask-extension/pull/25947)) -- feat: Enable hardware wallets for smart transactions in swaps ([#25742](https://github.com/MetaMask/metamask-extension/pull/25742)) -- fix: fix link redirection ([#25983](https://github.com/MetaMask/metamask-extension/pull/25983)) -- fix: fix overlapping modals ([#25962](https://github.com/MetaMask/metamask-extension/pull/25962)) -- feat: Show the Close extension button on the Smart Transaction Status Page for a pending dapp transaction ([#25965](https://github.com/MetaMask/metamask-extension/pull/25965)) -- fix(multichain): use accounts{Added,Removed} to fetch/clear balances ([#25884](https://github.com/MetaMask/metamask-extension/pull/25884)) -- test: Add integration tests for permit simulation section ([#25856](https://github.com/MetaMask/metamask-extension/pull/25856)) -- fix: fixed max width for permissions page ([#25870](https://github.com/MetaMask/metamask-extension/pull/25870)) -- fix: show current network if domains are undefined ([#25960](https://github.com/MetaMask/metamask-extension/pull/25960)) -- fix: notification slowness and crashes ([#25946](https://github.com/MetaMask/metamask-extension/pull/25946)) -- ci: Disabling non-lint CI on the l10n_crowdin_action branch ([#25809](https://github.com/MetaMask/metamask-extension/pull/25809)) -- refactor: use `withKeyring` method ([#25435](https://github.com/MetaMask/metamask-extension/pull/25435)) -- feat: add BTC support survey link ([#25875](https://github.com/MetaMask/metamask-extension/pull/25875)) -- fix: re-organize files under assets folder ([#25897](https://github.com/MetaMask/metamask-extension/pull/25897)) -- fix: fix css nft detail ([#25931](https://github.com/MetaMask/metamask-extension/pull/25931)) -- fix: Implement Auto-Enable Feature for Basic Functionality in Metamask Extension v12.1.0 ([#25944](https://github.com/MetaMask/metamask-extension/pull/25944)) -- fix: Handle error when offscreen document already exists ([#25138](https://github.com/MetaMask/metamask-extension/pull/25138)) -- test: Expand coverage of sourcemap validator ([#25115](https://github.com/MetaMask/metamask-extension/pull/25115)) -- feat: Add full screen Snap Home and Dialog ([#25670](https://github.com/MetaMask/metamask-extension/pull/25670)) -- chore: swaps codeowners reorg ([#24803](https://github.com/MetaMask/metamask-extension/pull/24803)) -- fix: track token detection enabled ([#25822](https://github.com/MetaMask/metamask-extension/pull/25822)) -- fix: rm locales in other languages ([#25936](https://github.com/MetaMask/metamask-extension/pull/25936)) -- fix: fix ([#25907](https://github.com/MetaMask/metamask-extension/pull/25907)) -- fix: password reset ([#25847](https://github.com/MetaMask/metamask-extension/pull/25847)) -- New Crowdin translations by Github Action ([#24889](https://github.com/MetaMask/metamask-extension/pull/24889)) -- fix: Remove abandoned test:unit:jest command ([#25905](https://github.com/MetaMask/metamask-extension/pull/25905)) -- fix(22851): check if active device to prevent autoconnect for hw ([#25503](https://github.com/MetaMask/metamask-extension/pull/25503)) -- test: Removed step from e2e tests ([#25910](https://github.com/MetaMask/metamask-extension/pull/25910)) -- fix: calcTokenAmount BigNumber more than 15 digits error ([#25799](https://github.com/MetaMask/metamask-extension/pull/25799)) -- feat: add custom form check alerts ([#25259](https://github.com/MetaMask/metamask-extension/pull/25259)) -- fix: test failure on firefox ([#25895](https://github.com/MetaMask/metamask-extension/pull/25895)) -- fix: disables "swap and send" for MMI ([#25886](https://github.com/MetaMask/metamask-extension/pull/25886)) -- fix: Fixed flaky test 24645 ([#25786](https://github.com/MetaMask/metamask-extension/pull/25786)) -- chore: refactor SwapsController so it extends from BaseControllerV2 ([#25681](https://github.com/MetaMask/metamask-extension/pull/25681)) -- feat: Replace "Manage in settings" with "No thanks" in the STX Opt In modal, only show the modal for non-zero balances ([#25848](https://github.com/MetaMask/metamask-extension/pull/25848)) -- feat: Display advanced section within confirmation by default for some users ([#25687](https://github.com/MetaMask/metamask-extension/pull/25687)) -- chore: bump assets-controllers to v36.0.0 ([#25857](https://github.com/MetaMask/metamask-extension/pull/25857)) -- fix: add name to scuttling exception list ([#25849](https://github.com/MetaMask/metamask-extension/pull/25849)) -- fix: update build version to align with firefox's newer version restrictions ([#25456](https://github.com/MetaMask/metamask-extension/pull/25456)) -- feat: regression label ([#25691](https://github.com/MetaMask/metamask-extension/pull/25691)) -- chore: Master sync ([#25816](https://github.com/MetaMask/metamask-extension/pull/25816)) -- fix: contract data in metrics ([#25759](https://github.com/MetaMask/metamask-extension/pull/25759)) -- fix: flaky test `ERC721 NFTs testdapp interaction` ([#25854](https://github.com/MetaMask/metamask-extension/pull/25854)) -- fix: flaky test `Create BTC Account cannot create multiple BTC accounts...` ([#25861](https://github.com/MetaMask/metamask-extension/pull/25861)) -- feat: support creation of Bitcoin testnet accounts ([#25772](https://github.com/MetaMask/metamask-extension/pull/25772)) -- fix: use of an header in a dedicated call ([#25828](https://github.com/MetaMask/metamask-extension/pull/25828)) -- feat(tests): add btc e2e tests ([#25663](https://github.com/MetaMask/metamask-extension/pull/25663)) -- feat: NFT details new design ([#25524](https://github.com/MetaMask/metamask-extension/pull/25524)) -- feat: Add fuzzy matching for name lookup ([#25264](https://github.com/MetaMask/metamask-extension/pull/25264)) -- fix: edit path to dist folder ([#25826](https://github.com/MetaMask/metamask-extension/pull/25826)) -- chore: update @metamask/bitcoin-wallet-snap to 0.2.4 (#25808) ([#25808](https://github.com/MetaMask/metamask-extension/pull/25808)) -- chore: Patch security issue in snaps-utils ([#25827](https://github.com/MetaMask/metamask-extension/pull/25827)) -- feat: add option of copy to info row component ([#25682](https://github.com/MetaMask/metamask-extension/pull/25682)) -- fix: skip blockaid validations for users internal accounts ([#25695](https://github.com/MetaMask/metamask-extension/pull/25695)) -- chore: refactor custody component ([#25684](https://github.com/MetaMask/metamask-extension/pull/25684)) -- Merge origin/develop into master-sync -- chore: update @metamask/bitcoin-wallet-snap to 0.2.4 ([#25808](https://github.com/MetaMask/metamask-extension/pull/25808)) -- chore: removed unused getCustodianAccountsByAddress method ([#25798](https://github.com/MetaMask/metamask-extension/pull/25798)) -- feat: Make Jest unit tests run faster in GitHub actions ([#25726](https://github.com/MetaMask/metamask-extension/pull/25726)) -- revert: un-revert metrics and signature refactor test ([#25758](https://github.com/MetaMask/metamask-extension/pull/25758)) -- feat: Add `ui_customizations` metric for transactions ([#25736](https://github.com/MetaMask/metamask-extension/pull/25736)) -- test: add e2e tests for navigation (#25652) ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) -- chore: remove `BTC_BETA_SUPPORT` flag ([#25776](https://github.com/MetaMask/metamask-extension/pull/25776)) -- chore: update @metamask/bitcoin-wallet-snap to 0.2.3 ([#25775](https://github.com/MetaMask/metamask-extension/pull/25775)) -- feat: add more whitelisted portfolio URLs ([#25767](https://github.com/MetaMask/metamask-extension/pull/25767)) -- fix: Fix page width for fullscreen mode send page ([#25639](https://github.com/MetaMask/metamask-extension/pull/25639)) -- chore: Update Snaps codeowners list ([#25581](https://github.com/MetaMask/metamask-extension/pull/25581)) -- fix: fine-tune for `Delineator` component styles ([#25760](https://github.com/MetaMask/metamask-extension/pull/25760)) -- feat: decode transaction data ([#25597](https://github.com/MetaMask/metamask-extension/pull/25597)) -- feat: add `Delineator` component ([#25610](https://github.com/MetaMask/metamask-extension/pull/25610)) -- feat(ramps): update isNativeTokenBuyable to include BTC ([#25621](https://github.com/MetaMask/metamask-extension/pull/25621)) -- fix: Fix issue 25285 max insufficient funds for gas ([#25574](https://github.com/MetaMask/metamask-extension/pull/25574)) -- feat: add BTC experimental toggle ([#25672](https://github.com/MetaMask/metamask-extension/pull/25672)) -- build: bump gas-fee-controller to v18 and remove patch ([#25679](https://github.com/MetaMask/metamask-extension/pull/25679)) -- fix: show correct asset and balance when BTC account is the selected account ([#25719](https://github.com/MetaMask/metamask-extension/pull/25719)) -- feat(btc): add BTC account creation menu entry ([#25625](https://github.com/MetaMask/metamask-extension/pull/25625)) -- fix: flaky test `Test Snap Metrics test snap update rejected metric` ([#25744](https://github.com/MetaMask/metamask-extension/pull/25744)) -- chore(deps): bump @metamask/accounts-controller from ^17.0.0 to ^17.2.0 ([#25676](https://github.com/MetaMask/metamask-extension/pull/25676)) -- fix: use LAVAMOAT_UPDATE_TOKEN in attributions workflow ([#25731](https://github.com/MetaMask/metamask-extension/pull/25731)) -- fix: caveat mutations for non-EVM accounts ([#25739](https://github.com/MetaMask/metamask-extension/pull/25739)) -- test: Add UI integration tests ([#24428](https://github.com/MetaMask/metamask-extension/pull/24428)) -- fix: revert "test: add e2e tests for navigation (#25652)" ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) -- chore: Revert "test: e2e metrics test and refactor" ([#25722](https://github.com/MetaMask/metamask-extension/pull/25722)) -- feat: bundle pre-installed Bitcoin Wallet Snap ([#25715](https://github.com/MetaMask/metamask-extension/pull/25715)) -- fix: protect against phishing domain redirects in main/sub frames for http(s) requests ([#25153](https://github.com/MetaMask/metamask-extension/pull/25153)) -- fix: Fix crash of Transaction screen with smart transaction ([#25717](https://github.com/MetaMask/metamask-extension/pull/25717)) -- fix: Hide MMI Account Mistmatch BannerAlert from Sign-in with Ethereum (SIWE) Redesign Page ([#25662](https://github.com/MetaMask/metamask-extension/pull/25662)) -- fix: flaky test `Create token, approve token and approve token without gas approves an already created token and displays the token approval data` ([#25706](https://github.com/MetaMask/metamask-extension/pull/25706)) -- feat: Enable SIWE Signature Redesign ([#25660](https://github.com/MetaMask/metamask-extension/pull/25660)) -- fix: flaky test `Request-queue UI changes handles three confirmations on three confirmations concurrently` ([#25675](https://github.com/MetaMask/metamask-extension/pull/25675)) -- feat: move unit tests from Circleci to Github actions ([#25570](https://github.com/MetaMask/metamask-extension/pull/25570)) -- test: e2e metrics test and refactor ([#25632](https://github.com/MetaMask/metamask-extension/pull/25632)) -- test: add e2e tests for navigation ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) -- feat: support security alerts API ([#25544](https://github.com/MetaMask/metamask-extension/pull/25544)) -- feat(ramps): add flag to ensure ramp networks are only fetched once ([#25686](https://github.com/MetaMask/metamask-extension/pull/25686)) -- fix: allow ramps dev environment on Flask ([#25659](https://github.com/MetaMask/metamask-extension/pull/25659)) -- feat: added check for if the selected account is BTC in transaction-list ([#25642](https://github.com/MetaMask/metamask-extension/pull/25642)) -- feat: Gas Fees Redesign PoC ([#24714](https://github.com/MetaMask/metamask-extension/pull/24714)) -- fix: show connected toast only for EVM accounts ([#25628](https://github.com/MetaMask/metamask-extension/pull/25628)) -- fix: changed logic to use the new banner alert ([#25626](https://github.com/MetaMask/metamask-extension/pull/25626)) -- fix: set network client id for domain ([#25646](https://github.com/MetaMask/metamask-extension/pull/25646)) -- feat: improvement for how we display big and small numbers ([#25438](https://github.com/MetaMask/metamask-extension/pull/25438)) -- chore: restore bot workflow to update attributions ([#25211](https://github.com/MetaMask/metamask-extension/pull/25211)) -- test: add swap e2e tests on Tenderly network ([#25060](https://github.com/MetaMask/metamask-extension/pull/25060)) -- fix: UX: Multichain: Add safeguard to throw error when confirmation chainId doesn't match current chainId ([#25634](https://github.com/MetaMask/metamask-extension/pull/25634)) -- chore: updates MMI custody controller ([#25631](https://github.com/MetaMask/metamask-extension/pull/25631)) -- fix: flaky test `Test Snap Get Locale test snap_getLocale functionality` ([#25648](https://github.com/MetaMask/metamask-extension/pull/25648)) -- fix: Skip blockaid validation for SIWE signature types ([#25612](https://github.com/MetaMask/metamask-extension/pull/25612)) -- feat: Add support for security alerts on zkSync, Berachain, Scroll and Metachain One on extension ([#25555](https://github.com/MetaMask/metamask-extension/pull/25555)) -- fix: Multichain: UX: Check for transactions on all networks and QueuedRequestCount ([#25614](https://github.com/MetaMask/metamask-extension/pull/25614)) -- feat: define which keyring methods Portfolio can call ([#25633](https://github.com/MetaMask/metamask-extension/pull/25633)) -- chore: flaky E2E tests improved ([#25565](https://github.com/MetaMask/metamask-extension/pull/25565)) -- feat: add SIWE mismatch account warning alert ([#25613](https://github.com/MetaMask/metamask-extension/pull/25613)) -- fix: support multichain in blockexplorer and qr code ([#25526](https://github.com/MetaMask/metamask-extension/pull/25526)) -- fix: decimal places displayed on token value on permit pages ([#25410](https://github.com/MetaMask/metamask-extension/pull/25410)) -- feat: added BTC variant to ramps-card and illustration image ([#25615](https://github.com/MetaMask/metamask-extension/pull/25615)) -- fix: Remove unused fixtures and fix test name in smart swaps disabled spec ([#25616](https://github.com/MetaMask/metamask-extension/pull/25616)) -- chore: Update @metamask/smart-transactions-controller from 10.1.2 to 10.1.6 ([#25611](https://github.com/MetaMask/metamask-extension/pull/25611)) -- fix: Fix issue 22837 about unknown error during ledger pair ([#25462](https://github.com/MetaMask/metamask-extension/pull/25462)) -- test: add e2e to swap with snap account ([#25558](https://github.com/MetaMask/metamask-extension/pull/25558)) -- chore: exclude running git diff job for the e2e quality gate in `develop`, `master` and release branches ([#25605](https://github.com/MetaMask/metamask-extension/pull/25605)) -- chore: [Delivery] Update author mapping list for PR ([#25606](https://github.com/MetaMask/metamask-extension/pull/25606)) -- fix: page object selector not found ([#25624](https://github.com/MetaMask/metamask-extension/pull/25624)) -- test: Initial PR for integrating the Page Object Model (POM) into e2e test suite ([#25373](https://github.com/MetaMask/metamask-extension/pull/25373)) -- refactor: Replace deprecated mixins with Text component in selected-account.component.js ([#25262](https://github.com/MetaMask/metamask-extension/pull/25262)) -- refactor: Replace deprecated mixins with Text component in unlock-page.component.js ([#25227](https://github.com/MetaMask/metamask-extension/pull/25227)) -- chore: Create a story for RestoreVaultPage component ([#25284](https://github.com/MetaMask/metamask-extension/pull/25284)) -- fix(snaps): Fix alignment of install origin is `snap-authorship-expanded` ([#25583](https://github.com/MetaMask/metamask-extension/pull/25583)) -- feat: Remove blockaid migration BannerAlert ([#25556](https://github.com/MetaMask/metamask-extension/pull/25556)) -- fix: failingt e2e `Click bridge button from asset page @no-mmi loads portfolio tab when flag is turned off` ([#25607](https://github.com/MetaMask/metamask-extension/pull/25607)) -- chore: adds quality gate for rerunning e2e spec files that are new or have been modified ([#24556](https://github.com/MetaMask/metamask-extension/pull/24556)) -- chore(deps): bump assets controller to v34.0.0 ([#25540](https://github.com/MetaMask/metamask-extension/pull/25540)) -- chore: add bridge controller, store and api utils ([#25044](https://github.com/MetaMask/metamask-extension/pull/25044)) -- fix: add eth_signTypedData and eth_signTypedData_v3 to `methodsRequiringNetworkSwitch` ([#25562](https://github.com/MetaMask/metamask-extension/pull/25562)) - Fixed an issue where the wallet was not accessible with a new password after resetting it ([#25847](https://github.com/MetaMask/metamask-extension/pull/25847)) - Fixed number formatting for swap + send transaction details to avoid scientific notation for small token amounts ([#26029](https://github.com/MetaMask/metamask-extension/pull/26029)) - Fixed an issue with link redirection to ensure proper navigation ([#25983](https://github.com/MetaMask/metamask-extension/pull/25983)) diff --git a/README.md b/README.md index d70eb03a32b2..4f15e138be56 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ For [general questions](https://community.metamask.io/c/learn/26), [feature requ MetaMask supports Firefox, Google Chrome, and Chromium-based browsers. We recommend using the latest available browser version. -For up to the minute news, follow our [Twitter](https://twitter.com/metamask) or [Medium](https://medium.com/metamask) pages. +For up to the minute news, follow us on [X](https://x.com/MetaMask). To learn how to develop MetaMask-compatible applications, visit our [Developer Docs](https://metamask.github.io/metamask-docs/). diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 4ce44a388ac1..f118bc17df41 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -235,10 +235,6 @@ "fast": { "message": "ፈጣን" }, - "fiat": { - "message": "ፊያት", - "description": "Exchange type" - }, "fileImportFail": { "message": "ፋይል ማስመጣት እየሰራ አይደለም? እዚህ ላይ ጠቅ ያድርጉ!", "description": "Helps user import their account from a JSON file" @@ -493,12 +489,6 @@ "prev": { "message": "የቀደመ" }, - "primaryCurrencySetting": { - "message": "ተቀዳሚ የገንዘብ ዓይነት" - }, - "primaryCurrencySettingDescription": { - "message": "ዋጋዎች በራሳቸው የሰንሰለት ገንዘብ ዓይነት (ለምሳሌ ETH) በቅድሚያ እንዲታዪ ይምረጡ። ዋጋዎች በተመረጠ የፊያት ገንዘብ ዓይነት እንዲታዩ ደግሞ ፊያትን ይምረጡ።" - }, "privacyMsg": { "message": "የግለኝነት መጠበቂያ ህግ" }, @@ -750,9 +740,6 @@ "unlockMessage": { "message": "ያልተማከለ ድር ይጠባበቃል" }, - "updatedWithDate": { - "message": "የዘመነ $1" - }, "urlErrorMsg": { "message": "URIs አግባብነት ያለው የ HTTP/HTTPS ቅድመ ቅጥያ ይፈልጋል።" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 6cb79c56b136..d9717df6b190 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -505,12 +505,6 @@ "prev": { "message": "السابق" }, - "primaryCurrencySetting": { - "message": "العملة الأساسية" - }, - "primaryCurrencySettingDescription": { - "message": "حدد خيار \"المحلية\" لتحديد أولويات عرض القيم بالعملة المحلية للسلسلة (مثلاً ETH). حدد Fiat لتحديد أولويات عرض القيم بعملات fiat المحددة الخاصة بك." - }, "privacyMsg": { "message": "سياسة الخصوصية" }, @@ -762,9 +756,6 @@ "unlockMessage": { "message": "شبكة الويب اللامركزية بانتظارك" }, - "updatedWithDate": { - "message": "تم تحديث $1" - }, "urlErrorMsg": { "message": "تتطلب الروابط بادئة HTTP/HTTPS مناسبة." }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 1fa7a14393d4..749b1561dafe 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Предишен" }, - "primaryCurrencySetting": { - "message": "Основна валута" - }, - "primaryCurrencySettingDescription": { - "message": "Изберете местна, за да приоритизирате показването на стойности в основната валута на веригата (например ETH). Изберете Fiat, за да поставите приоритет на показването на стойности в избраната от вас fiat валута." - }, "privacyMsg": { "message": "Политика за поверителност" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Децентрализираната мрежа очаква" }, - "updatedWithDate": { - "message": "Актуализирано $1 " - }, "urlErrorMsg": { "message": "URI изискват съответния HTTP / HTTPS префикс." }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index a9cc5aa0d845..15acaa2e6765 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "দ্রুত" }, - "fiat": { - "message": "ফিয়াট", - "description": "Exchange type" - }, "fileImportFail": { "message": "ফাইল আমদানি কাজ করছে না? এখানে ক্লিক করুন!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "পূর্ববর্তী" }, - "primaryCurrencySetting": { - "message": "প্রাথমিক মুদ্রা" - }, - "primaryCurrencySettingDescription": { - "message": "চেনটিতে (যেমন ETH) দেশীয় মুদ্রায় মানগুলি প্রদর্শনকে অগ্রাধিকার দিতে দেশীয় নির্বাচন করুন। আপনার নির্দেশিত মুদ্রায় মানগুলির প্রদর্শনকে অগ্রাধিকার দিতে নির্দেশিত নির্বাচন করুন।" - }, "privacyMsg": { "message": "সম্মত হয়েছেন" }, @@ -759,9 +749,6 @@ "unlockMessage": { "message": "ছড়িয়ে ছিটিয়ে থাকা ওয়েব অপেক্ষা করছে" }, - "updatedWithDate": { - "message": "আপডেট করা $1" - }, "urlErrorMsg": { "message": "URI গুলির যথাযথ HTTP/HTTPS প্রেফিক্সের প্রয়োজন।" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 92f2b2771ff9..fc9e2afb41e6 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -489,12 +489,6 @@ "personalAddressDetected": { "message": "Adreça personal detectada. Introduir l'adreça del contracte de fitxa." }, - "primaryCurrencySetting": { - "message": "Divisa principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecciona Natiu per a prioritzar la mostra de valors en la divisa nadiua de la cadena (p. ex. ETH). Selecciona Fiat per prioritzar la mostra de valors en la divisa fiduciària seleccionada." - }, "privacyMsg": { "message": "Política de privadesa" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "La web descentralitzada està esperant" }, - "updatedWithDate": { - "message": "Actualitzat $1" - }, "urlErrorMsg": { "message": "Els URIs requereixen el prefix HTTP/HTTPS apropiat." }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 6e3bfa315303..4113f8c5cc42 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -105,10 +105,6 @@ "failed": { "message": "Neúspěšné" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 12eba292e0a4..37e4663523cf 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -489,12 +489,6 @@ "prev": { "message": "Forrige" }, - "primaryCurrencySetting": { - "message": "Primær Valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Vælg lokal for fortrinsvis at vise værdier i kædens (f.eks. ETH) lokale valuta. Vælg Fiat for fortrinsvis at vise værdier i din valgte fiat valuta." - }, "privacyMsg": { "message": "Privatlivspolitik" }, @@ -734,9 +728,6 @@ "unlockMessage": { "message": "Det decentraliserede internet venter" }, - "updatedWithDate": { - "message": "Opdaterede $1" - }, "urlErrorMsg": { "message": "Links kræver det rette HTTP/HTTPS-præfix." }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 3b23de7fb61b..6177fe229bfb 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Wenn Ihre Transaktion in den Block aufgenommen wird, wird die Differenz zwischen Ihrer maximalen Grundgebühr und der tatsächlichen Grundgebühr erstattet. Der Gesamtbetrag wird berechnet als maximale Grundgebühr (in GWEI) * Gas-Limit." }, - "advancedConfiguration": { - "message": "Erweiterte Einstellungen" - }, "advancedDetailsDataDesc": { "message": "Daten" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gas-Optionen aktualisieren" }, - "alertBannerMultipleAlertsDescription": { - "message": "Wenn Sie diese Anfrage genehmigen, könnten Dritte, die für Betrügereien bekannt sind, alle Ihre Assets an sich reißen." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Mehrere Benachrichtigungen!" - }, "alertDisableTooltip": { "message": "Dies kann in „Einstellungen > Benachrichtigungen“ geändert werden." }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Um mit dieser Transaktion fortzufahren, müssen Sie das Gas-Limit auf 21.000 oder mehr erhöhen." }, - "alertMessageInsufficientBalance": { - "message": "Sie haben nicht genug ETH auf Ihrem Konto, um die Transaktionsgebühren zu bezahlen." - }, "alertMessageNetworkBusy": { "message": "Die Gas-Preise sind hoch und die Schätzungen sind weniger genau." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Falsches Konto" }, - "alertSettingsUnconnectedAccount": { - "message": "Eine Webseite mit einem nicht verknüpften Konto durchsuchen" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Diese Warnung wird im Popup angezeigt, wenn Sie eine verbundene Webseite durchsuchen, aber das aktuell ausgewählte Konto ist nicht verbunden." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Wenn eine Webseite versucht, die entfernte window.web3 API zu verwenden" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Diese Benachrichtigung wird in einem Popup-Fenster angezeigt, wenn Sie eine Website besuchen, die versucht, die entfernte window.web3-API zu verwenden, und die dadurch möglicherweise beschädigt wird." - }, "alerts": { "message": "Benachrichtigungen" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Beta-Nutzungsbedingungen" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta kann Ihre geheime Wiederherstellungsphrase nicht wiederherstellen." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta wird Sie nie nach Ihrer geheimen Wiederherstellungsphrase fragen." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Ich habe die Benachrichtigung zur Kenntnis genommen und möchte trotzdem fortfahren" }, - "confirmAlertModalDetails": { - "message": "Wenn Sie sich anmelden, könnten Dritte, die für Betrügereien bekannt sind, all Ihre Assets an sich reißen. Lesen Sie bitte die Benachrichtigungen, bevor Sie fortfahren." - }, - "confirmAlertModalTitle": { - "message": "Ihre Assets könnten gefährdet sein" - }, "confirmConnectCustodianRedirect": { "message": "Sobald Sie auf „Weiter“ klicken, leiten wir Sie weiter zu $1." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask ist mit dieser Seite verbunden, aber es sind noch keine Konten verbunden" }, - "connectedWith": { - "message": "Verbunden mit" - }, "connecting": { "message": "Verbinden" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Details zur Gebühr" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dateiimport fehlgeschlagen? Bitte hier klicken!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Datei", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask ist nicht mit dieser Website verbunden" }, - "noConversionDateAvailable": { - "message": "Kein Umrechnungskursdaten verfügbar" - }, "noConversionRateAvailable": { "message": "Kein Umrechnungskurs verfügbar" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional pinnen" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Phishing-Warnungen basieren auf der Kommunikation mit $1. jsDeliver hat Zugriff auf Ihre IP-Adresse. $2 ansehen.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 T", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "Preis nicht verfügbar" }, - "primaryCurrencySetting": { - "message": "Hauptwährung" - }, - "primaryCurrencySettingDescription": { - "message": "Wählen Sie 'Nativ', um dem Anzeigen von Werten in der nativen Währung der Chain (z. B. ETH) Vorrang zu geben. Wählen Sie 'Fiat', um dem Anzeigen von Werten in Ihrer gewählten Fiat-Währung Vorrang zu geben." - }, "primaryType": { "message": "Primärer Typ" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Abgelehnt" }, - "remember": { - "message": "Erinnern:" - }, "remove": { "message": "Entfernen" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia-Testnetzwerk" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask nutzt diese vertrauenswürdigen Dienstleistungen von Drittanbietern, um die Benutzerfreundlichkeit und Sicherheit der Produkte zu verbessern." - }, "setApprovalForAll": { "message": "Erlaubnis für alle erteilen" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Ihre Transaktion ist abgeschlossen" }, - "smartTransactionTakingTooLong": { - "message": "Entschuldigung für die Wartezeit" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Wenn Ihre Transaktion nicht innerhalb von $1 abgeschlossen wird, wird sie storniert und Ihnen wird kein Gas berechnet.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Versuchen Sie Ihren Swap erneut. Wir werden hier sein, um Sie beim nächsten Mal vor ähnlichen Risiken zu schützen." }, - "stxEstimatedCompletion": { - "message": "Geschätzter Abschluss in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap fehlgeschlagen" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Aktualisierungsanfrage" }, - "updatedWithDate": { - "message": "$1 aktualisiert" - }, "uploadDropFile": { "message": "Legen Sie Ihre Datei hier ab" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "unsere Hardware-Wallet-Verbindungsanleitung" }, - "walletCreationSuccessDetail": { - "message": "Sie haben Ihre Wallet erfolgreich geschützt. Halten Sie Ihre geheime Wiederherstellungsphrase sicher und geheim -- es liegt in Ihrer Verantwortung!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask kann Ihre geheime Wiederherstellungsphrase nicht wiederherstellen." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask-Team wird nie nach Ihrer geheimen Wiederherstellungsphrase fragen." - }, - "walletCreationSuccessReminder3": { - "message": "$1 mit jemandem oder riskieren Sie, dass Ihre Gelder gestohlen werden.", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Geben Sie niemals Ihre geheime Wiederherstellungsphrase an andere weiter", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Wallet-Erstellung erfolgreich" - }, "wantToAddThisNetwork": { "message": "Möchten Sie dieses Netzwerk hinzufügen?" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 5df72a342c5f..c7ce18893b4d 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Όταν η συναλλαγή σας συμπεριληφθεί στο μπλοκ, οποιαδήποτε διαφορά μεταξύ της μέγιστης βασικής χρέωσής σας και της πραγματικής βασικής χρέωσής θα επιστραφεί. Το συνολικό ποσό υπολογίζεται ως μέγιστο βασικό τέλος (σε GWEI) * όριο τελών συναλλαγής." }, - "advancedConfiguration": { - "message": "Προηγμένη ρύθμιση παραμέτρων" - }, "advancedDetailsDataDesc": { "message": "Δεδομένα" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Ενημέρωση επιλογών των τελών συναλλαγών" }, - "alertBannerMultipleAlertsDescription": { - "message": "Εάν εγκρίνετε αυτό το αίτημα, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Πολλαπλές ειδοποιήσεις!" - }, "alertDisableTooltip": { "message": "Αυτό μπορεί να αλλάξει στις \"Ρυθμίσεις > Ειδοποιήσεις\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Για να συνεχίσετε με αυτή τη συναλλαγή, θα πρέπει να αυξήσετε το όριο των τελών συναλλαγών σε 21000 ή περισσότερο." }, - "alertMessageInsufficientBalance": { - "message": "Δεν έχετε αρκετά ETH στον λογαριασμό σας για να πληρώσετε τα τέλη συναλλαγών." - }, "alertMessageNetworkBusy": { "message": "Οι τιμές των τελών συναλλαγών είναι υψηλές και οι εκτιμήσεις είναι λιγότερο ακριβείς." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Λάθος λογαριασμός" }, - "alertSettingsUnconnectedAccount": { - "message": "Περιήγηση σε έναν ιστότοπο με έναν μη συνδεδεμένο επιλέγμενο λογαριασμό" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Αυτή η ειδοποίηση εμφανίζεται στο αναδυόμενο παράθυρο κατά την περιήγηση σε μια συνδεδεμένη web3 ιστοσελίδα, αλλά ο τρέχων επιλεγμένος λογαριασμός δεν είναι συνδεδεμένος." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Όταν μια ιστοσελίδα προσπαθεί να χρησιμοποιήσει το window.web3 API που έχει αφαιρεθεί" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Αυτή η ειδοποίηση εμφανίζεται στο αναδυόμενο παράθυρο όταν περιηγείστε σε μια ιστοσελίδα που προσπαθεί να χρησιμοποιήσει το window.web3 API που έχει αφαιρεθεί, και μπορεί, ως αποτέλεσμα, να μη λειτουργεί." - }, "alerts": { "message": "Ειδοποιήσεις" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Όροι Χρήσης της Δοκιμαστικής Έκδοσης" }, - "betaWalletCreationSuccessReminder1": { - "message": "Η δοκιμαστική έκδοση του MetaMask δεν μπορεί να ανακτήσει τη Μυστική Φράση Ανάκτησής σας." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Η δοκιμαστική έκδοση του MetaMask δεν θα σας ζητήσει ποτέ τη Μυστική Φράση Ανάκτησής σας." - }, "billionAbbreviation": { "message": "Δ", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Έχω ενημερωθεί για την ειδοποίηση και εξακολουθώ να θέλω να συνεχίσω" }, - "confirmAlertModalDetails": { - "message": "Εάν συνδεθείτε, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία. Ελέγξτε τις ειδοποιήσεις πριν συνεχίσετε." - }, - "confirmAlertModalTitle": { - "message": "Τα περιουσιακά σας στοιχεία μπορεί να κινδυνεύουν" - }, "confirmConnectCustodianRedirect": { "message": "Θα σας ανακατευθύνουμε στο $1 όταν κάνετε κλικ για να συνεχίσετε." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Το MetaMask είναι συνδεδεμένο σε αυτόν τον ιστότοπο, αλλά δεν έχουν συνδεθεί ακόμα λογαριασμοί" }, - "connectedWith": { - "message": "Συνδέεται με" - }, "connecting": { "message": "Σύνδεση" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Λεπτομέρειες χρεώσεων" }, - "fiat": { - "message": "Εντολή", - "description": "Exchange type" - }, "fileImportFail": { "message": "Η εισαγωγή αρχείων δεν λειτουργεί; Κάντε κλικ εδώ!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Αρχείο JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "Το MetaMask δεν συνδέεται με αυτόν τον ιστότοπο" }, - "noConversionDateAvailable": { - "message": "Δεν υπάρχει διαθέσιμη ημερομηνία μετατροπής νομίσματος" - }, "noConversionRateAvailable": { "message": "Δεν υπάρχει διαθέσιμη ισοτιμία μετατροπής" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Καρφιτσώστε το MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Οι ειδοποιήσεις ανίχνευσης για phishing βασίζονται στην επικοινωνία με το $1. Το jsDeliver θα έχει πρόσβαση στη διεύθυνση IP σας. Δείτε $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1Η", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "μη διαθέσιμη τιμή" }, - "primaryCurrencySetting": { - "message": "Κύριο νόμισμα" - }, - "primaryCurrencySettingDescription": { - "message": "Επιλέξτε εγχώριο για να δώσετε προτεραιότητα στην εμφάνιση των τιμών στο νόμισμα της αλυσίδας (π.χ. ETH). Επιλέξτε Παραστατικό για να δώσετε προτεραιότητα στην εμφάνιση τιμών στο επιλεγμένο παραστατικό νόμισμα." - }, "primaryType": { "message": "Βασικός τύπος" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Απορρίφθηκε" }, - "remember": { - "message": "Να θυμάστε:" - }, "remove": { "message": "Κατάργηση" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Δίκτυο δοκιμών Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "Το MetaMask χρησιμοποιεί αυτές τις αξιόπιστες υπηρεσίες τρίτων για να ενισχύσει τη χρηστικότητα και την ασφάλεια των προϊόντων." - }, "setApprovalForAll": { "message": "Ρύθμιση έγκρισης για όλους" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Η συναλλαγή σας ολοκληρώθηκε" }, - "smartTransactionTakingTooLong": { - "message": "Συγγνώμη για την αναμονή" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Εάν η συναλλαγή σας δεν ολοκληρωθεί εντός $1, θα ακυρωθεί και δεν θα χρεωθείτε με τέλη συναλλαγών.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Προσπαθήστε ξανά να κάνετε ανταλλαγή. Θα είμαστε εδώ για να σας προστατεύσουμε από παρόμοιους κινδύνους και την επόμενη φορά." }, - "stxEstimatedCompletion": { - "message": "Εκτιμώμενη ολοκλήρωση σε < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Η ανταλλαγή απέτυχε" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Αίτημα ενημέρωσης" }, - "updatedWithDate": { - "message": "Ενημερώθηκε $1" - }, "uploadDropFile": { "message": "Αφήστε το αρχείο σας εδώ" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "ο οδηγός μας σύνδεσης πορτοφολιού υλικού" }, - "walletCreationSuccessDetail": { - "message": "Προστατεύσατε με επιτυχία το πορτοφόλι σας. Διατηρήστε τη Μυστική Φράση Ανάκτησης ασφαλής και μυστική - είναι δική σας ευθύνη!" - }, - "walletCreationSuccessReminder1": { - "message": "Το MetaMask δεν μπορεί να ανακτήσει τη Μυστική Φράση Ανάκτησής σας." - }, - "walletCreationSuccessReminder2": { - "message": "Το MetaMask δεν θα σας ζητήσει ποτέ τη Μυστική Φράση Ανάκτησής σας." - }, - "walletCreationSuccessReminder3": { - "message": "$1 με οποιονδήποτε ή να διακινδυνεύστε τα χρήματά σας να κλαπούν", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Ποτέ μην μοιράζεστε τη Μυστική Φράση Ανάκτησης σας", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Επιτυχής δημιουργία πορτοφολιού" - }, "wantToAddThisNetwork": { "message": "Θέλετε να προσθέσετε αυτό το δίκτυο;" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e312be4794e5..e8b2625103d3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -162,6 +162,9 @@ "accountOptions": { "message": "Account options" }, + "accountPermissionToast": { + "message": "Account permissions updated" + }, "accountSelectionRequired": { "message": "You need to select an account!" }, @@ -174,6 +177,12 @@ "accountsConnected": { "message": "Accounts connected" }, + "accountsPermissionsTitle": { + "message": "See your accounts and suggest transactions" + }, + "accountsSmallCase": { + "message": "accounts" + }, "active": { "message": "Active" }, @@ -364,9 +373,6 @@ "advancedBaseGasFeeToolTip": { "message": "When your transaction gets included in the block, any difference between your max base fee and the actual base fee will be refunded. Total amount is calculated as max base fee (in GWEI) * gas limit." }, - "advancedConfiguration": { - "message": "Advanced configuration" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -392,6 +398,10 @@ "advancedPriorityFeeToolTip": { "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." }, + "aggregatedBalancePopover": { + "message": "This reflects the value of all tokens you own on a given network. If you prefer seeing this value in ETH or other currencies, go to $1.", + "description": "$1 represents the settings page" + }, "agreeTermsOfUse": { "message": "I agree to MetaMask's $1", "description": "$1 is the `terms` link" @@ -414,12 +424,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, @@ -435,8 +439,8 @@ "alertMessageGasTooLow": { "message": "To continue with this transaction, you’ll need to increase the gas limit to 21000 or higher." }, - "alertMessageInsufficientBalance": { - "message": "You do not have enough ETH in your account to pay for transaction fees." + "alertMessageInsufficientBalance2": { + "message": "You do not have enough ETH in your account to pay for network fees." }, "alertMessageNetworkBusy": { "message": "Gas prices are high and estimates are less accurate." @@ -492,18 +496,6 @@ "alertReasonWrongAccount": { "message": "Wrong account" }, - "alertSettingsUnconnectedAccount": { - "message": "Browsing a website with an unconnected account selected" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." - }, - "alertSettingsWeb3ShimUsage": { - "message": "When a website tries to use the removed window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." - }, "alerts": { "message": "Alerts" }, @@ -643,6 +635,12 @@ "assetOptions": { "message": "Asset options" }, + "assets": { + "message": "Assets" + }, + "assetsDescription": { + "message": "Autodetect tokens in your wallet, display NFTs, and get batched account balance updates" + }, "attemptSendingAssets": { "message": "You may lose your assets if you try to send them from another network. Transfer funds safely between networks by using a bridge." }, @@ -757,12 +755,6 @@ "betaTerms": { "message": "Beta Terms of use" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta can’t recover your Secret Recovery Phrase." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta will never ask you for your Secret Recovery Phrase." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -862,6 +854,9 @@ "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeSelectNetwork": { + "message": "Select network" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, @@ -1014,12 +1009,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "I have acknowledged the alert and still want to proceed" }, - "confirmAlertModalDetails": { - "message": "If you sign in, a third party known for scams might take all your assets. Please review the alerts before you proceed." - }, - "confirmAlertModalTitle": { - "message": "Your assets may be at risk" - }, "confirmConnectCustodianRedirect": { "message": "We will redirect you to $1 upon clicking continue." }, @@ -1068,6 +1057,9 @@ "confirmTitlePermitTokens": { "message": "Spending cap request" }, + "confirmTitleRevokeApproveTransaction": { + "message": "Remove permission" + }, "confirmTitleSIWESignature": { "message": "Sign-in request" }, @@ -1080,6 +1072,12 @@ "confirmTitleTransaction": { "message": "Transaction request" }, + "confirmationAlertModalDetails": { + "message": "To protect your assets and login information, we suggest you reject the request." + }, + "confirmationAlertModalTitle": { + "message": "This request is suspicious" + }, "confirmed": { "message": "Confirmed" }, @@ -1092,6 +1090,9 @@ "confusingEnsDomain": { "message": "We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam." }, + "congratulations": { + "message": "Congratulations!" + }, "connect": { "message": "Connect" }, @@ -1169,8 +1170,17 @@ "connectedSnaps": { "message": "Connected Snaps" }, - "connectedWith": { - "message": "Connected with" + "connectedWithAccount": { + "message": "$1 accounts connected", + "description": "$1 represents account length" + }, + "connectedWithAccountName": { + "message": "Connected with $1", + "description": "$1 represents account name" + }, + "connectedWithNetworks": { + "message": "$1 networks connected", + "description": "$1 represents network length" }, "connecting": { "message": "Connecting" @@ -1199,6 +1209,9 @@ "connectingToSepolia": { "message": "Connecting to Sepolia test network" }, + "connectionDescription": { + "message": "This site wants to" + }, "connectionFailed": { "message": "Connection failed" }, @@ -1319,7 +1332,7 @@ "message": "CryptoCompare" }, "currencyConversion": { - "message": "Currency conversion" + "message": "Currency" }, "currencyRateCheckToggle": { "message": "Show balance and token price checker" @@ -1531,12 +1544,41 @@ "defaultRpcUrl": { "message": "Default RPC URL" }, + "defaultSettingsSubTitle": { + "message": "MetaMask uses default settings to best balance safety and ease of use. Change these settings to further increase your privacy." + }, + "defaultSettingsTitle": { + "message": "Default settings" + }, "delete": { "message": "Delete" }, "deleteContact": { "message": "Delete contact" }, + "deleteMetaMetricsData": { + "message": "Delete MetaMetrics data" + }, + "deleteMetaMetricsDataDescription": { + "message": "This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "This request can't be completed right now due to an analytics system server issue, please try again later" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "We are unable to delete this data right now" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "We are about to remove all your MetaMetrics data. Are you sure?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Delete MetaMetrics data?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "You initiated this action on $1. This process can take up to 30 days. View the $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "If you delete this network, you will need to add it again to view your assets in this network" }, @@ -1591,16 +1633,16 @@ "disconnectAllAccountsText": { "message": "accounts" }, + "disconnectAllDescription": { + "message": "If you disconnect from $1, you’ll need to reconnect your accounts and networks to use this site again.", + "description": "$1 represents the website hostname" + }, "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" }, "disconnectPrompt": { "message": "Disconnect $1" @@ -1766,6 +1808,9 @@ "editPermission": { "message": "Edit permission" }, + "editPermissions": { + "message": "Edit permissions" + }, "editSpeedUpEditGasFeeModalTitle": { "message": "Edit speed up gas fee" }, @@ -1778,6 +1823,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" }, @@ -1993,10 +2041,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -2082,6 +2126,9 @@ "message": "This gas fee has been suggested by $1. Overriding this may cause a problem with your transaction. Please reach out to $1 if you have questions.", "description": "$1 represents the Dapp's origin" }, + "gasFee": { + "message": "Gas fee" + }, "gasIsETH": { "message": "Gas is $1 " }, @@ -2152,6 +2199,9 @@ "generalCameraErrorTitle": { "message": "Something went wrong...." }, + "generalDescription": { + "message": "Sync settings across devices, select network preferences, and track token data" + }, "genericExplorerView": { "message": "View account on $1" }, @@ -2315,6 +2365,10 @@ "id": { "message": "ID" }, + "ifYouGetLockedOut": { + "message": "If you get locked out of the app or get a new device, you will lose your funds. Be sure to back up your Secret Recovery Phrase in $1 ", + "description": "$1 is the menu path to be shown with font weight bold" + }, "ignoreAll": { "message": "Ignore all" }, @@ -2396,6 +2450,9 @@ "inYourSettings": { "message": "in your Settings" }, + "included": { + "message": "included" + }, "infuraBlockedNotification": { "message": "MetaMask is unable to connect to the blockchain host. Review possible reasons $1.", "description": "$1 is a clickable link with with text defined by the 'here' key" @@ -2562,13 +2619,14 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON File", "description": "format for importing an account" }, + "keepReminderOfSRP": { + "message": "Keep a reminder of your Secret Recovery Phrase somewhere safe. If you lose it, no one can help you get it back. Even worse, you won’t be able access to your wallet ever again. $1", + "description": "$1 is a learn more link" + }, "keyringAccountName": { "message": "Account name" }, @@ -2627,6 +2685,9 @@ "message": "Learn how to $1", "description": "$1 is link to cancel or speed up transactions" }, + "learnHow": { + "message": "Learn how" + }, "learnMore": { "message": "learn more" }, @@ -2634,6 +2695,9 @@ "message": "Want to $1 about gas?", "description": "$1 will be replaced by the learnMore translation key" }, + "learnMoreAboutPrivacy": { + "message": "Learn more about privacy best practices." + }, "learnMoreKeystone": { "message": "Learn More" }, @@ -2785,6 +2849,9 @@ "message": "Make sure nobody is looking", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "manageDefaultSettings": { + "message": "Manage default settings" + }, "marketCap": { "message": "Market cap" }, @@ -2828,6 +2895,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "The connection status button shows if the website you’re visiting is connected to your currently selected account." }, + "metaMetricsIdNotAvailableError": { + "message": "Since you've never opted into MetaMetrics, there's no data to delete here." + }, "metadataModalSourceTooltip": { "message": "$1 is hosted on npm and $2 is this Snap’s unique identifier.", "description": "$1 is the snap name and $2 is the snap NPM id." @@ -2904,6 +2974,14 @@ "more": { "message": "more" }, + "moreAccounts": { + "message": "+ $1 more accounts", + "description": "$1 is the number of accounts" + }, + "moreNetworks": { + "message": "+ $1 more networks", + "description": "$1 is the number of networks" + }, "multichainAddEthereumChainConfirmationDescription": { "message": "You're adding this network to MetaMask and giving this site permission to use it." }, @@ -2977,10 +3055,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" @@ -3086,6 +3160,9 @@ "networkOptions": { "message": "Network options" }, + "networkPermissionToast": { + "message": "Network permissions updated" + }, "networkProvider": { "message": "Network provider" }, @@ -3124,6 +3201,9 @@ "networks": { "message": "Networks" }, + "networksSmallCase": { + "message": "networks" + }, "nevermind": { "message": "Nevermind" }, @@ -3242,9 +3322,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -3648,10 +3725,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Pin MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -4031,6 +4104,9 @@ "permitSimulationDetailInfo": { "message": "You're giving the spender permission to spend this many tokens from your account." }, + "permittedChainToastUpdate": { + "message": "$1 has access to $2." + }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." }, @@ -4082,12 +4158,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -4318,8 +4388,11 @@ "rejected": { "message": "Rejected" }, - "remember": { - "message": "Remember:" + "rememberSRPIfYouLooseAccess": { + "message": "Remember, if you lose your Secret Recovery Phrase, you lose access to your wallet. $1 to keep this set of words safe so you can always access your funds." + }, + "reminderSet": { + "message": "Reminder set!" }, "remove": { "message": "Remove" @@ -4400,6 +4473,13 @@ "requestNotVerifiedError": { "message": "Because of an error, this request was not verified by the security provider. Proceed with caution." }, + "requestingFor": { + "message": "Requesting for" + }, + "requestingForAccount": { + "message": "Requesting for $1", + "description": "Name of Account" + }, "requestsAwaitingAcknowledgement": { "message": "requests waiting to be acknowledged" }, @@ -4488,6 +4568,12 @@ "revealTheSeedPhrase": { "message": "Reveal seed phrase" }, + "review": { + "message": "Review" + }, + "reviewAlert": { + "message": "Review alert" + }, "reviewAlerts": { "message": "Review alerts" }, @@ -4513,6 +4599,9 @@ "revokePermission": { "message": "Revoke permission" }, + "revokeSimulationDetailsDesc": { + "message": "You're removing someone's permission to spend tokens from your account." + }, "revokeSpendingCap": { "message": "Revoke spending cap for your $1", "description": "$1 is a token symbol" @@ -4575,6 +4664,12 @@ "securityAndPrivacy": { "message": "Security & privacy" }, + "securityDescription": { + "message": "Reduce your chances of joining unsafe networks and protect your accounts" + }, + "securityPrivacyPath": { + "message": "Settings > Security & Privacy." + }, "securityProviderPoweredBy": { "message": "Powered by $1", "description": "The security provider that is providing data" @@ -4741,9 +4836,6 @@ "sepolia": { "message": "Sepolia test network" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask uses these trusted third-party services to enhance product usability and safety." - }, "setApprovalForAll": { "message": "Set approval for all" }, @@ -4760,6 +4852,9 @@ "settings": { "message": "Settings" }, + "settingsOptimisedForEaseOfUseAndSecurity": { + "message": "Settings are optimised for ease of use and security. Change these any time." + }, "settingsSearchMatchingNotFound": { "message": "No matching results found." }, @@ -4806,6 +4901,9 @@ "showMore": { "message": "Show more" }, + "showNativeTokenAsMainBalance": { + "message": "Show native token as main balance" + }, "showNft": { "message": "Show NFT" }, @@ -4946,18 +5044,11 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" }, - "smartTransactionTakingTooLong": { - "message": "Sorry for the wait" - }, - "smartTransactionTakingTooLongDescription": { - "message": "If your transaction is not finalized within $1, it will be canceled and you will not be charged for gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5159,6 +5250,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" }, @@ -5383,10 +5484,6 @@ "stxCancelledSubDescription": { "message": "Try your swap again. We’ll be here to protect you against similar risks next time." }, - "stxEstimatedCompletion": { - "message": "Estimated completion in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap failed" }, @@ -5595,12 +5692,22 @@ "message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.", "description": "$1 is the selected network, e.g. Ethereum or BSC" }, + "swapGasIncludedTooltipExplanation": { + "message": "This quote incorporates gas fees by adjusting the token amount sent or received. You may receive ETH in a separate transaction on your activity list." + }, + "swapGasIncludedTooltipExplanationLinkText": { + "message": "Learn more about gas fees" + }, "swapHighSlippage": { "message": "High slippage" }, "swapHighSlippageWarning": { "message": "Slippage amount is very high." }, + "swapIncludesGasAndMetaMaskFee": { + "message": "Includes gas and a $1% MetaMask fee", + "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." + }, "swapIncludesMMFee": { "message": "Includes a $1% MetaMask fee.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." @@ -6331,9 +6438,6 @@ "updatedRpcForNetworks": { "message": "Network RPCs Updated" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, @@ -6469,25 +6573,9 @@ "walletConnectionGuide": { "message": "our hardware wallet connection guide" }, - "walletCreationSuccessDetail": { - "message": "You’ve successfully protected your wallet. Keep your Secret Recovery Phrase safe and secret -- it’s your responsibility!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask can’t recover your Secret Recovery Phrase." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask will never ask you for your Secret Recovery Phrase." - }, - "walletCreationSuccessReminder3": { - "message": "$1 with anyone or risk your funds being stolen", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Never share your Secret Recovery Phrase", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Wallet creation successful" + "walletProtectedAndReadyToUse": { + "message": "Your wallet is protected and ready to use. You can find your Secret Recovery Phrase in $1 ", + "description": "$1 is the menu path to be shown with font weight bold" }, "wantToAddThisNetwork": { "message": "Want to add this network?" @@ -6590,6 +6678,9 @@ "yourBalance": { "message": "Your balance" }, + "yourBalanceIsAggregated": { + "message": "Your balance is aggregated" + }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, @@ -6602,6 +6693,9 @@ "yourTransactionJustConfirmed": { "message": "We weren't able to cancel your transaction before it was confirmed on the blockchain." }, + "yourWalletIsReady": { + "message": "Your wallet is ready" + }, "zeroGasPriceOnSpeedUpError": { "message": "Zero gas price on speed up" } diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 63fadc1b03e0..25cb6cd3df29 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -400,12 +400,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, @@ -418,9 +412,6 @@ "alertMessageGasTooLow": { "message": "To continue with this transaction, you’ll need to increase the gas limit to 21000 or higher." }, - "alertMessageInsufficientBalance": { - "message": "You do not have enough ETH in your account to pay for transaction fees." - }, "alertMessageNetworkBusy": { "message": "Gas prices are high and estimates are less accurate." }, @@ -475,18 +466,6 @@ "alertReasonWrongAccount": { "message": "Wrong account" }, - "alertSettingsUnconnectedAccount": { - "message": "Browsing a website with an unconnected account selected" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." - }, - "alertSettingsWeb3ShimUsage": { - "message": "When a website tries to use the removed window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." - }, "alerts": { "message": "Alerts" }, @@ -997,12 +976,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "I have acknowledged the alert and still want to proceed" }, - "confirmAlertModalDetails": { - "message": "If you sign in, a third party known for scams might take all your assets. Please review the alerts before you proceed." - }, - "confirmAlertModalTitle": { - "message": "Your assets may be at risk" - }, "confirmConnectCustodianRedirect": { "message": "We will redirect you to $1 upon clicking continue." }, @@ -1940,10 +1913,6 @@ "feeDetails": { "message": "Fee details" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" @@ -3172,9 +3141,6 @@ "noConnectedAccountTitle": { "message": "MetaMask isn’t connected to this site" }, - "noConversionDateAvailable": { - "message": "No currency conversion date available" - }, "noConversionRateAvailable": { "message": "No conversion rate available" }, @@ -4013,12 +3979,6 @@ "priceUnavailable": { "message": "price unavailable" }, - "primaryCurrencySetting": { - "message": "Primary currency" - }, - "primaryCurrencySettingDescription": { - "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." - }, "primaryType": { "message": "Primary type" }, @@ -4840,7 +4800,7 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" @@ -6231,9 +6191,6 @@ "updateRequest": { "message": "Update request" }, - "updatedWithDate": { - "message": "Updated $1" - }, "uploadDropFile": { "message": "Drop your file here" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 67143b9609d3..0b4dc1432ac8 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Cuando su transacción se incluya en el bloque, se reembolsará cualquier diferencia entre su tarifa base máxima y la tarifa base real. El importe total se calcula como tarifa base máxima (en GWEI) * límite de gas." }, - "advancedConfiguration": { - "message": "Configuración avanzada" - }, "advancedDetailsDataDesc": { "message": "Datos" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Actualizar opciones de gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si aprueba esta solicitud, un tercero conocido por estafas podría quedarse con todos sus activos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "¡Alertas múltiples!" - }, "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Para continuar con esta transacción, deberá aumentar el límite de gas a 21000 o más." }, - "alertMessageInsufficientBalance": { - "message": "No tiene suficiente ETH en su cuenta para pagar las tarifas de transacción." - }, "alertMessageNetworkBusy": { "message": "Los precios del gas son altos y las estimaciones son menos precisas." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Cuenta incorrecta" }, - "alertSettingsUnconnectedAccount": { - "message": "Explorando un sitio web con una cuenta no conectada seleccionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio conectado de Web3, pero la cuenta actualmente seleccionada no está conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Cuando un sitio web intenta utilizar la API de window.web3 que se eliminó" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio que intenta utilizar la API de window.web3 que se eliminó y que puede que no funcione." - }, "alerts": { "message": "Alertas" }, @@ -718,12 +694,6 @@ "betaTerms": { "message": "Términos de uso de beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask beta no puede recuperar su frase secreta de recuperación." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask beta nunca le pedirá su frase secreta de recuperación." - }, "billionAbbreviation": { "message": "mm", "description": "Shortened form of 'billion'" @@ -969,12 +939,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Soy consciente de la alerta y aun así deseo continuar" }, - "confirmAlertModalDetails": { - "message": "Si inicia sesión, un tercero conocido por estafas podría quedarse con todos sus activos. Revise las alertas antes de continuar." - }, - "confirmAlertModalTitle": { - "message": "Sus activos podrían estar en riesgo" - }, "confirmConnectCustodianRedirect": { "message": "Lo redirigiremos a $1 al hacer clic en continuar." }, @@ -1097,9 +1061,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask está conectado a este sitio, pero aún no hay cuentas conectadas" }, - "connectedWith": { - "message": "Conectado con" - }, "connecting": { "message": "Conectando" }, @@ -1516,14 +1477,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" }, @@ -1876,10 +1829,6 @@ "feeDetails": { "message": "Detalles de la tarifa" }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? Haga clic aquí.", "description": "Helps user import their account from a JSON file" @@ -2439,9 +2388,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Archivo JSON", "description": "format for importing an account" @@ -2848,10 +2794,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" @@ -3095,9 +3037,6 @@ "noConnectedAccountTitle": { "message": "MetaMask no está conectado a este sitio" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -3498,10 +3437,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Fijar MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Las alertas de detección de phishing se basan en la comunicación con $1. jsDeliver tendrá acceso a su dirección IP. Ver 2$.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 d", "description": "Shortened form of '1 day'" @@ -3918,12 +3853,6 @@ "priceUnavailable": { "message": "precio no disponible" }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "primaryType": { "message": "Tipo principal" }, @@ -4148,9 +4077,6 @@ "rejected": { "message": "Rechazado" }, - "remember": { - "message": "Recuerde:" - }, "remove": { "message": "Quitar" }, @@ -4558,9 +4484,6 @@ "sepolia": { "message": "Red de prueba Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utiliza estos servicios de terceros de confianza para mejorar la usabilidad y la seguridad de los productos." - }, "setApprovalForAll": { "message": "Establecer aprobación para todos" }, @@ -4750,13 +4673,6 @@ "smartTransactionSuccess": { "message": "Su transacción está completa" }, - "smartTransactionTakingTooLong": { - "message": "Disculpe la espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si su transacción no finaliza en $1, se cancelará y no se le cobrará el gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transacciones inteligentes" }, @@ -5173,10 +5089,6 @@ "stxCancelledSubDescription": { "message": "Intente su swap nuevamente. Estaremos aquí para protegerlo contra riesgos similares la próxima vez." }, - "stxEstimatedCompletion": { - "message": "Finalización estimada en < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Error al intercambiar" }, @@ -6088,9 +6000,6 @@ "updateRequest": { "message": "Solicitud de actualización" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "uploadDropFile": { "message": "Ingrese su archivo aquí" }, @@ -6226,26 +6135,6 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión del monedero físico" }, - "walletCreationSuccessDetail": { - "message": "Ha protegido con éxito su monedero. Mantenga su frase secreta de recuperación a salvo y en secreto: ¡es su responsabilidad!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask no puede recuperar su frase secreta de recuperación." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask nunca le pedirá su frase secreta de recuperación." - }, - "walletCreationSuccessReminder3": { - "message": "$1 con nadie o se arriesga a que le roben los fondos", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca comparta su frase secreta de recuperación", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Creación exitosa del monedero" - }, "wantToAddThisNetwork": { "message": "¿Desea añadir esta red?" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 1b8bc945343b..cd980aaa99c2 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -157,18 +157,6 @@ "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Explorando un sitio web con una cuenta no conectada seleccionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio conectado de Web3, pero la cuenta actualmente seleccionada no está conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Cuando un sitio web intenta utilizar la API de window.web3 que se eliminó" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esta alerta aparece en la ventana emergente cuando explora un sitio que intenta utilizar la API de window.web3 que se eliminó y que puede que no funcione." - }, "alerts": { "message": "Alertas" }, @@ -782,10 +770,6 @@ "feeAssociatedRequest": { "message": "Esta solicitud tiene asociada una tarifa." }, - "fiat": { - "message": "Fiduciaria", - "description": "Exchange type" - }, "fileImportFail": { "message": "¿No funciona la importación del archivo? ¡Haga clic aquí!", "description": "Helps user import their account from a JSON file" @@ -1049,9 +1033,6 @@ "invalidSeedPhrase": { "message": "Frase secreta de recuperación no válida" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Archivo JSON", "description": "format for importing an account" @@ -1321,9 +1302,6 @@ "noAccountsFound": { "message": "No se encuentran cuentas para la consulta de búsqueda determinada" }, - "noConversionDateAvailable": { - "message": "No hay fecha de conversión de moneda disponible" - }, "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, @@ -1489,12 +1467,6 @@ "prev": { "message": "Ant." }, - "primaryCurrencySetting": { - "message": "Moneda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Seleccione Nativa para dar prioridad a mostrar los valores en la moneda nativa de la cadena (p. ej., ETH). Seleccione Fiduciaria para dar prioridad a mostrar los valores en la moneda fiduciaria seleccionada." - }, "priorityFee": { "message": "Tarifa de prioridad" }, @@ -1580,9 +1552,6 @@ "rejected": { "message": "Rechazado" }, - "remember": { - "message": "Recuerde:" - }, "remove": { "message": "Quitar" }, @@ -1745,9 +1714,6 @@ "message": "Enviando $1", "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utiliza estos servicios de terceros de confianza para mejorar la usabilidad y la seguridad de los productos." - }, "settings": { "message": "Configuración" }, @@ -2405,9 +2371,6 @@ "message": "El envío de tokens coleccionables (ERC-721) no se admite actualmente", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "$1 actualizado" - }, "urlErrorMsg": { "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, @@ -2470,26 +2433,6 @@ "walletConnectionGuide": { "message": "nuestra guía de conexión de la cartera de hardware" }, - "walletCreationSuccessDetail": { - "message": "Ha protegido con éxito su cartera. Mantenga su frase secreta de recuperación a salvo y en secreto: ¡es su responsabilidad!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask no puede recuperar su frase secreta de recuperación." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask nunca le pedirá su frase secreta de recuperación." - }, - "walletCreationSuccessReminder3": { - "message": "$1 con nadie o se arriesga a que le roben los fondos", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca comparta su frase secreta de recuperación", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Creación exitosa de la cartera" - }, "web3ShimUsageNotification": { "message": "Parece que el sitio web actual intentó utilizar la API de window.web3 que se eliminó. Si el sitio no funciona, haga clic en $1 para obtener más información.", "description": "$1 is a clickable link." diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index acebcc9091da..38125572b8ec 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -498,12 +498,6 @@ "prev": { "message": "Eelm" }, - "primaryCurrencySetting": { - "message": "Põhivaluuta" - }, - "primaryCurrencySettingDescription": { - "message": "Valige omavääring, et prioriseerida vääringu kuvamist ahela omavääringus (nt ETH). Valige Fiat, et prioriseerida vääringu kuvamist valitud fiat-vääringus." - }, "privacyMsg": { "message": "privaatsuspoliitika" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Detsentraliseeritud veeb ootab" }, - "updatedWithDate": { - "message": "Värskendatud $1" - }, "urlErrorMsg": { "message": "URI-d nõuavad sobivat HTTP/HTTPS-i prefiksit." }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index c9c1bafdc7bf..c1a4deb11ce4 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "سریع" }, - "fiat": { - "message": "حکم قانونی", - "description": "Exchange type" - }, "fileImportFail": { "message": "وارد کردن فایل کار نمیکند؟ اینجا کلیک نمایید!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "قبلی" }, - "primaryCurrencySetting": { - "message": "واحد پول اصلی" - }, - "primaryCurrencySettingDescription": { - "message": "برای اولویت دهی نمایش قیمت ها در واحد پولی اصلی زنجیره (مثلًا ETH)، اصلی را انتخاب کنید. برای اولویت دهی نمایش قیمت ها در فیات واحد پولی شما، فیات را انتخاب کنید." - }, "privacyMsg": { "message": "خط‌مشی رازداری" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "وب غیر متمرکز شده انتظار میکشد" }, - "updatedWithDate": { - "message": "بروزرسانی شد 1$1" - }, "urlErrorMsg": { "message": "URl ها نیازمند پیشوند مناسب HTTP/HTTPS اند." }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 1c9cdb7c7a43..89e274dd4466 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Nopea" }, - "fiat": { - "message": "Kiinteä", - "description": "Exchange type" - }, "fileImportFail": { "message": "Eikö tiedoston tuominen onnistu? Klikkaa tästä!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "Aiemp." }, - "primaryCurrencySetting": { - "message": "Ensisijainen valuutta" - }, - "primaryCurrencySettingDescription": { - "message": "Valitse natiivivaihtoehto näyttääksesi arvot ensisijaisesti ketjun natiivivaluutalla (esim. ETH). Valitse oletusmääräys asettaaksesi valitsemasi oletusvaluutan ensisijaiseksi." - }, "privacyMsg": { "message": "Tietosuojakäytäntö" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "Hajautettu verkko odottaa" }, - "updatedWithDate": { - "message": "$1 päivitetty" - }, "urlErrorMsg": { "message": "URI:t vaativat asianmukaisen HTTP/HTTPS-etuliitteen." }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index e08c88bd7ffa..498c1878fd10 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -436,12 +436,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para bigyang priyoridad ang pagpapakita ng mga halaga sa native currency ng chain (hal. ETH). Piliin ang Fiat para bigyang priyoridad ang pagpapakita ng mga halaga sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Privacy" }, @@ -677,9 +671,6 @@ "unlockMessage": { "message": "Naghihintay ang decentralized web" }, - "updatedWithDate": { - "message": "Na-update ang $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URI ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index e63594a57539..780300aebe08 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Lorsque votre transaction est intégrée au bloc, toute différence entre vos frais de base maximaux et les frais de base réels vous sera remboursée. Le montant total est calculé comme suit : frais de base maximaux (en GWEI) × limite de carburant." }, - "advancedConfiguration": { - "message": "Configuration avancée" - }, "advancedDetailsDataDesc": { "message": "Données" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Mettre à jour les options de gaz" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Plusieurs alertes !" - }, "alertDisableTooltip": { "message": "Vous pouvez modifier ceci dans « Paramètres > Alertes »" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Pour effectuer cette transaction, vous devez augmenter la limite de gaz à 21 000 ou plus." }, - "alertMessageInsufficientBalance": { - "message": "Vous n’avez pas assez d’ETH sur votre compte pour payer les frais de transaction." - }, "alertMessageNetworkBusy": { "message": "Les prix du gaz sont élevés et les estimations sont moins précises." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Mauvais compte" }, - "alertSettingsUnconnectedAccount": { - "message": "Navigation sur un site Web avec un compte non connecté sélectionné" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Cette alerte s’affiche dans le pop-up lorsque vous naviguez sur un site web3 connecté, mais que le compte actuellement sélectionné n’est pas connecté." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Lorsqu’un site Web tente d’utiliser l’API window.web3 supprimée" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Cette alerte s’affiche dans le pop-up lorsque vous naviguez sur un site qui tente d’utiliser l’API window.web3 supprimée, et qui peut par conséquent être défaillant." - }, "alerts": { "message": "Alertes" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Conditions d’utilisation de la version bêta" }, - "betaWalletCreationSuccessReminder1": { - "message": "La version bêta de MetaMask ne peut pas retrouver votre phrase secrète de récupération." - }, - "betaWalletCreationSuccessReminder2": { - "message": "La version bêta de MetaMask ne vous demandera jamais votre phrase secrète de récupération." - }, "billionAbbreviation": { "message": "Mrd", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "J’ai pris connaissance de l’alerte, mais je souhaite quand même continuer" }, - "confirmAlertModalDetails": { - "message": "Si vous vous connectez, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs. Veuillez examiner les alertes avant de continuer." - }, - "confirmAlertModalTitle": { - "message": "Vous risquez de perdre tout ou partie de vos actifs" - }, "confirmConnectCustodianRedirect": { "message": "Nous vous redirigerons vers $1 une fois que vous cliquerez sur « Continuer »." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask est connecté à ce site, mais aucun compte n’est encore connecté" }, - "connectedWith": { - "message": "Connecté avec" - }, "connecting": { "message": "Connexion…" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Détails des frais" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "L’importation de fichier ne fonctionne pas ? Cliquez ici !", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Fichier JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask n’est pas connecté à ce site" }, - "noConversionDateAvailable": { - "message": "Aucune date de conversion des devises n’est disponible" - }, "noConversionRateAvailable": { "message": "Aucun taux de conversion disponible" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Épingler MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Les alertes de détection d’hameçonnage reposent sur la communication avec $1. jsDeliver aura accès à votre adresse IP. Voir $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 j", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "prix non disponible" }, - "primaryCurrencySetting": { - "message": "Devise principale" - }, - "primaryCurrencySettingDescription": { - "message": "Sélectionnez « natif » pour donner la priorité à l’affichage des valeurs dans la devise native de la chaîne (par ex. ETH). Sélectionnez « fiduciaire » pour donner la priorité à l’affichage des valeurs dans la devise de votre choix." - }, "primaryType": { "message": "Type principal" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Rejeté" }, - "remember": { - "message": "Rappel :" - }, "remove": { "message": "Supprimer" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Réseau de test Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask utilise ces services tiers de confiance pour améliorer la convivialité et la sécurité des produits." - }, "setApprovalForAll": { "message": "Définir l’approbation pour tous" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Votre transaction est terminée" }, - "smartTransactionTakingTooLong": { - "message": "Désolé de vous avoir fait attendre" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si votre transaction n’est pas finalisée dans un délai de $1, elle sera annulée et les frais de gaz ne vous seront pas facturés.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transactions intelligentes" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Réessayez le swap. Nous serons là pour vous protéger contre des risques similaires la prochaine fois." }, - "stxEstimatedCompletion": { - "message": "Délai estimé < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Échec du swap" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Demande de mise à jour" }, - "updatedWithDate": { - "message": "Mis à jour $1" - }, "uploadDropFile": { "message": "Déposez votre fichier ici" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "notre guide de connexion des portefeuilles matériels" }, - "walletCreationSuccessDetail": { - "message": "Votre portefeuille est bien protégé. Conservez votre phrase secrète de récupération en sécurité et en toute discrétion. C’est votre responsabilité !" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask ne peut pas restaurer votre phrase secrète de récupération." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask ne vous demandera jamais votre phrase secrète de récupération." - }, - "walletCreationSuccessReminder3": { - "message": "$1 avec n’importe qui, sinon vous risquez de voir vos fonds subtilisés", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Ne partagez jamais votre phrase secrète de récupération", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Portefeuille créé avec succès" - }, "wantToAddThisNetwork": { "message": "Voulez-vous ajouter ce réseau ?" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 9d118e31c098..413bf21d586b 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "מהיר" }, - "fiat": { - "message": "פיאט", - "description": "Exchange type" - }, "fileImportFail": { "message": "ייבוא הקובץ לא עובד? לחצ/י כאן!", "description": "Helps user import their account from a JSON file" @@ -505,12 +501,6 @@ "prev": { "message": "הקודם" }, - "primaryCurrencySetting": { - "message": "מטבע ראשי" - }, - "primaryCurrencySettingDescription": { - "message": "בחר/י 'מקומי' כדי לתעדף הצגת ערכים במטבע המקומי של הצ'יין (למשל ETH). בחר/י פיאט כדי לתעדף הצגת ערכים במטבע הפיאט שבחרת." - }, "privacyMsg": { "message": "מדיניות הפרטיות" }, @@ -762,9 +752,6 @@ "unlockMessage": { "message": "הרשת המבוזרת מחכה" }, - "updatedWithDate": { - "message": "עודכן $1" - }, "urlErrorMsg": { "message": "כתובות URI דורשות את קידומת HTTP/HTTPS המתאימה." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index f75c0279d401..8a3744a255f5 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "जब आपका ट्रांसेक्शन ब्लॉक में शामिल हो जाता है, तो आपके अधिकतम बेस फ़ीस और वास्तविक बेस फ़ीस के बीच का कोई भी अंतर वापस कर दिया जाता है। कुल अमाउंट को अधिकतम बेस फ़ीस (GWEI में) * गैस लिमिट के रुप में कैलकुलेट किया जाता है।" }, - "advancedConfiguration": { - "message": "एडवांस्ड कॉन्फ़िगरेशन" - }, "advancedDetailsDataDesc": { "message": "डेटा" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "गैस के विकल्प को अपडेट करें" }, - "alertBannerMultipleAlertsDescription": { - "message": "यदि आप इस रिक्वेस्ट को एप्रूव करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" - }, - "alertBannerMultipleAlertsTitle": { - "message": "एकाधिक एलर्ट!" - }, "alertDisableTooltip": { "message": "इसे \"सेटिंग > अलर्ट\" में बदला जा सकता है" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "इस ट्रांसेक्शन को जारी रखने के लिए, आपको गैस लिमिट को 21000 या अधिक तक बढ़ाना होगा।" }, - "alertMessageInsufficientBalance": { - "message": "ट्रांसेक्शन फीस का भुगतान करने के लिए आपके अकाउंट में पर्याप्त ETH नहीं है।" - }, "alertMessageNetworkBusy": { "message": "गैस प्राइसें अधिक हैं और अनुमान कम सटीक हैं।" }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "गलत अकाउंट" }, - "alertSettingsUnconnectedAccount": { - "message": "जो कनेक्टेड नहीं है वह अकाउंट चुनकर कोई वेबसाइट ब्राउज़ करना" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "यह चेतावनी पॉपअप में तब दिखाई जाती है, जब आप कनेक्टेड web3 साइट ब्राउज़ कर रहे होते हैं, लेकिन वर्तमान में चुना गया अकाउंट कनेक्ट नहीं होता है।" - }, - "alertSettingsWeb3ShimUsage": { - "message": "जब कोई वेबसाइट हटाए गए window.web3 API का इस्तेमाल करने की कोशिश करती है" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "यह एलर्ट पॉपअप में तब दिखाया जाता है, जब आप ऐसी साइट ब्राउज़ कर रहे होते हैं, जो हटाए गए window.web3 API का इस्तेमाल करने की कोशिश करती है और परिणामस्वरूप उसमें गड़बड़ी आ सकती है।" - }, "alerts": { "message": "चेतावनियां" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "बीटा के इस्तेमाल की शर्तें" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask बीटा आपका सीक्रेट रिकवरी फ्रेज़ रिकवर नहीं कर सकता।" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask बीटा आपसे आपका गुप्त रिकवरी वाक्यांश कभी नहीं मांगेगा।" - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "मैंने एलर्ट को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" }, - "confirmAlertModalDetails": { - "message": "यदि आप साइन इन करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है। कृपया आगे बढ़ने से पहले एलर्ट की समीक्षा करें।" - }, - "confirmAlertModalTitle": { - "message": "आपके एसेट खतरे में हो सकते हैं" - }, "confirmConnectCustodianRedirect": { "message": "'जारी रखें' पर क्लिक करने पर हम आपको $1 पर रीडायरेक्ट कर देंगे।" }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask इस साइट से कनेक्टेड है, लेकिन अभी तक कोई अकाउंट कनेक्ट नहीं किया गया है" }, - "connectedWith": { - "message": "से कनेक्ट किया गया" - }, "connecting": { "message": "कनेक्ट किया जा रहा है" }, @@ -1519,14 +1480,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 डिस्कनेक्ट करें" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "फ़ीस का ब्यौरा" }, - "fiat": { - "message": "फिएट", - "description": "Exchange type" - }, "fileImportFail": { "message": "फाइल इम्पोर्ट काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "जैज़आइकन्स" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON फाइल", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask इस साइट से कनेक्टेड नहीं है।" }, - "noConversionDateAvailable": { - "message": "कोई करेंसी कन्वर्शन तारीख उपलब्ध नहीं है" - }, "noConversionRateAvailable": { "message": "कोई भी कन्वर्शन दर उपलब्ध नहीं है" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional को पिन करें" }, - "onboardingUsePhishingDetectionDescription": { - "message": "फिशिंग डिटेक्शन अलर्ट $1 के साथ संचार पर निर्भर करते हैं। jsDeliver की पहुंच आपके IP एड्रेस तक होगी। $2 देखें।", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "प्राइस अनुपलब्ध है" }, - "primaryCurrencySetting": { - "message": "प्राथमिक मुद्रा" - }, - "primaryCurrencySettingDescription": { - "message": "चेन की ओरिजिनल करेंसी (जैसे ETH) में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए ओरिजिनल को चुनें। अपनी चुना गया फिएट करेंसी में प्रदर्शित वैल्यूज़ को प्राथमिकता देने के लिए फिएट को चुनें।" - }, "primaryType": { "message": "प्राइमरी टाइप" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "रिजेक्ट" }, - "remember": { - "message": "याद रखें:" - }, "remove": { "message": "हटाएं" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia टेस्ट नेटवर्क" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask उत्पाद की उपयोगिता और सुरक्षा को बढ़ाने के लिए इन विश्वसनीय तीसरे-पक्ष की सेवाओं का इस्तेमाल करता है।" - }, "setApprovalForAll": { "message": "सभी के लिए स्वीकृति सेट करें" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "आपका ट्रांसेक्शन पूरा हो गया है" }, - "smartTransactionTakingTooLong": { - "message": "माफ़ी चाहते हैं कि आपको इंतज़ार करना पड़ा" - }, - "smartTransactionTakingTooLongDescription": { - "message": "यदि आपका ट्रांसेक्शन $1 के भीतर फाइनलाइज़ नहीं होता है, तो इसे कैंसिल कर दिया जाएगा और आपसे गैस फ़ीस नहीं ली जाएगी।", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "अपना स्वैप फिर से कोशिश करें। अगली बार भी इस तरह के जोखिमों से आपको बचाने के लिए हम यहां होंगे।" }, - "stxEstimatedCompletion": { - "message": "<$1 में पूरा होने का अनुमान", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "स्वैप नहीं हो पाया" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "अपडेट का अनुरोध" }, - "updatedWithDate": { - "message": "अपडेट किया गया $1" - }, "uploadDropFile": { "message": "अपनी फ़ाइल यहां छोड़ें" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "हमारी hardware wallet कनेक्शन गाइड" }, - "walletCreationSuccessDetail": { - "message": "आपने अपने वॉलेट को सफलतापूर्वक सुरक्षित कर लिया है। अपने सीक्रेट रिकवरी फ्रेज को सुरक्षित और गुप्त रखें -- यह आपकी जिम्मेदारी है!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask आपके सीक्रेट रिकवरी फ्रेज को फिर से प्राप्त नहीं कर सकता है।" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask कभी भी आपके सीक्रेट रिकवरी फ्रेज के बारे में नहीं पूछेगा।" - }, - "walletCreationSuccessReminder3": { - "message": "$1 किसी के साथ या आपके फंड के चोरी होने का खतरा", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "अपने सीक्रेट रिकवरी फ्रेज को कभी शेयर ना करें", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "वॉलेट का निर्माण सफल हुआ" - }, "wantToAddThisNetwork": { "message": "इस नेटवर्क को जोड़ना चाहते हैं?" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 8e9091f911db..a4e2e37bde22 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -87,10 +87,6 @@ "failed": { "message": "विफल" }, - "fiat": { - "message": "FIAT एक्सचेंज टाइप", - "description": "Exchange type" - }, "fileImportFail": { "message": "फ़ाइल आयात काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 4463408d9435..7f9334f49f5c 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Odaberite da se prvo prikazuju valute u osnovnoj valuti bloka (npr. ETH). Odaberite mogućnost Fiat za prikazivanje valuta u odabranoj valuti Fiat." - }, "privacyMsg": { "message": "Pravilnik o zaštiti privatnosti" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "Decentralizirani internet čeka" }, - "updatedWithDate": { - "message": "Ažurirano $1" - }, "urlErrorMsg": { "message": "URI-jevima se zahtijeva prikladan prefiks HTTP/HTTPS." }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 700f6debed18..7309b04dbd05 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -153,10 +153,6 @@ "failed": { "message": "Tonbe" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Enpòte dosye ki pa travay? Klike la a!", "description": "Helps user import their account from a JSON file" @@ -357,12 +353,6 @@ "prev": { "message": "Avan" }, - "primaryCurrencySetting": { - "message": "Lajan ou itilize pi plis la" - }, - "primaryCurrencySettingDescription": { - "message": "Chwazi ETH pou bay priyorite montre valè nan ETH. Chwazi Fiat priyorite montre valè nan lajan ou chwazi a." - }, "privacyMsg": { "message": "Règleman sou enfòmasyon prive" }, @@ -548,9 +538,6 @@ "unlockMessage": { "message": "Entènèt desantralize a ap tann" }, - "updatedWithDate": { - "message": "Mete ajou $1" - }, "urlErrorMsg": { "message": "URIs mande pou apwopriye prefiks HTTP / HTTPS a." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 4786cfb9703d..7b2b429ae5ed 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -501,12 +501,6 @@ "prev": { "message": "Előző" }, - "primaryCurrencySetting": { - "message": "Elsődleges pénznem" - }, - "primaryCurrencySettingDescription": { - "message": "Válaszd a helyit, hogy az értékek elsősorban a helyi pénznemben jelenjenek meg (pl. ETH). Válaszd a Fiatot, hogy az értékek elsősorban a választott fiat pénznemben jelenjenek meg." - }, "privacyMsg": { "message": "Adatvédelmi szabályzat" }, @@ -755,9 +749,6 @@ "unlockMessage": { "message": "A decentralizált hálózat csak önre vár" }, - "updatedWithDate": { - "message": "$1 frissítve" - }, "urlErrorMsg": { "message": "Az URI-hez szükség van a megfelelő HTTP/HTTPS előtagra." }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 185af2ea637a..39d64cc98618 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Saat transaksi Anda dimasukkan ke dalam blok, selisih antara biaya dasar maks dan biaya dasar aktual akan dikembalikan. Jumlah total dihitung sebagai biaya dasar maks (dalam GWEI) * batas gas." }, - "advancedConfiguration": { - "message": "Konfigurasi lanjutan" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Perbarui opsi gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Beberapa peringatan!" - }, "alertDisableTooltip": { "message": "Ini dapat diubah dalam \"Pengaturan > Peringatan\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Untuk melanjutkan transaksi ini, Anda perlu meningkatkan batas gas menjadi 21000 atau lebih tinggi." }, - "alertMessageInsufficientBalance": { - "message": "Anda tidak memiliki cukup ETH di akun untuk membayar biaya transaksi." - }, "alertMessageNetworkBusy": { "message": "Harga gas tinggi dan estimasinya kurang akurat." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Akun salah" }, - "alertSettingsUnconnectedAccount": { - "message": "Memilih untuk menjelajahi situs web dengan akun yang tidak terhubung" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Peringatan ini ditampilkan dalam sembulan saat Anda menelusuri situs web3 yang terhubung, tetapi akun yang baru saja dipilih tidak terhubung." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Saat situs web mencoba menggunakan API window.web3 yang dihapus" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Peringatan ini ditampilkan dalam sembulan saat Anda menelusuri situs yang mencoba menggunakan API window.web3 yang dihapus, dan bisa mengakibatkan kerusakan." - }, "alerts": { "message": "Peringatan" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Ketentuan penggunaan Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta tidak dapat memulihkan Frasa Pemulihan Rahasia Anda." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta tidak akan pernah menanyakan Frasa Pemulihan Rahasia Anda." - }, "billionAbbreviation": { "message": "M", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Saya telah mengetahui peringatannya dan tetap ingin melanjutkan" }, - "confirmAlertModalDetails": { - "message": "Jika masuk, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda. Tinjau peringatannya sebelum melanjutkan." - }, - "confirmAlertModalTitle": { - "message": "Aset Anda mungkin berisiko" - }, "confirmConnectCustodianRedirect": { "message": "Kami akan mengarahkan Anda ke $1 setelah mengeklik lanjutkan." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask terhubung ke situs ini, tetapi belum ada akun yang terhubung" }, - "connectedWith": { - "message": "Terhubung dengan" - }, "connecting": { "message": "Menghubungkan" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Detail biaya" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Impor file tidak bekerja? Klik di sini!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask tidak terhubung ke situs ini" }, - "noConversionDateAvailable": { - "message": "Tanggal konversi mata uang tidak tersedia" - }, "noConversionRateAvailable": { "message": "Nilai konversi tidak tersedia" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Sematkan MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Peringatan deteksi pengelabuan bergantung pada komunikasi dengan $1. jsDeliver akan mendapat akses ke alamat IP Anda. Lihat $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1H", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "harga tidak tersedia" }, - "primaryCurrencySetting": { - "message": "Mata uang primer" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih asal untuk memprioritaskan nilai yang ditampilkan dalam mata uang asal chain (contoh, ETH). Pilih Fiat untuk memprioritaskan nilai yang ditampilkan dalam mata uang fiat yang Anda pilih." - }, "primaryType": { "message": "Tipe primer" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Ditolak" }, - "remember": { - "message": "Ingatlah:" - }, "remove": { "message": "Hapus" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Jaringan uji Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask menggunakan layanan pihak ketiga tepercaya ini untuk meningkatkan kegunaan dan keamanan produk." - }, "setApprovalForAll": { "message": "Atur persetujuan untuk semua" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Transaksi Anda selesai" }, - "smartTransactionTakingTooLong": { - "message": "Maaf telah menunggu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Jika transaksi tidak diselesaikan dalam $1, transaksi akan dibatalkan dan Anda tidak akan dikenakan biaya gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transaksi Pintar" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Cobalah untuk menukar lagi. Kami akan selalu hadir untuk melindungi Anda dari risiko serupa di lain waktu." }, - "stxEstimatedCompletion": { - "message": "Estimasi penyelesaian dalam < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Pertukaran gagal" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Permintaan pembaruan" }, - "updatedWithDate": { - "message": "Diperbarui $1" - }, "uploadDropFile": { "message": "Letakkan fail di sini" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "panduan koneksi dompet perangkat keras kami" }, - "walletCreationSuccessDetail": { - "message": "Anda telah berhasil melindungi dompet Anda. Jaga agar Frasa Pemulihan Rahasia tetap aman dan terlindungi. Ini merupakan tanggung jawab Anda!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask tidak dapat memulihkan Frasa Pemulihan Rahasia Anda." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask tidak akan pernah menanyakan Frasa Pemulihan Rahasia Anda." - }, - "walletCreationSuccessReminder3": { - "message": "$1 dengan siapa pun atau dana Anda berisiko dicuri", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Jangan pernah membagikan Frasa Pemulihan Rahasia Anda", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Dompet berhasil dibuat" - }, "wantToAddThisNetwork": { "message": "Ingin menambahkan jaringan ini?" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 7c413941da92..70e81c595852 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -209,18 +209,6 @@ "alertDisableTooltip": { "message": "Può essere cambiato in \"Impostazioni > Avvisi\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Navigazione su un sito con un account non connesso" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Questo avviso è mostrato nel popup quando stai visitando un sito Web3, ma l'account selezionato non è connesso al sito." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando un sito prova a usare la API window.web3 rimossa" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "L'avviso che viene mostrato nel popup quando stai visitando un sito che prova a usare la API window.web3 rimossa e che potrebbe non funzionare." - }, "alerts": { "message": "Avvisi" }, @@ -808,10 +796,6 @@ "feeAssociatedRequest": { "message": "Una tassa è associata a questa richiesta." }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importazione file non funziona? Clicca qui!", "description": "Helps user import their account from a JSON file" @@ -1165,12 +1149,6 @@ "prev": { "message": "Precedente" }, - "primaryCurrencySetting": { - "message": "Moneta Primaria" - }, - "primaryCurrencySettingDescription": { - "message": "Seleziona ETH per privilegiare la visualizzazione dei valori nella moneta nativa della blockhain. Seleziona Fiat per privilegiare la visualizzazione dei valori nella moneta selezionata." - }, "privacyMsg": { "message": "Politica sulla Privacy" }, @@ -1698,9 +1676,6 @@ "unlockMessage": { "message": "Il web decentralizzato ti attende" }, - "updatedWithDate": { - "message": "Aggiornata $1" - }, "urlErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index e699ccf2dc09..ca1e76018a81 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "トランザクションがブロックに含まれた場合、最大基本料金と実際の基本料金の差が返金されます。合計金額は、最大基本料金 (Gwei単位) * ガスリミットで計算されます。" }, - "advancedConfiguration": { - "message": "詳細設定" - }, "advancedDetailsDataDesc": { "message": "データ" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "ガスオプションを更新" }, - "alertBannerMultipleAlertsDescription": { - "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われる可能性があります。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "複数アラート!" - }, "alertDisableTooltip": { "message": "これは「設定」>「アラート」で変更できます" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "このトランザクションを続行するには、ガスリミットを21000以上に上げる必要があります。" }, - "alertMessageInsufficientBalance": { - "message": "アカウントにトランザクション手数料を支払うのに十分なETHがありません。" - }, "alertMessageNetworkBusy": { "message": "ガス価格が高く、見積もりはあまり正確ではありません。" }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "正しくないアカウント" }, - "alertSettingsUnconnectedAccount": { - "message": "選択した未接続のアカウントを使用してWebサイトをブラウズしています" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "このアラートは、選択中のアカウントが未接続のままweb3サイトを閲覧しているときにポップアップ表示されます。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "Webサイトが削除済みのwindow.web3 APIを使用しようとした場合" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "このアラートは、削除されたwindow.web3 APIを使用しようとし、その結果破損している可能性があるサイトをブラウズした際、ポップアップに表示されます。" - }, "alerts": { "message": "アラート" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "ベータ版利用規約" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMaskベータ版はシークレットリカバリーフレーズを復元できません。" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMaskベータ版がユーザーのシークレットリカバリーフレーズを求めることは絶対にありません。" - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "アラートを確認したうえで続行します" }, - "confirmAlertModalDetails": { - "message": "サインインすると、詐欺が判明しているサードパーティにすべての資産を奪われる可能性があります。続ける前にアラートを確認してください。" - }, - "confirmAlertModalTitle": { - "message": "資産が危険にさらされている可能性があります" - }, "confirmConnectCustodianRedirect": { "message": "「続行」をクリックすると、$1にリダイレクトされます。" }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMaskはこのサイトに接続されていますが、まだアカウントは接続されていません" }, - "connectedWith": { - "message": "接続先" - }, "connecting": { "message": "接続中..." }, @@ -1519,14 +1480,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を接続解除" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "手数料の詳細" }, - "fiat": { - "message": "法定通貨", - "description": "Exchange type" - }, "fileImportFail": { "message": "ファイルのインポートが機能していない場合、ここをクリックしてください!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSONファイル", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMaskはこのサイトに接続されていません" }, - "noConversionDateAvailable": { - "message": "通貨換算日がありません" - }, "noConversionRateAvailable": { "message": "利用可能な換算レートがありません" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutionalをピン留めする" }, - "onboardingUsePhishingDetectionDescription": { - "message": "フィッシング検出アラートには$1との通信が必要です。jsDeliverはユーザーのIPアドレスにアクセスします。$2をご覧ください。", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1日", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "価格が利用できません" }, - "primaryCurrencySetting": { - "message": "プライマリ通貨" - }, - "primaryCurrencySettingDescription": { - "message": "チェーンのネイティブ通貨 (ETHなど) による値の表示を優先するには、「ネイティブ」を選択します。選択した法定通貨による値の表示を優先するには、「法定通貨」を選択します。" - }, "primaryType": { "message": "基本型" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "拒否されました" }, - "remember": { - "message": "ご注意:" - }, "remove": { "message": "削除" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepoliaテストネットワーク" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMaskはこれらの信頼できるサードパーティサービスを使用して、製品の使いやすさと安全性を向上させています。" - }, "setApprovalForAll": { "message": "すべてを承認に設定" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "トランザクションが完了しました" }, - "smartTransactionTakingTooLong": { - "message": "お待たせして申し訳ございません" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1以内にトランザクションが完了しない場合はキャンセルされ、ガス代は請求されません。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "スマートトランザクション" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "もう一度スワップをお試しください。次回は同様のリスクを避けられるようサポートします。" }, - "stxEstimatedCompletion": { - "message": "$1未満で完了予定", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "スワップに失敗しました" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "更新リクエスト" }, - "updatedWithDate": { - "message": "$1が更新されました" - }, "uploadDropFile": { "message": "ここにファイルをドロップします" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "弊社のハードウェアウォレット接続ガイド" }, - "walletCreationSuccessDetail": { - "message": "ウォレットが正常に保護されました。シークレットリカバリーフレーズを安全かつ機密に保管してください。これはユーザーの責任です!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMaskはシークレットリカバリーフレーズを復元できません。" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMaskがユーザーのシークレットリカバリーフレーズを確認することは絶対にありません。" - }, - "walletCreationSuccessReminder3": { - "message": "誰に対しても$1。資金が盗まれる恐れがあります", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "シークレットリカバリーフレーズは決して教えないでください", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "ウォレットが作成されました" - }, "wantToAddThisNetwork": { "message": "このネットワークを追加しますか?" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 6471b738b5b9..120651f0b759 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "ವೇಗ" }, - "fiat": { - "message": "ಫಿಯೆಟ್", - "description": "Exchange type" - }, "fileImportFail": { "message": "ಫೈಲ್ ಆಮದು ಮಾಡುವಿಕೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತಿಲ್ಲವೇ? ಇಲ್ಲಿ ಕ್ಲಿಕ್ ಮಾಡಿ!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "ಹಿಂದಿನ" }, - "primaryCurrencySetting": { - "message": "ಪ್ರಾಥಮಿಕ ಕರೆನ್ಸಿ" - }, - "primaryCurrencySettingDescription": { - "message": "ಸರಪಳಿಯ ಸ್ಥಳೀಯ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಸ್ಥಳೀಯವನ್ನು ಆಯ್ಕೆಮಾಡಿ (ಉದಾ. ETH). ನಿಮ್ಮ ಆಯ್ಕೆಮಾಡಿದ ಫಿಯೆಟ್ ಕರೆನ್ಸಿಯಲ್ಲಿ ಮೌಲ್ಯಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಆದ್ಯತೆ ನೀಡಲು ಫಿಯೆಟ್ ಆಯ್ಕೆಮಾಡಿ." - }, "privacyMsg": { "message": "ಗೌಪ್ಯತೆ ನೀತಿ" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "ವಿಕೇಂದ್ರೀಕೃತ ವೆಬ್ ನಿರೀಕ್ಷಿಸುತ್ತಿದೆ" }, - "updatedWithDate": { - "message": "$1 ನವೀಕರಿಸಲಾಗಿದೆ" - }, "urlErrorMsg": { "message": "URI ಗಳಿಗೆ ಸೂಕ್ತವಾದ HTTP/HTTPS ಪೂರ್ವಪ್ರತ್ಯಯದ ಅಗತ್ಯವಿದೆ." }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 8419b0437090..051a19589005 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "트랜잭션이 블록에 포함되면 최대 기본 요금과 실제 기본 요금 간의 차액이 환불됩니다. 총 금액은 최대 기본 요금(GWEI 단위) 곱하기 가스 한도로 계산합니다." }, - "advancedConfiguration": { - "message": "고급 옵션" - }, "advancedDetailsDataDesc": { "message": "데이터" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "가스 옵션 업데이트" }, - "alertBannerMultipleAlertsDescription": { - "message": "이 요청을 승인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다." - }, - "alertBannerMultipleAlertsTitle": { - "message": "여러 경고!" - }, "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "이 트랜잭션을 계속 진행하려면, 가스 한도를 21000 이상으로 늘려야 합니다." }, - "alertMessageInsufficientBalance": { - "message": "계정에 트랜잭션 수수료를 지불할 수 있는 이더리움이 충분하지 않습니다." - }, "alertMessageNetworkBusy": { "message": "가스비가 높고 견적의 정확도도 떨어집니다." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "잘못된 계정" }, - "alertSettingsUnconnectedAccount": { - "message": "연결되지 않은 계정을 선택하여 웹사이트 탐색" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "이 경고는 연결된 web3 사이트를 탐색하고 있지만 현재 선택한 계정이 연결되지 않은 경우 팝업에 표시됩니다." - }, - "alertSettingsWeb3ShimUsage": { - "message": "웹사이트가 제거된 window.web3 API를 이용하는 경우" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "이 경고는 제거된 window.web3 API를 이용하려다가 작동이 정지된 사이트를 탐색할 때 팝업으로 표시됩니다." - }, "alerts": { "message": "경고" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "베타 이용약관" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask 베타는 비밀복구구문을 복구할 수 없습니다." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask 베타는 비밀복구구문을 절대 묻지 않습니다." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "경고를 인지했으며, 계속 진행합니다" }, - "confirmAlertModalDetails": { - "message": "로그인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다. 계속하기 전에 경고를 검토하세요." - }, - "confirmAlertModalTitle": { - "message": "자산이 위험할 수 있습니다" - }, "confirmConnectCustodianRedirect": { "message": "계속을 클릭하면 $1(으)로 리디렉션됩니다." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask는 이 사이트와 연결되어 있지만, 아직 연결된 계정이 없습니다" }, - "connectedWith": { - "message": "연결 대상:" - }, "connecting": { "message": "연결 중" }, @@ -1519,14 +1480,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 연결 해제" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "수수료 세부 정보" }, - "fiat": { - "message": "명목", - "description": "Exchange type" - }, "fileImportFail": { "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 파일", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask가 이 사이트와 연결되어 있지 않습니다" }, - "noConversionDateAvailable": { - "message": "사용 가능한 통화 변환 날짜 없음" - }, "noConversionRateAvailable": { "message": "사용 가능한 환율 없음" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional 고정" }, - "onboardingUsePhishingDetectionDescription": { - "message": "피싱 감지 경고는 $1과(와)의 통신에 의존합니다. jsDeliver는 회원님의 IP 주소에 액세스할 수 있습니다. $2 보기.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1일", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "가격 사용 불가" }, - "primaryCurrencySetting": { - "message": "기본 통화" - }, - "primaryCurrencySettingDescription": { - "message": "체인의 고유 통화(예: ETH)로 값을 우선 표시하려면 고유를 선택합니다. 선택한 명목 통화로 값을 우선 표시하려면 명목을 선택합니다." - }, "primaryType": { "message": "기본 유형" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "거부됨" }, - "remember": { - "message": "참고:" - }, "remove": { "message": "제거" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia 테스트 네트워크" }, - "setAdvancedPrivacySettingsDetails": { - "message": "이와 같이 MetaMask는 신용있는 타사의 서비스를 사용하여 제품 가용성과 안전성을 향상합니다." - }, "setApprovalForAll": { "message": "모두 승인 설정" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "트랜잭션 완료" }, - "smartTransactionTakingTooLong": { - "message": "기다리게 해서 죄송합니다" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1 이내에 트랜잭션이 완료되지 않으면 트랜잭션이 취소되고 가스비가 부과되지 않습니다.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "스마트 트랜잭션" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "스왑을 다시 진행하세요. 다음에도 유사한 위험이 발생한다면 보호해 드리겠습니다." }, - "stxEstimatedCompletion": { - "message": "예상 잔여 시간: <$1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "스왑 실패" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "업데이트 요청" }, - "updatedWithDate": { - "message": "$1에 업데이트됨" - }, "uploadDropFile": { "message": "여기에 파일을 드롭" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "당사의 하드웨어 지갑 연결 가이드" }, - "walletCreationSuccessDetail": { - "message": "지갑을 성공적으로 보호했습니다. 비밀복구구문을 안전하게 비밀로 유지하세요. 이는 회원님의 책임입니다!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask는 비밀복구구문을 복구할 수 없습니다." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask는 비밀복구구문을 절대 묻지 않습니다." - }, - "walletCreationSuccessReminder3": { - "message": "누군가와 $1 또는 회원님의 자금을 도난당할 위험이 있습니다.", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "비밀복구구문을 절대 공유하지 마세요.", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "지갑 생성 성공" - }, "wantToAddThisNetwork": { "message": "이 네트워크를 추가하시겠습니까?" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 0668166eb8fe..fe825ae6b798 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Greitas" }, - "fiat": { - "message": "Standartinė valiuta", - "description": "Exchange type" - }, "fileImportFail": { "message": "Failo importavimas neveikia? Spustelėkite čia!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Peržiūra" }, - "primaryCurrencySetting": { - "message": "Pagrindinė valiuta" - }, - "primaryCurrencySettingDescription": { - "message": "Rinkitės vietinę, kad vertės pirmiausia būtų rodomos vietine grandinės valiuta (pvz., ETH). Rinkitės standartinę, kad vertės pirmiausia būtų rodomos jūsų pasirinkta standartine valiuta." - }, "privacyMsg": { "message": "Privatumo politika" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Laukiančios decentralizuotos svetainės" }, - "updatedWithDate": { - "message": "Atnaujinta $1" - }, "urlErrorMsg": { "message": "URI reikia atitinkamo HTTP/HTTPS priešdėlio." }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 3939c9145c12..697af7849327 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -504,12 +504,6 @@ "prev": { "message": "Iepr." }, - "primaryCurrencySetting": { - "message": "Primārā valūta" - }, - "primaryCurrencySettingDescription": { - "message": "Atlasīt vietējo, lai piešķirtu attēlotajām vērtībām prioritātes ķēdes vietējā vērtībā (piemēram, ETH). Atlasiet Fiat, lai piešķirtu augstāku prioritāti vērtībām jūsu atlasītajā fiat valūtā." - }, "privacyMsg": { "message": "Privātuma politika" }, @@ -761,9 +755,6 @@ "unlockMessage": { "message": "Decentralizētais tīkls jau gaida" }, - "updatedWithDate": { - "message": "Atjaunināts $1" - }, "urlErrorMsg": { "message": "URI jāsākas ar atbilstošo HTTP/HTTPS priedēkli." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index cfad6a22d73d..dc42e639ff2a 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -488,12 +488,6 @@ "prev": { "message": "Sebelumnya" }, - "primaryCurrencySetting": { - "message": "Mata Wang Utama" - }, - "primaryCurrencySettingDescription": { - "message": "Pilih natif untuk mengutamakan nilai paparan dalam mata wang natif rantaian (cth. ETH). Pilih Fiat untuk mengutamakan nilai paparan dalam mata wang fiat yang anda pilih." - }, "privacyMsg": { "message": "Dasar Privasi" }, @@ -742,9 +736,6 @@ "unlockMessage": { "message": "Web ternyahpusat menanti" }, - "updatedWithDate": { - "message": "Dikemaskini $1" - }, "urlErrorMsg": { "message": "URI memerlukan awalan HTTP/HTTPS yang sesuai." }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 946976a7a93e..cbebb9a14563 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -84,10 +84,6 @@ "failed": { "message": "mislukt" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Bestandsimport werkt niet? Klik hier!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 0d1fa5173a9f..45a101fc83a5 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Tidligere" }, - "primaryCurrencySetting": { - "message": "Hovedvaluta " - }, - "primaryCurrencySettingDescription": { - "message": "Velg nasjonal for å prioritere å vise verdier i nasjonal valuta i kjeden (f.eks. ETH). Velg Fiat for å prioritere visning av verdier i den valgte fiat-valutaen." - }, "privacyMsg": { "message": "Personvernerklæring" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Det desentraliserte internett venter deg" }, - "updatedWithDate": { - "message": "Oppdatert $1" - }, "urlErrorMsg": { "message": "URI-er krever det aktuelle HTTP/HTTPS-prefikset." }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index df15e05a0bb6..e12eb4379cf1 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -81,18 +81,6 @@ "alertDisableTooltip": { "message": "Mababago ito sa \"Mga Setting > Mga Alerto\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Napili ang pag-browse ng website nang may hindi nakakonektang account" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Makikita ang alertong ito sa popup kapag nagba-browse ka sa isang nakakonektang web3 site, pero hindi nakakonekta ang kasalukuyang napiling account." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Kapag sinubukan ng isang website na gamitin ang inalis na window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Makikita ang alertong ito sa popup kapag nagba-browse ka sa isang site na sumusubok na gamitin ang inalis na window.web3 API, at posibleng sira bilang resulta." - }, "alerts": { "message": "Mga Alerto" }, @@ -513,10 +501,6 @@ "feeAssociatedRequest": { "message": "May nauugnay na bayarin para sa request na ito." }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -955,12 +939,6 @@ "prev": { "message": "Nakaraan" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat currency." - }, "privacyMsg": { "message": "Patakaran sa Pagkapribado" }, @@ -1655,9 +1633,6 @@ "message": "Hindi kinikilala ang custom na network na ito. Inirerekomenda naming $1 ka bago magpatuloy", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "urlErrorMsg": { "message": "Kinakailangan ng mga URL ang naaangkop na HTTP/HTTPS prefix." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index cb82388a8634..d22673fa9f1f 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Szybko" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Importowanie pliku nie działa? Kliknij tutaj!", "description": "Helps user import their account from a JSON file" @@ -502,12 +498,6 @@ "prev": { "message": "Poprzednie" }, - "primaryCurrencySetting": { - "message": "Waluta podstawowa" - }, - "primaryCurrencySettingDescription": { - "message": "Wybierz walutę natywną, aby preferować wyświetlanie wartości w walucie natywnej łańcucha (np. ETH). Wybierz walutę fiat, aby preferować wyświetlanie wartości w wybranej przez siebie walucie fiat." - }, "privacyMsg": { "message": "Polityka prywatności" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Zdecentralizowana sieć oczekuje" }, - "updatedWithDate": { - "message": "Zaktualizowano $1" - }, "urlErrorMsg": { "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 246d2f7dce02..8d686eaee7c2 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Quando a sua transação for incluída no bloco, qualquer diferença entre a sua taxa-base máxima e a taxa-base real será reembolsada. O cálculo do valor total é feito da seguinte forma: taxa-base máxima (em GWEI) * limite de gás." }, - "advancedConfiguration": { - "message": "Configurações avançadas" - }, "advancedDetailsDataDesc": { "message": "Dados" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Atualizar opções de gás" }, - "alertBannerMultipleAlertsDescription": { - "message": "Se você aprovar esta solicitação, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Vários alertas!" - }, "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Para continuar com essa transação, você precisará aumentar o limite de gás para 21000 ou mais." }, - "alertMessageInsufficientBalance": { - "message": "Você não tem ETH suficiente em sua conta para pagar as taxas de transação." - }, "alertMessageNetworkBusy": { "message": "Os preços do gás são altos e as estimativas são menos precisas." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Conta incorreta" }, - "alertSettingsUnconnectedAccount": { - "message": "Navegando em um site com uma conta não conectada selecionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site conectado da web3, mas a conta atualmente selecionada não estiver conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando um site tenta usar a API window.web3 removida" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site que tente usar a API window.web3 removida, e que consequentemente possa apresentar problemas." - }, "alerts": { "message": "Alertas" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Termos de uso do Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "O MetaMask Beta não pode recuperar a sua Frase de Recuperação Secreta." - }, - "betaWalletCreationSuccessReminder2": { - "message": "O MetaMask Beta nunca pedirá sua Frase de Recuperação Secreta." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Reconheço o alerta e quero prosseguir mesmo assim" }, - "confirmAlertModalDetails": { - "message": "Se você fizer login, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos. Confira os alertas antes de prosseguir." - }, - "confirmAlertModalTitle": { - "message": "Seus ativos podem estar em risco" - }, "confirmConnectCustodianRedirect": { "message": "Você será redirecionado para $1 ao clicar em continuar." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "A MetaMask está conectada a este site, mas nenhuma conta está conectada ainda" }, - "connectedWith": { - "message": "Conectado com" - }, "connecting": { "message": "Conectando" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Detalhes da taxa" }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "A MetaMask não está conectada a este site" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Fixar MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "preço não disponível" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "primaryType": { "message": "Tipo primário" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Recusada" }, - "remember": { - "message": "Lembre-se:" - }, "remove": { "message": "Remover" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Rede de teste Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "A MetaMask utiliza esses serviços terceirizados de confiança para aumentar a usabilidade e a segurança dos produtos." - }, "setApprovalForAll": { "message": "Definir aprovação para todos" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Sua transação foi concluída" }, - "smartTransactionTakingTooLong": { - "message": "Desculpe pela espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Se a sua transação não for finalizada em $1, ela será cancelada e você não pagará gás.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transações inteligentes" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Tente trocar novamente. Estaremos aqui para proteger você contra riscos semelhantes no futuro." }, - "stxEstimatedCompletion": { - "message": "Conclusão estimada em até $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Falha na troca" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Solicitação de atualização" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "uploadDropFile": { "message": "Solte seu arquivo aqui" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, - "walletCreationSuccessDetail": { - "message": "Você protegeu sua carteira com sucesso. Guarde sua Frase de Recuperação Secreta em segredo e em segurança — é sua responsabilidade!" - }, - "walletCreationSuccessReminder1": { - "message": "A MetaMask não é capaz de recuperar sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder2": { - "message": "A equipe da MetaMask jamais pedirá sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder3": { - "message": "$1 com ninguém, senão seus fundos poderão ser roubados", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca compartilhe a sua Frase de Recuperação Secreta", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Carteira criada com sucesso" - }, "wantToAddThisNetwork": { "message": "Desejar adicionar esta rede?" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 4c4b17d74f6e..2becf1c495a1 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -157,18 +157,6 @@ "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, - "alertSettingsUnconnectedAccount": { - "message": "Navegando em um site com uma conta não conectada selecionada" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site conectado da web3, mas a conta atualmente selecionada não estiver conectada." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Quando um site tenta usar a API window.web3 removida" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Esse alerta é exibido no pop-up quando você estiver navegando em um site que tente usar a API window.web3 removida, e que consequentemente possa apresentar problemas." - }, "alerts": { "message": "Alertas" }, @@ -782,10 +770,6 @@ "feeAssociatedRequest": { "message": "Há uma taxa associada a essa solicitação." }, - "fiat": { - "message": "Fiduciária", - "description": "Exchange type" - }, "fileImportFail": { "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" @@ -1049,9 +1033,6 @@ "invalidSeedPhrase": { "message": "Frase de Recuperação Secreta inválida" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Arquivo JSON", "description": "format for importing an account" @@ -1321,9 +1302,6 @@ "noAccountsFound": { "message": "Nenhuma conta encontrada para a busca efetuada" }, - "noConversionDateAvailable": { - "message": "Não há uma data de conversão de moeda disponível" - }, "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, @@ -1411,10 +1389,6 @@ "onboardingPinExtensionTitle": { "message": "Sua instalação da MetaMask está concluída!" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "onlyConnectTrust": { "message": "Conecte-se somente com sites em que você confia.", "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." @@ -1493,12 +1467,6 @@ "prev": { "message": "Anterior" }, - "primaryCurrencySetting": { - "message": "Moeda principal" - }, - "primaryCurrencySettingDescription": { - "message": "Selecione Nativa para priorizar a exibição de valores na moeda nativa da cadeia (por ex., ETH). Selecione Fiduciária para priorizar a exibição de valores na moeda fiduciária selecionada." - }, "priorityFee": { "message": "Taxa de prioridade" }, @@ -1584,9 +1552,6 @@ "rejected": { "message": "Rejeitada" }, - "remember": { - "message": "Lembre-se:" - }, "remove": { "message": "Remover" }, @@ -1749,9 +1714,6 @@ "message": "Enviando $1", "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, - "setAdvancedPrivacySettingsDetails": { - "message": "A MetaMask utiliza esses serviços terceirizados de confiança para aumentar a usabilidade e a segurança dos produtos." - }, "settings": { "message": "Configurações" }, @@ -2409,9 +2371,6 @@ "message": "O envio de tokens colecionáveis (ERC-721) não é suportado no momento", "description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending" }, - "updatedWithDate": { - "message": "Atualizado em $1" - }, "urlErrorMsg": { "message": "Os URLs precisam do prefixo HTTP/HTTPS adequado." }, @@ -2474,26 +2433,6 @@ "walletConnectionGuide": { "message": "nosso guia de conexão com a carteira de hardware" }, - "walletCreationSuccessDetail": { - "message": "Você protegeu sua carteira com sucesso. Guarde sua Frase de Recuperação Secreta em segredo e em segurança — é sua responsabilidade!" - }, - "walletCreationSuccessReminder1": { - "message": "A MetaMask não é capaz de recuperar sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder2": { - "message": "A equipe da MetaMask jamais pedirá sua Frase de Recuperação Secreta." - }, - "walletCreationSuccessReminder3": { - "message": "$1 com ninguém, senão seus fundos poderão ser roubados", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Nunca compartilhe a sua Frase de Recuperação Secreta", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Carteira criada com sucesso" - }, "web3ShimUsageNotification": { "message": "Percebemos que o site atual tentou usar a API window.web3 removida. Se o site parecer estar corrompido, clique em $1 para obter mais informações.", "description": "$1 is a clickable link." diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index aad720151da6..912accba29be 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -495,12 +495,6 @@ "prev": { "message": "Ant" }, - "primaryCurrencySetting": { - "message": "Moneda principală" - }, - "primaryCurrencySettingDescription": { - "message": "Selectați nativ pentru a prioritiza valorile afișate în moneda nativă a lanțului (ex. ETH). Selectați Fiat pentru a prioritiza valorile afișate în moneda selectată fiat." - }, "privacyMsg": { "message": "Politica de Confidențialitate" }, @@ -746,9 +740,6 @@ "unlockMessage": { "message": "Web-ul descentralizat așteaptă" }, - "updatedWithDate": { - "message": "Actualizat $1" - }, "urlErrorMsg": { "message": "URL necesită prefixul potrivit HTTP/HTTPS." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 088c2c904e33..b36ac87f7dbe 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "После включения вашей транзакции в блок возмещается любая разница между вашей максимальной базовой комиссией и фактической базовой комиссией. Общая сумма рассчитывается следующим образом: максимальная базовая комиссия (в Гвей) x лимит газа." }, - "advancedConfiguration": { - "message": "Дополнительная конфигурация" - }, "advancedDetailsDataDesc": { "message": "Данные" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Обновить параметры газа" }, - "alertBannerMultipleAlertsDescription": { - "message": "Если вы одобрите этот запрос, третья сторона, которая, как известно, совершала мошеннические действия, может похитить все ваши активы." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Множественные оповещения!" - }, "alertDisableTooltip": { "message": "Это можно изменить в разделе «Настройки» > «Оповещения»" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Чтобы продолжить эту транзакцию, вам необходимо увеличить лимит газа до 21 000 или выше." }, - "alertMessageInsufficientBalance": { - "message": "На вашем счету недостаточно ETH для оплаты комиссий за транзакцию." - }, "alertMessageNetworkBusy": { "message": "Цены газа высоки, а оценки менее точны." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Неверный счет" }, - "alertSettingsUnconnectedAccount": { - "message": "Просмотр веб-сайта с выбранным неподключенным счетом" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Это предупреждение отображается во всплывающем окне, когда вы просматриваете подключенный сайт web3, но текущий выбранный счет не подключен." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Когда веб-сайт пытается использовать удаленный API window.web3" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Это предупреждение отображается во всплывающем окне, когда вы просматриваете сайт, который пытается использовать удаленный API window.web3 и из-за этого может не работать." - }, "alerts": { "message": "Оповещения" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Условия использования бета-версии" }, - "betaWalletCreationSuccessReminder1": { - "message": "Бета-версия MetaMask не сможет восстановить вашу секретную фразу для восстановления." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Бета-версия MetaMask никогда не запрашивает у вас секретную фразу для восстановления." - }, "billionAbbreviation": { "message": "Б", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Я подтвердил(-а) получение предупреждения и все еще хочу продолжить" }, - "confirmAlertModalDetails": { - "message": "Если вы войдете, третья сторона, которая, как известно, совершала мошеннические действия, может похитиь твсе ваши активы. Прежде чем продолжить, просмотрите оповещения." - }, - "confirmAlertModalTitle": { - "message": "Ваши активы могут быть в опасности" - }, "confirmConnectCustodianRedirect": { "message": "Мы перенаправим вас на $1 после нажатия кнопки «Продолжить»." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask подключен к этому сайту, но счета пока не подключены" }, - "connectedWith": { - "message": "Подключен(-а) к" - }, "connecting": { "message": "Подключение..." }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Сведения о комиссии" }, - "fiat": { - "message": "Фиатная", - "description": "Exchange type" - }, "fileImportFail": { "message": "Импорт файлов не работает? Нажмите здесь!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON-файл", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask не подключен к этому сайту" }, - "noConversionDateAvailable": { - "message": "Дата обмена валюты недоступна" - }, "noConversionRateAvailable": { "message": "Нет доступного обменного курса" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Закрепить MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Оповещения об обнаружении фишинга зависят от связи с $1. jsDeliver получит доступ к вашему IP-адресу. Посмотрите $ 2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 Д", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "цена недоступна" }, - "primaryCurrencySetting": { - "message": "Основная валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Выберите «собственная», чтобы установить приоритет отображения значений в собственной валюте блокчейна (например, ETH). Выберите «Фиатная», чтобы установить приоритет отображения значений в выбранной фиатной валюте." - }, "primaryType": { "message": "Основной тип" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Отклонено" }, - "remember": { - "message": "Помните:" - }, "remove": { "message": "Удалить" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Тестовая сеть Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask использует эти доверенные сторонние сервисы для повышения удобства использования и безопасности продукта." - }, "setApprovalForAll": { "message": "Установить одобрение для всех" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Ваша транзакция завершена" }, - "smartTransactionTakingTooLong": { - "message": "Извините за ожидание" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Если ваша транзакция не будет завершена в течение $1, она будет отменена и с вас не будет взиматься плата за газ.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Умные транзакции" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Попробуйте выполнить своп еще раз. Мы готовы защитить вас от подобных рисков в следующий раз." }, - "stxEstimatedCompletion": { - "message": "Предполагаемое завершение через < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Своп не удался" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Запрос обновления" }, - "updatedWithDate": { - "message": "Обновлено $1" - }, "uploadDropFile": { "message": "Переместите свой файл сюда" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "наше руководство по подключению аппаратного кошелька" }, - "walletCreationSuccessDetail": { - "message": "Вы успешно защитили свой кошелек. Сохраните секретную фразу для восстановления в тайне — вы отвечаете за ее сохранность!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask не сможет восстановить вашу секретную фразу для восстановления." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask никогда не запрашивает у вас секретную фразу для восстановления." - }, - "walletCreationSuccessReminder3": { - "message": "$1, чтобы предотвратить кражу ваших средств", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Никогда не сообщайте никому свою секретную фразу для восстановления", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Кошелек создан" - }, "wantToAddThisNetwork": { "message": "Хотите добавить эту сеть?" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 564e7cb12d94..829435f28ff7 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -238,10 +238,6 @@ "fast": { "message": "Rýchle" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "Import souboru nefunguje? Klikněte sem!", "description": "Helps user import their account from a JSON file" @@ -480,12 +476,6 @@ "prev": { "message": "Predchádzajúce" }, - "primaryCurrencySetting": { - "message": "Primárna mena" - }, - "primaryCurrencySettingDescription": { - "message": "Vyberte natívne, ak chcete priorizovať zobrazovanie hodnôt v natívnej mene reťazca (napr. ETH). Ak chcete priorizovať zobrazovanie hodnôt vo svojej vybranej mene fiat, zvoľte možnosť Fiat." - }, "privacyMsg": { "message": "Zásady ochrany osobních údajů" }, @@ -731,9 +721,6 @@ "unlockMessage": { "message": "Decentralizovaný web čaká" }, - "updatedWithDate": { - "message": "Aktualizované $1" - }, "urlErrorMsg": { "message": "URI vyžadují korektní HTTP/HTTPS prefix." }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 8d43d184c427..cb82e0358212 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Hiter" }, - "fiat": { - "message": "Klasične", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz z datoteko ne deluje? Kliknite tukaj!", "description": "Helps user import their account from a JSON file" @@ -496,12 +492,6 @@ "prev": { "message": "Prej" }, - "primaryCurrencySetting": { - "message": "Glavna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izberite Native za prikaz vrednosti v privzeti valuti verige (npr. ETH). Izberite Klasične za prikaz vrednosti v izbrani klasični valuti." - }, "privacyMsg": { "message": "Zasebnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizirana spletna denarnica" }, - "updatedWithDate": { - "message": "Posodobljeno $1" - }, "urlErrorMsg": { "message": "URI zahtevajo ustrezno HTTP/HTTPS predpono." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index d3f5e27c6235..e15ae23086b3 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -241,10 +241,6 @@ "fast": { "message": "Брзо" }, - "fiat": { - "message": "Dekret", - "description": "Exchange type" - }, "fileImportFail": { "message": "Uvoz datoteke ne radi? Kliknite ovde!", "description": "Helps user import their account from a JSON file" @@ -499,12 +495,6 @@ "prev": { "message": "Prethodno" }, - "primaryCurrencySetting": { - "message": "Primarna valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Izaberite primarnu da biste postavili prioritete u prikazivanju vrednosti u primarnoj valuti lanca (npr. ETH). Izaberite Fiat da biste postavili prioritete u prikazivanju vrednosti u vašoj izabranoj fiat valuti." - }, "privacyMsg": { "message": "Smernice za privatnost" }, @@ -753,9 +743,6 @@ "unlockMessage": { "message": "Decentralizovani veb čeka" }, - "updatedWithDate": { - "message": "Ažuriran $1" - }, "urlErrorMsg": { "message": "URI-ovi zahtevaju odgovarajući prefiks HTTP / HTTPS." }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index a98db5bea015..163cdebc426e 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -492,12 +492,6 @@ "prev": { "message": "Föregående" }, - "primaryCurrencySetting": { - "message": "Primär valuta" - }, - "primaryCurrencySettingDescription": { - "message": "Välj native för att prioritera visning av värden i den ursprungliga valutan i kedjan (t.ex. ETH). Välj Fiat för att prioritera visning av värden i din valda fiatvaluta." - }, "privacyMsg": { "message": "Integritetspolicy" }, @@ -740,9 +734,6 @@ "unlockMessage": { "message": "Den decentraliserade webben väntar" }, - "updatedWithDate": { - "message": "Uppdaterat $1" - }, "urlErrorMsg": { "message": "URI:er kräver lämpligt HTTP/HTTPS-prefix." }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index d8ad6258e8ba..c1535d76cdd8 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -486,12 +486,6 @@ "prev": { "message": "Hakiki" }, - "primaryCurrencySetting": { - "message": "Sarafu ya Msingi" - }, - "primaryCurrencySettingDescription": { - "message": "Chagua mzawa ili kuweka kipaumbele kuonyesha thamani kwenye sarafu mzawa ya mnyororo (k.m ETH). Chagua Fiat ili uwelke kipaumbale kuonyesha thamani kwenye sarafu yako ya fiat uliyoichagua." - }, "privacyMsg": { "message": "Sera ya Faragha" }, @@ -743,9 +737,6 @@ "unlockMessage": { "message": "Wavuti uliotenganishwa unasubiri" }, - "updatedWithDate": { - "message": "Imesasishwa $1" - }, "urlErrorMsg": { "message": "URI huhitaji kiambishi sahihi cha HTTP/HTTPS." }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 8e38061bebb2..d5d1929a2dc4 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -129,10 +129,6 @@ "fast": { "message": "வேகமான" }, - "fiat": { - "message": "FIAT", - "description": "Exchange type" - }, "fileImportFail": { "message": "கோப்பு இறக்குமதி வேலை செய்யவில்லையா? இங்கே கிளிக் செய்யவும்!", "description": "Helps user import their account from a JSON file" diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index c3b4a8a6e3fa..e6c074fe1264 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -120,10 +120,6 @@ "fast": { "message": "เร็ว" }, - "fiat": { - "message": "เงินตรา", - "description": "Exchange type" - }, "fileImportFail": { "message": "นำเข้าไฟล์ไม่สำเหร็จ กดที่นี่!", "description": "Helps user import their account from a JSON file" @@ -371,9 +367,6 @@ "unlock": { "message": "ปลดล็อก" }, - "updatedWithDate": { - "message": "อัปเดต $1 แล้ว" - }, "urlErrorMsg": { "message": "URI ต้องมีคำนำหน้าเป็น HTTP หรือ HTTPS" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 0f17e7a2148b..e076ac55176b 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Kapag nakasama ang iyong transaksyon sa block, ire-refund ang anumang difference sa pagitan ng iyong pinakamataas na batayang bayad at ang aktwal na batayang bayad. Ang kabuuang halaga ay kinakalkula bilang pinakamataas na batayang bayad (sa GWEI) * ng limitasyon ng gas." }, - "advancedConfiguration": { - "message": "Advanced na pagsasaayos" - }, "advancedDetailsDataDesc": { "message": "Data" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "I-update ang mga opsyon sa gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Kung aaprubahan mo ang kahilingang ito, maaaring kunin ng third party na kilala sa mga panloloko ang lahat asset mo." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Iba't ibang alerto!" - }, "alertDisableTooltip": { "message": "Puwede itong baguhin sa \"Mga Setting > Mga Alerto\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Para magpatuloy sa transaksyong ito, kakailanganin mong dagdagan ang gas limit sa 21000 o mas mataas." }, - "alertMessageInsufficientBalance": { - "message": "Wala kang sapat na ETH sa iyong account para bayaran ang mga bayad sa transaksyon." - }, "alertMessageNetworkBusy": { "message": "Ang mga presyo ng gas ay mataas at ang pagtantiya ay hindi gaanong tumpak." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Maling account" }, - "alertSettingsUnconnectedAccount": { - "message": "Nagba-browse sa isang website na may napiling hindi konektadong account" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Ang alertong ito ay ipinapakita sa popup kapag nagba-browse ka ng konektadong web3 site, ngunit ang kasalukuyang napiling account ay hindi nakakonekta." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Kapag sinubukan ng isang website na gamitin ang inalis na window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Ang alertong ito ay ipinapakita sa popup kapag nagba-browse ka sa isang site na sumusubok na gamitin ang inalis na window.web3 API, at maaaring masira bilang resulta." - }, "alerts": { "message": "Mga Alerto" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Mga tuntunin sa paggamit ng Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "Hindi mabawi ng MetaMask Beta ang iyong Lihim na Parirala sa Pagbawi." - }, - "betaWalletCreationSuccessReminder2": { - "message": "Hindi kailanman hihingiin sa iyo ng MetaMask Beta ang iyong Lihim na Parirala sa Pagbawi." - }, "billionAbbreviation": { "message": "B", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Kinikilala ko ang mga alerto at nais ko pa rin magpatuloy" }, - "confirmAlertModalDetails": { - "message": "Kung mag-sign in ka, maaaring kunin ng third party na kilala sa mga panloloko ang lahat ng iyong mga asset. Mangyaring suriin ang mga alerto bago ka magpatuloy." - }, - "confirmAlertModalTitle": { - "message": "Maaaring nasa panganib ang iyong mga asset" - }, "confirmConnectCustodianRedirect": { "message": "Ire-redirect ka namin sa $1 sa pagpindot ng magpatuloy." }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "Konektado ang MetaMask sa site na ito, ngunit wala pang mga account ang konektado" }, - "connectedWith": { - "message": "Nakakonekta sa" - }, "connecting": { "message": "Kumokonekta" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Mga detalye ng singil" }, - "fiat": { - "message": "Fiat", - "description": "Exchange type" - }, "fileImportFail": { "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Mga Jazzicon" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "File na JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "Ang MetaMask ay hindi nakakonekta sa site na ito" }, - "noConversionDateAvailable": { - "message": "Walang available na petsa sa pagpapapalit ng currency" - }, "noConversionRateAvailable": { "message": "Hindi available ang rate ng palitan" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "I-pin ang MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Ang mga alerto sa pagtuklas ng phishing ay umaasa sa komunikasyon sa $1. Ang jsDeliver ay magkakaroon ng access sa iyong IP address. Tingnan ang $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1D", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "hindi available ang presyo" }, - "primaryCurrencySetting": { - "message": "Pangunahing Currency" - }, - "primaryCurrencySettingDescription": { - "message": "Piliin ang native para maisapriyoridad ang pagpapakita ng mga value sa native na currency ng chain (hal. ETH). Piliin ang Fiat para maisapriyoridad ang pagpapakita ng mga value sa napili mong fiat na salapi." - }, "primaryType": { "message": "Pangunahing uri" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Tinanggihan" }, - "remember": { - "message": "Tandaan:" - }, "remove": { "message": "Alisin" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia test network" }, - "setAdvancedPrivacySettingsDetails": { - "message": "Ginagamit ng MetaMask ang mga pinagkakatiwalaang serbisyo ng third-party na ito para mapahusay ang kakayahang magamit at kaligtasan ng produkto." - }, "setApprovalForAll": { "message": "Itakda ang Pag-apruba para sa Lahat" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Nakumpleto ang transaksyon mo" }, - "smartTransactionTakingTooLong": { - "message": "Paumanhin sa paghihintay" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Kung ang transaksyon mo ay hindi natapos sa loob ng $1, ito ay kakanselahin at hindi ka sisingilin para sa gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Mga Smart Transaction" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Subukan muli ang pag-swap. Narito kami para protektahan ka sa mga katulad na panganib sa susunod." }, - "stxEstimatedCompletion": { - "message": "Tinatayang pagkumpleto sa loob ng < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Nabigo ang pag-swap" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Hiling sa pag-update" }, - "updatedWithDate": { - "message": "Na-update noong $1" - }, "uploadDropFile": { "message": "I-drop ang file mo rito" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "ang aming gabay sa pagkonekta ng wallet na hardware" }, - "walletCreationSuccessDetail": { - "message": "Matagumpay mong naprotektahan ang iyong wallet. Panatilihing ligtas at sikreto ang iyong Lihim na Parirala sa Pagbawi - pananagutan mo ito!" - }, - "walletCreationSuccessReminder1": { - "message": "Di mababawi ng MetaMask ang iyong Lihim na Parirala sa Pagbawi." - }, - "walletCreationSuccessReminder2": { - "message": "Kailanman ay hindi hihingin ng MetaMask ang iyong Lihim na Parirala sa Pagbawi." - }, - "walletCreationSuccessReminder3": { - "message": "$1 sa sinuman o panganib na manakaw ang iyong pondo", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Huwag kailanman ibahagi ang iyong Lihim na Parirala sa Pagbawi", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Matagumpay ang paggawa ng wallet" - }, "wantToAddThisNetwork": { "message": "Gusto mo bang idagdag ang network na ito?" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index b521a5e9a182..4465fd7c0d78 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "İşleminiz bloka dahil edildiğinde maks. baz ücretiniz ile gerçek paz ücret arasındaki fark iade edilecektir. Toplam miktar, maks. baz ücret (GWEI'de) * gaz limiti olarak hesaplanacaktır." }, - "advancedConfiguration": { - "message": "Gelişmiş yapılandırma" - }, "advancedDetailsDataDesc": { "message": "Veri" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gaz seçeneklerini güncelle" }, - "alertBannerMultipleAlertsDescription": { - "message": "Bu talebi onaylarsanız dolandırıcılıkla bilinen üçüncü bir taraf tüm varlıklarınızı çalabilir." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Çoklu uyarı!" - }, "alertDisableTooltip": { "message": "\"Ayarlar > Uyarılar\" kısmında değiştirilebilir" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Bu işlemle devam etmek için gaz limitini 21000 veya üzeri olacak şekilde artırmanız gerekecek." }, - "alertMessageInsufficientBalance": { - "message": "Hesabınızda işlem ücretlerini ödemek için yeterli ETH yok." - }, "alertMessageNetworkBusy": { "message": "Gaz fiyatları yüksektir ve tahmin daha az kesindir." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Yanlış hesap" }, - "alertSettingsUnconnectedAccount": { - "message": "Bağlı olmayan bir hesap ile bir web sitesine göz atma seçildi" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Bu uyarı, bağlı bir web3 sitesinde gezdiğinizde gösterilir ancak şu anda seçili hesap bağlı değildir." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Bir web sitesi kaldırılmış window.web3 API'sini kullanmaya çalıştığında" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Bu uyarı, kaldırılmış window.web3 API kullanmaya çalışan bir ve bunun sonucu olarak bozulmuş olabilen bir sitede gezindiğinizde açılır pencerede gösterilir." - }, "alerts": { "message": "Uyarılar" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Beta Kullanım koşulları" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta Gizli Kurtarma İfadenizi kurtaramaz." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask hiçbir zaman Gizli Kurtarma İfadenizi istemez." - }, "billionAbbreviation": { "message": "MR", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Uyarıyı kabul ediyor ve yine de ilerlemek istiyorum" }, - "confirmAlertModalDetails": { - "message": "Oturum açarsanız dolandırıcıklarla bilinen üçüncü bir taraf tüm varlıklarınızı ele geçirebilir. İlerlemeden önce lütfen uyarıları inceleyin." - }, - "confirmAlertModalTitle": { - "message": "Varlıklarınız risk altında olabilir" - }, "confirmConnectCustodianRedirect": { "message": "Devam et düğmesine tıkladığınızda sizi şuraya yönlendireceğiz: $1." }, @@ -1100,9 +1064,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" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Ücret bilgileri" }, - "fiat": { - "message": "Fiat Para", - "description": "Exchange type" - }, "fileImportFail": { "message": "Dosya içe aktarma çalışmıyor mu? Buraya tıklayın!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON Dosyası", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask bu siteye bağlı değil" }, - "noConversionDateAvailable": { - "message": "Para birimi dönüşüm tarihi mevcut değil" - }, "noConversionRateAvailable": { "message": "Dönüşüm oranı mevcut değil" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "MetaMask Institutional'ı sabitle" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Kimlik avı tespiti uyarıları $1 ile iletişime bağlıdır. jsDeliver IP adresinize erişim sağlayacaktır. Şunu görüntüleyin: $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1G", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "fiyat mevcut değil" }, - "primaryCurrencySetting": { - "message": "Öncelikli para birimi" - }, - "primaryCurrencySettingDescription": { - "message": "Değerlerin zincirin yerli para biriminde (ör. ETH) görüntülenmesini önceliklendirmek için yerli seçimi yapın. Seçtiğiniz fiat parada değerlerin gösterilmesini önceliklendirmek için Fiat Para seçin." - }, "primaryType": { "message": "Öncelikli tür" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Reddedildi" }, - "remember": { - "message": "Unutmayın:" - }, "remove": { "message": "Kaldır" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia test ağı" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask, ürünün kullanılabilirliğini ve güvenliğini iyileştirmek amacıyla bu güvenilir üçüncü taraf hizmetlerini kullanır." - }, "setApprovalForAll": { "message": "Tümüne onay ver" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "İşleminiz tamamlandı" }, - "smartTransactionTakingTooLong": { - "message": "Beklettiğimiz için özür dileriz" - }, - "smartTransactionTakingTooLongDescription": { - "message": "İşleminiz $1 dahilinde sonuçlanmazsa iptal edilir ve sizden gaz ücreti alınmaz.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Akıllı İşlemler" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Swap işlemini tekrar deneyin. Bir dahaki sefere sizi benzer risklere karşı korumak için burada olacağız." }, - "stxEstimatedCompletion": { - "message": "Tamamlanmasına kalan tahmini süre $1 altında", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap başarısız oldu" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Talebi güncelle" }, - "updatedWithDate": { - "message": "$1 güncellendi" - }, "uploadDropFile": { "message": "Dosyanızı buraya sürükleyin" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "donanım cüzdanı bağlantı kılavuzumuz" }, - "walletCreationSuccessDetail": { - "message": "Cüzdanınızı başarılı bir şekilde korudunuz. Gizli Kurtarma İfadenizi güvenli ve gizli tutun -- bunun sorumluluğu size aittir!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask Gizli Kurtarma İfadenizi kurtaramıyor." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask hiçbir zaman Gizli Kurtarma İfadenizi istemeyecektir." - }, - "walletCreationSuccessReminder3": { - "message": "$1 hiç kimseyle başkasıyla paylaşmayın, aksi halde çalınma riskiyle karşı karşıya kalırsınız", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Gizli Kurtarma İfadenizi", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Cüzdan oluşturma başarılı" - }, "wantToAddThisNetwork": { "message": "Bu ağı eklemek istiyor musunuz?" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 37bab506a87d..b0c011690910 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -244,10 +244,6 @@ "fast": { "message": "Швидка" }, - "fiat": { - "message": "Вказівка", - "description": "Exchange type" - }, "fileImportFail": { "message": "Не працює імпорт файлу? Натисніть тут!", "description": "Helps user import their account from a JSON file" @@ -508,12 +504,6 @@ "prev": { "message": "Попередній" }, - "primaryCurrencySetting": { - "message": "Первісна валюта" - }, - "primaryCurrencySettingDescription": { - "message": "Оберіть \"рідна\", щоб пріоритезувати показ сум у рідних валютах мережі (напр.ETH). \nОберіть \"фіатна\", щоб пріоритезувати показ сум у ваших обраних фіатних валютах." - }, "privacyMsg": { "message": "Політика конфіденційності" }, @@ -765,9 +755,6 @@ "unlockMessage": { "message": "Децентралізована мережа очікує" }, - "updatedWithDate": { - "message": "Оновлено $1" - }, "urlErrorMsg": { "message": "URIs вимагають відповідного префікса HTTP/HTTPS." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index a8946a010469..2d8d89a25ee7 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "Khi các giao dịch của bạn được đưa vào khối, mọi phần chênh lệch giữa phí cơ sở tối đa và phí cơ sở thực tế đều sẽ được hoàn lại. Tổng số tiền sẽ được tính bằng phí cơ sở tối đa (theo GWEI) * hạn mức phí gas." }, - "advancedConfiguration": { - "message": "Cấu hình nâng cao" - }, "advancedDetailsDataDesc": { "message": "Dữ liệu" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Cập nhật tùy chọn phí gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo có thể lấy hết tài sản của bạn." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Có nhiều cảnh báo!" - }, "alertDisableTooltip": { "message": "Bạn có thể thay đổi trong phần \"Cài đặt > Cảnh báo\"" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "Để tiếp tục giao dịch này, bạn cần tăng giới hạn phí gas lên 21000 hoặc cao hơn." }, - "alertMessageInsufficientBalance": { - "message": "Bạn không có đủ ETH trong tài khoản để thanh toán phí giao dịch." - }, "alertMessageNetworkBusy": { "message": "Phí gas cao và ước tính kém chính xác hơn." }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "Tài khoản không đúng" }, - "alertSettingsUnconnectedAccount": { - "message": "Đang duyệt trang web khi chọn một tài khoản không được kết nối" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "Cảnh báo này hiển thị trong cửa sổ bật lên khi bạn đang duyệt một trang web đã được kết nối trên web3, nhưng tài khoản đang chọn không được kết nối." - }, - "alertSettingsWeb3ShimUsage": { - "message": "Khi một trang web cố dùng API window.web3 đã bị xóa" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "Cảnh báo này hiển thị trong cửa sổ bật lên khi bạn đang duyệt một trang web cố sử dụng API window.web3 đã bị xóa nên có thể bị lỗi." - }, "alerts": { "message": "Cảnh báo" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "Điều khoản sử dụng phiên bản Beta" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask Beta không thể khôi phục Cụm từ khôi phục bí mật của bạn." - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask Beta sẽ không bao giờ hỏi về Cụm từ khôi phục bí mật của bạn." - }, "billionAbbreviation": { "message": "Tỷ", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "Tôi đã hiểu rõ cảnh báo và vẫn muốn tiếp tục" }, - "confirmAlertModalDetails": { - "message": "Nếu bạn đăng nhập, một bên thứ ba nổi tiếng là lừa đảo có thể lấy tất cả tài sản của bạn. Vui lòng xem lại các cảnh báo trước khi tiếp tục." - }, - "confirmAlertModalTitle": { - "message": "Tài sản của bạn có thể gặp rủi ro" - }, "confirmConnectCustodianRedirect": { "message": "Chúng tôi sẽ chuyển hướng bạn đến $1 sau khi nhấn vào tiếp tục." }, @@ -1100,9 +1064,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" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "Chi tiết phí" }, - "fiat": { - "message": "Pháp định", - "description": "Exchange type" - }, "fileImportFail": { "message": "Tính năng nhập tập tin không hoạt động? Nhấp vào đây!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "Tập tin JSON", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask không được kết nối với trang web này" }, - "noConversionDateAvailable": { - "message": "Hiện không có ngày quy đổi tiền tệ nào" - }, "noConversionRateAvailable": { "message": "Không có sẵn tỷ lệ quy đổi nào" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "Ghim MetaMask Institutional" }, - "onboardingUsePhishingDetectionDescription": { - "message": "Thông báo phát hiện dấu hiệu lừa đảo tùy thuộc vào quá trình truyền tin với $1. jsDeliver sẽ có quyền truy cập vào địa chỉ IP của bạn. Xem $2.", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 Ngày", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "giá không khả dụng" }, - "primaryCurrencySetting": { - "message": "Tiền tệ chính" - }, - "primaryCurrencySettingDescription": { - "message": "Chọn Gốc để ưu tiên hiển thị giá trị bằng đơn vị tiền tệ gốc của chuỗi (ví dụ: ETH). Chọn Pháp định để ưu tiên hiển thị giá trị bằng đơn vị tiền pháp định mà bạn chọn." - }, "primaryType": { "message": "Loại chính" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "Đã từ chối" }, - "remember": { - "message": "Ghi nhớ:" - }, "remove": { "message": "Xóa" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Mạng thử nghiệm Sepolia" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask sử dụng các dịch vụ của bên thứ ba đáng tin cậy này để nâng cao sự hữu ích và an toàn của sản phẩm." - }, "setApprovalForAll": { "message": "Thiết lập chấp thuận tất cả" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "Giao dịch của bạn đã hoàn tất" }, - "smartTransactionTakingTooLong": { - "message": "Xin lỗi đã để bạn đợi lâu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Nếu giao dịch của bạn không được hoàn thành trong vòng $1, thì giao dịch sẽ bị hủy và bạn sẽ không bị tính phí gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Giao dịch thông minh" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "Hãy thử hoán đổi lại. Chúng tôi ở đây để bảo vệ bạn trước những rủi ro tương tự trong lần tới." }, - "stxEstimatedCompletion": { - "message": "Dự kiến hoàn thành sau < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Hoán đổi không thành công" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "Yêu cầu cập nhật" }, - "updatedWithDate": { - "message": "Đã cập nhật $1" - }, "uploadDropFile": { "message": "Thả tập tin của bạn vào đây" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "hướng dẫn của chúng tôi về cách kết nối ví cứng" }, - "walletCreationSuccessDetail": { - "message": "Bạn đã bảo vệ thành công ví của mình. Hãy đảm bảo an toàn và bí mật cho Cụm từ khôi phục bí mật của bạn -- đây là trách nhiệm của bạn!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask không thể khôi phục Cụm từ khôi phục bí mật của bạn." - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask sẽ không bao giờ hỏi về Cụm từ khôi phục bí mật của bạn." - }, - "walletCreationSuccessReminder3": { - "message": "$1 với bất kỳ ai, nếu không bạn sẽ có nguy cơ bị mất tiền", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "Không bao giờ chia sẻ Cụm từ khôi phục bí mật của bạn", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "Tạo ví thành công" - }, "wantToAddThisNetwork": { "message": "Bạn muốn thêm mạng này?" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index cef81c20f839..37836c219ccd 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -331,9 +331,6 @@ "advancedBaseGasFeeToolTip": { "message": "当您的交易被包含在区块中时,您的最大基础费用与实际基础费用之间的任何差额将被退还。总金额按最大基础费用(以GWEI为单位)*燃料限制计算。" }, - "advancedConfiguration": { - "message": "高级配置" - }, "advancedDetailsDataDesc": { "message": "数据" }, @@ -381,12 +378,6 @@ "alertActionUpdateGasFeeLevel": { "message": "更新燃料选项" }, - "alertBannerMultipleAlertsDescription": { - "message": "如果您批准此请求,以欺诈闻名的第三方可能会拿走您的所有资产。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "多个提醒!" - }, "alertDisableTooltip": { "message": "这可以在“设置 > 提醒”中进行更改" }, @@ -399,9 +390,6 @@ "alertMessageGasTooLow": { "message": "要继续此交易,您需要将燃料限制提高到 21000 或更高。" }, - "alertMessageInsufficientBalance": { - "message": "您的账户中没有足够的 ETH 来支付交易费用。" - }, "alertMessageNetworkBusy": { "message": "燃料价格很高,估算不太准确。" }, @@ -456,18 +444,6 @@ "alertReasonWrongAccount": { "message": "错误账户" }, - "alertSettingsUnconnectedAccount": { - "message": "浏览网站时选择的账户未连接" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "当您在浏览已连接的Web3网站,但当前所选择的账户没有连接时,此提醒会在弹出的窗口中显示。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "当网站尝试使用已经删除的 window.web3 API 时" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "当您浏览尝试使用已删除的 window.web3 API 并因此可能出现故障的网站时,此警报会显示在弹出窗口中。" - }, "alerts": { "message": "提醒" }, @@ -721,12 +697,6 @@ "betaTerms": { "message": "测试版使用条款" }, - "betaWalletCreationSuccessReminder1": { - "message": "MetaMask 测试版无法恢复您的账户私钥助记词。" - }, - "betaWalletCreationSuccessReminder2": { - "message": "MetaMask 测试版绝对不会向您索要账户私钥助记词。" - }, "billionAbbreviation": { "message": "十亿", "description": "Shortened form of 'billion'" @@ -972,12 +942,6 @@ "confirmAlertModalAcknowledgeSingle": { "message": "我已知晓提醒并仍想继续" }, - "confirmAlertModalDetails": { - "message": "如果您登录,以欺诈闻名的第三方可能会拿走您的所有资产。在继续操作之前,请查看提醒。" - }, - "confirmAlertModalTitle": { - "message": "您的资产可能面临风险" - }, "confirmConnectCustodianRedirect": { "message": "点击“继续”后,我们会将您重定向到 $1。" }, @@ -1100,9 +1064,6 @@ "connectedSnapAndNoAccountDescription": { "message": "MetaMask 已连接到此网站,但尚未连接任何账户" }, - "connectedWith": { - "message": "已连接" - }, "connecting": { "message": "连接中……" }, @@ -1519,14 +1480,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" }, @@ -1879,10 +1832,6 @@ "feeDetails": { "message": "费用详情" }, - "fiat": { - "message": "法币", - "description": "Exchange type" - }, "fileImportFail": { "message": "文件导入失败?点击这里!", "description": "Helps user import their account from a JSON file" @@ -2442,9 +2391,6 @@ "jazzicons": { "message": "Jazzicons" }, - "jsDeliver": { - "message": "jsDeliver" - }, "jsonFile": { "message": "JSON 文件", "description": "format for importing an account" @@ -2851,10 +2797,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" @@ -3098,9 +3040,6 @@ "noConnectedAccountTitle": { "message": "MetaMask 未连接到此站点" }, - "noConversionDateAvailable": { - "message": "没有可用的货币转换日期" - }, "noConversionRateAvailable": { "message": "无可用汇率" }, @@ -3501,10 +3440,6 @@ "onboardingPinMmiExtensionLabel": { "message": "将MetaMask Institutional置顶" }, - "onboardingUsePhishingDetectionDescription": { - "message": "网络钓鱼检测提醒依赖于与 $1 的通信。jsDeliver 将有权访问您的 IP 地址。查看 $2。", - "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" - }, "oneDayAbbreviation": { "message": "1 天", "description": "Shortened form of '1 day'" @@ -3921,12 +3856,6 @@ "priceUnavailable": { "message": "价格不可用" }, - "primaryCurrencySetting": { - "message": "主要货币" - }, - "primaryCurrencySettingDescription": { - "message": "选择原生以优先显示链的原生货币(例如 ETH)的值。选择法币以优先显示以您所选法币显示的值。" - }, "primaryType": { "message": "主要类型" }, @@ -4151,9 +4080,6 @@ "rejected": { "message": "已拒绝" }, - "remember": { - "message": "记住:" - }, "remove": { "message": "删除" }, @@ -4561,9 +4487,6 @@ "sepolia": { "message": "Sepolia测试网络" }, - "setAdvancedPrivacySettingsDetails": { - "message": "MetaMask 使用这些可信的第三方服务来提高产品可用性和安全性。" - }, "setApprovalForAll": { "message": "设置批准所有" }, @@ -4753,13 +4676,6 @@ "smartTransactionSuccess": { "message": "您的交易已完成" }, - "smartTransactionTakingTooLong": { - "message": "抱歉让您久等" - }, - "smartTransactionTakingTooLongDescription": { - "message": "如果您的交易在 $1 内未完成,则会取消,您无需支付燃料费。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "智能交易" }, @@ -5176,10 +5092,6 @@ "stxCancelledSubDescription": { "message": "再次尝试进行交换。下次我们会在这里保护您免受类似风险。 " }, - "stxEstimatedCompletion": { - "message": "预计将在 $1 内完成", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "交换失败" }, @@ -6091,9 +6003,6 @@ "updateRequest": { "message": "更新请求" }, - "updatedWithDate": { - "message": "已于 $1 更新" - }, "uploadDropFile": { "message": "将您的文件放在此处" }, @@ -6229,26 +6138,6 @@ "walletConnectionGuide": { "message": "我们的硬件钱包连接指南" }, - "walletCreationSuccessDetail": { - "message": "您已经成功地保护了您的钱包。请确保您的账户私钥助记词安全和秘密——这是您的责任!" - }, - "walletCreationSuccessReminder1": { - "message": "MetaMask 无法恢复您的账户私钥助记词。" - }, - "walletCreationSuccessReminder2": { - "message": "MetaMask 绝对不会索要您的账户私钥助记词。" - }, - "walletCreationSuccessReminder3": { - "message": "对任何人 $1,否则您的资金有被盗风险", - "description": "$1 is separated as walletCreationSuccessReminder3BoldSection so that we can bold it" - }, - "walletCreationSuccessReminder3BoldSection": { - "message": "切勿分享您的账户私钥助记词", - "description": "This string is localized separately from walletCreationSuccessReminder3 so that we can bold it" - }, - "walletCreationSuccessTitle": { - "message": "钱包创建成功" - }, "wantToAddThisNetwork": { "message": "想要添加此网络吗?" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 0924d284b529..32e98ed12288 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -77,18 +77,6 @@ "alertDisableTooltip": { "message": "這可以在「設定 > 提醒」裡變更" }, - "alertSettingsUnconnectedAccount": { - "message": "選擇尚未連結的帳戶瀏覽一個網站時" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "當您瀏覽一個使用 web3 的網站,但目前選擇的帳戶沒有連結時,這個提醒會顯示在一個彈跳視窗。" - }, - "alertSettingsWeb3ShimUsage": { - "message": "當一個網站試著使用已經移除的 window.web3 API" - }, - "alertSettingsWeb3ShimUsageDescription": { - "message": "當您瀏覽一個嘗試使用已經移除的 window.web3 API 的網站,可能會因此故障時,這個提醒會顯示在一個彈跳視窗。" - }, "alerts": { "message": "提醒" }, @@ -512,10 +500,6 @@ "feeAssociatedRequest": { "message": "這個請求會附帶一筆手續費。" }, - "fiat": { - "message": "法定貨幣", - "description": "Exchange type" - }, "fileImportFail": { "message": "檔案匯入失敗?點擊這裡!", "description": "Helps user import their account from a JSON file" @@ -944,12 +928,6 @@ "prev": { "message": "前一頁" }, - "primaryCurrencySetting": { - "message": "主要貨幣" - }, - "primaryCurrencySettingDescription": { - "message": "選擇原生來優先使用鏈上原生貨幣 (例如 ETH) 顯示金額。選擇法定貨幣來優先使用您選擇的法定貨幣顯示金額。" - }, "privacyMsg": { "message": "隱私政策" }, @@ -1380,9 +1358,6 @@ "message": "無法辨識這個自訂網路。我們建議您先$1再繼續。", "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." }, - "updatedWithDate": { - "message": "更新時間 $1" - }, "urlErrorMsg": { "message": "URL 需要以適當的 HTTP/HTTPS 作為開頭" }, diff --git a/app/images/animations/smart-transaction-status/confirmed.lottie.json b/app/images/animations/smart-transaction-status/confirmed.lottie.json new file mode 100644 index 000000000000..d5552d380b45 --- /dev/null +++ b/app/images/animations/smart-transaction-status/confirmed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Confirmation 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Confirmation","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[11.185,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-58.942,-0.221],[-19.5,39.221],[58.942,-39.221]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.725],"y":[0]},"t":81,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.566],"y":[0]},"t":89,"s":[26.7]},{"t":102,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":81,"op":175,"st":81,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":49,"s":[30,30,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.694,0.694,0.167],"y":[0,0,0]},"t":89,"s":[100,100,100]},{"t":135,"s":[89,89,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":49,"op":175,"st":49,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Xplosion 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[428,428,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Xplosion 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[364,376,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Xplosion 12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[190,22,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Xplosion 11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[351,462,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Xplosion 10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[54,460,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Xplosion 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[427,66,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Xplosion 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[28,247,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[71,71,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Xplosion 9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[243,409,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Xplosion 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[250,92,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Xplosion 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[477,245,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Xplosion 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[72,373,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Xplosion","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[84,90,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":37,"s":[30,30,100]},{"t":77,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.722089460784,0.722089460784,0.722089460784,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":37,"op":89,"st":37,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/failed.lottie.json b/app/images/animations/smart-transaction-status/failed.lottie.json new file mode 100644 index 000000000000..f2405f22c72d --- /dev/null +++ b/app/images/animations/smart-transaction-status/failed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Fail","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.086,0.086,0.667],"y":[0.96,0.96,1]},"o":{"x":[0.015,0.015,0.333],"y":[0.599,0.599,0]},"t":41,"s":[40,40,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.539,0.539,0.167],"y":[-0.194,-0.194,0]},"t":71,"s":[80,80,100]},{"t":103,"s":[75,75,100]}],"ix":6}},"ao":0,"ip":41,"op":179,"st":4,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"exlamation point","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,59.504,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"exclamation line","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.571],[0,26.571]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"triangle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.306,-7.458],[0,0],[-8.611,0],[0,0],[4.306,7.458],[0,0]],"o":[[0,0],[-4.306,7.458],[0,0],[8.611,0],[0,0],[-4.306,-7.458]],"v":[[-9.688,-95.736],[-113.775,84.549],[-104.088,101.329],[104.088,101.329],[113.775,84.549],[9.688,-95.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.878431372549,0.392156862745,0.439215686275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.878431432387,0.392156892664,0.439215716194,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/processing.lottie.json b/app/images/animations/smart-transaction-status/processing.lottie.json new file mode 100644 index 000000000000..96a4356b24ce --- /dev/null +++ b/app/images/animations/smart-transaction-status/processing.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":117,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Processing","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":9,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":23,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":41,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":70,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":92,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"t":117,"s":[371,284,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":6,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":0,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":4,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":19,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":38,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":60,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":91,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"t":119,"s":[251,234,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":3,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":0,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":17,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":35,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":60,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":90,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"t":117,"s":[130,250,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-intro.lottie.json b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json new file mode 100644 index 000000000000..7ab9d476cdaf --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":112,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingIntro","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":112,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":39,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":49,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[0]},{"t":94,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":39,"op":112,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-117,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-100,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-82,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-57,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-27,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":0,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":12,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-117,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-117,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-113,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-98,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-79,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-57,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-26,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":2,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":12,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":17,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":17,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":39,"st":-114,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-117,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-108,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-94,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-76,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-47,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-25,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":12,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-111,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":85,"s":[40.089]},{"t":92,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":86.5,"s":[0]},{"t":93,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":80,"s":[0]},{"t":86.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":20,"op":112,"st":20,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":104,"s":[121.104]},{"t":111,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":105.5,"s":[0]},{"t":112,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":99,"s":[0]},{"t":105.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":39,"op":112,"st":39,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":101,"s":[166.352]},{"t":108,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":102.5,"s":[0]},{"t":109,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":96,"s":[0]},{"t":102.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":36,"op":112,"st":36,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-loop.lottie.json b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json new file mode 100644 index 000000000000..caf5052ee85f --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":32,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingLoop","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":32,"st":-80,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-41,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-31,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-15,"s":[0]},{"t":14,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":-70,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-197,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-180,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-162,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-137,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-107,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":-80,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":-68,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-197,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-197,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-193,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-178,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-159,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-137,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-106,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":-78,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":-68,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":-63,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-41,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":-72,"s":[100,100,100]},{"t":-63,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-41.6060606060606,"op":-41,"st":-194,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-197,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-188,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-174,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-156,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-127,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-105,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":-80,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":-68,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-191,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":5,"s":[40.089]},{"t":12,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":6.5,"s":[0]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"t":6.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":24,"s":[121.104]},{"t":31,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":25.5,"s":[0]},{"t":32,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":19,"s":[0]},{"t":25.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-41,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":21,"s":[166.352]},{"t":28,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":22.5,"s":[0]},{"t":29,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":16,"s":[0]},{"t":22.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-44,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 33bf9bac0f22..76fb2386f1f6 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -102,6 +102,10 @@ export const SENTRY_BACKGROUND_STATE = { destNetworkAllowlist: [], srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CronjobController: { @@ -226,7 +230,7 @@ export const SENTRY_BACKGROUND_STATE = { showFiatInTestnets: true, showTestNetworks: true, smartTransactionsOptInStatus: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showConfirmationAdvancedDetails: true, }, diff --git a/app/scripts/controllers/account-tracker-controller.test.ts b/app/scripts/controllers/account-tracker-controller.test.ts new file mode 100644 index 000000000000..dbabb927fa71 --- /dev/null +++ b/app/scripts/controllers/account-tracker-controller.test.ts @@ -0,0 +1,813 @@ +import EventEmitter from 'events'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { InternalAccount } from '@metamask/keyring-api'; +import { BlockTracker, Provider } from '@metamask/network-controller'; + +import { flushPromises } from '../../../test/lib/timer-helpers'; +import { createTestProviderTools } from '../../../test/stub/provider'; +import PreferencesController from './preferences-controller'; +import type { + AccountTrackerControllerOptions, + AllowedActions, + AllowedEvents, +} from './account-tracker-controller'; +import AccountTrackerController, { + getDefaultAccountTrackerControllerState, +} from './account-tracker-controller'; + +const noop = () => true; +const currentNetworkId = '5'; +const currentChainId = '0x5'; +const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; +const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; + +const SELECTED_ADDRESS = '0x123'; + +const INITIAL_BALANCE_1 = '0x1'; +const INITIAL_BALANCE_2 = '0x2'; +const UPDATE_BALANCE = '0xabc'; +const UPDATE_BALANCE_HOOK = '0xabcd'; + +const GAS_LIMIT = '0x111111'; +const GAS_LIMIT_HOOK = '0x222222'; + +// The below three values were generated by running MetaMask in the browser +// The response to eth_call, which is called via `ethContract.balances` +// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly +// formatted or else ethers will throw an error. +const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; +const EXPECTED_CONTRACT_BALANCE_1 = '0x038d7ea4c68006'; +const EXPECTED_CONTRACT_BALANCE_2 = '0x0186a0'; + +const mockAccounts = { + [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: INITIAL_BALANCE_1 }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: INITIAL_BALANCE_2, + }, +}; + +class MockBlockTracker extends EventEmitter { + getCurrentBlock = noop; + + getLatestBlock = noop; +} + +function buildMockBlockTracker({ shouldStubListeners = true } = {}) { + const blockTrackerStub = new MockBlockTracker(); + if (shouldStubListeners) { + jest.spyOn(blockTrackerStub, 'addListener').mockImplementation(); + jest.spyOn(blockTrackerStub, 'removeListener').mockImplementation(); + } + return blockTrackerStub; +} + +type WithControllerOptions = { + completedOnboarding?: boolean; + useMultiAccountBalanceChecker?: boolean; + getNetworkClientById?: jest.Mock; + getSelectedAccount?: jest.Mock; +} & Partial; + +type WithControllerCallback = ({ + controller, + blockTrackerFromHookStub, + blockTrackerStub, + triggerAccountRemoved, +}: { + controller: AccountTrackerController; + blockTrackerFromHookStub: MockBlockTracker; + blockTrackerStub: MockBlockTracker; + triggerAccountRemoved: (address: string) => void; +}) => ReturnValue; + +type WithControllerArgs = + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback]; + +function withController( + ...args: WithControllerArgs +): ReturnValue { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { + completedOnboarding = false, + useMultiAccountBalanceChecker = false, + getNetworkClientById, + getSelectedAccount, + ...accountTrackerOptions + } = rest; + const { provider } = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT }, + }, + networkId: currentNetworkId, + chainId: currentNetworkId, + }); + const blockTrackerStub = buildMockBlockTracker(); + + const controllerMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + const getSelectedAccountStub = () => + ({ + id: 'accountId', + address: SELECTED_ADDRESS, + } as InternalAccount); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + getSelectedAccount || getSelectedAccountStub, + ); + + const { provider: providerFromHook } = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE_HOOK, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, + }, + networkId: '0x1', + chainId: '0x1', + }); + + const getNetworkStateStub = jest.fn().mockReturnValue({ + selectedNetworkClientId: 'selectedNetworkClientId', + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + getNetworkStateStub, + ); + + const blockTrackerFromHookStub = buildMockBlockTracker(); + const getNetworkClientByIdStub = jest.fn().mockReturnValue({ + configuration: { + chainId: currentChainId, + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }); + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + getNetworkClientById || getNetworkClientByIdStub, + ); + + const getOnboardingControllerState = jest.fn().mockReturnValue({ + completedOnboarding, + }); + controllerMessenger.registerActionHandler( + 'OnboardingController:getState', + getOnboardingControllerState, + ); + + const controller = new AccountTrackerController({ + state: getDefaultAccountTrackerControllerState(), + provider: provider as Provider, + blockTracker: blockTrackerStub as unknown as BlockTracker, + getNetworkIdentifier: jest.fn(), + preferencesController: { + store: { + getState: () => ({ + useMultiAccountBalanceChecker, + }), + }, + } as PreferencesController, + messenger: controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), + ...accountTrackerOptions, + }); + + return fn({ + controller, + blockTrackerFromHookStub, + blockTrackerStub, + triggerAccountRemoved: (address: string) => { + controllerMessenger.publish('KeyringController:accountRemoved', address); + }, + }); +} + +describe('AccountTrackerController', () => { + describe('start', () => { + it('restarts the subscription to the block tracker and update accounts', async () => { + withController(({ controller, blockTrackerStub }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.start(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(1); // called first time with no args + + controller.start(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 2, + 'latest', + expect.any(Function), + ); + expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( + 2, + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(2); // called second time with no args + + controller.stop(); + }); + }); + }); + + describe('stop', () => { + it('ends the subscription to the block tracker', async () => { + withController(({ controller, blockTrackerStub }) => { + controller.stop(); + + expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( + 1, + 'latest', + expect.any(Function), + ); + }); + }); + }); + + describe('startPollingByNetworkClientId', () => { + it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledTimes(1); + expect(updateAccountsSpy).toHaveBeenCalledTimes(1); + + controller.stopAllPolling(); + }); + }); + + it('should subscribe to the block tracker and update accounts for each networkClientId', async () => { + const blockTrackerFromHookStub1 = buildMockBlockTracker(); + const blockTrackerFromHookStub2 = buildMockBlockTracker(); + const blockTrackerFromHookStub3 = buildMockBlockTracker(); + withController( + { + getNetworkClientById: jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub1, + }; + case 'goerli': + return { + configuration: { + chainId: '0x5', + }, + blockTracker: blockTrackerFromHookStub2, + }; + case 'networkClientId1': + return { + configuration: { + chainId: '0xa', + }, + blockTracker: blockTrackerFromHookStub3, + }; + default: + throw new Error('unexpected networkClientId'); + } + }), + }, + ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(blockTrackerFromHookStub1.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + controller.startPollingByNetworkClientId('goerli'); + + expect(blockTrackerFromHookStub2.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('goerli'); + + controller.startPollingByNetworkClientId('networkClientId1'); + + expect(blockTrackerFromHookStub3.addListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(updateAccountsSpy).toHaveBeenCalledWith('networkClientId1'); + + controller.stopAllPolling(); + }, + ); + }); + }); + + describe('stopPollingByPollingToken', () => { + it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + const pollingToken = + controller.startPollingByNetworkClientId('mainnet'); + + controller.stopPollingByPollingToken(pollingToken); + + expect(blockTrackerFromHookStub.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + }); + }); + + it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { + withController(({ controller, blockTrackerFromHookStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('mainnet'); + + controller.stopPollingByPollingToken(pollingToken1); + + expect(blockTrackerFromHookStub.removeListener).not.toHaveBeenCalled(); + + controller.stopAllPolling(); + }); + }); + + it('should error if no pollingToken is passed', () => { + withController(({ controller }) => { + expect(() => { + controller.stopPollingByPollingToken(undefined); + }).toThrow('pollingToken required'); + }); + }); + + it('should error if no matching pollingToken is found', () => { + withController(({ controller }) => { + expect(() => { + controller.stopPollingByPollingToken('potato'); + }).toThrow('pollingToken not found'); + }); + }); + }); + + describe('stopAll', () => { + it('should end all subscriptions', async () => { + const blockTrackerFromHookStub1 = buildMockBlockTracker(); + const blockTrackerFromHookStub2 = buildMockBlockTracker(); + const getNetworkClientByIdStub = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub1, + }; + case 'goerli': + return { + configuration: { + chainId: '0x5', + }, + blockTracker: blockTrackerFromHookStub2, + }; + default: + throw new Error('unexpected networkClientId'); + } + }); + withController( + { + getNetworkClientById: getNetworkClientByIdStub, + }, + ({ controller, blockTrackerStub }) => { + jest.spyOn(controller, 'updateAccounts').mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + controller.startPollingByNetworkClientId('goerli'); + + controller.stopAllPolling(); + + expect(blockTrackerStub.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(blockTrackerFromHookStub1.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + expect(blockTrackerFromHookStub2.removeListener).toHaveBeenCalledWith( + 'latest', + expect.any(Function), + ); + }, + ); + }); + }); + + describe('blockTracker "latest" events', () => { + it('updates currentBlockGasLimit, currentBlockGasLimitByChainId, and accounts when polling is initiated via `start`', async () => { + const blockTrackerStub = buildMockBlockTracker({ + shouldStubListeners: false, + }); + withController( + { + blockTracker: blockTrackerStub as unknown as BlockTracker, + }, + async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.start(); + blockTrackerStub.emit('latest', 'blockNumber'); + + await flushPromises(); + + expect(updateAccountsSpy).toHaveBeenCalledWith(undefined); + + expect(controller.state).toStrictEqual({ + accounts: {}, + accountsByChainId: {}, + currentBlockGasLimit: GAS_LIMIT, + currentBlockGasLimitByChainId: { + [currentChainId]: GAS_LIMIT, + }, + }); + + controller.stop(); + }, + ); + }); + + it('updates only the currentBlockGasLimitByChainId and accounts when polling is initiated via `startPollingByNetworkClientId`', async () => { + const blockTrackerFromHookStub = buildMockBlockTracker({ + shouldStubListeners: false, + }); + const providerFromHook = createTestProviderTools({ + scaffold: { + eth_getBalance: UPDATE_BALANCE_HOOK, + eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, + eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, + }, + networkId: '0x1', + chainId: '0x1', + }).provider; + const getNetworkClientByIdStub = jest.fn().mockReturnValue({ + configuration: { + chainId: '0x1', + }, + blockTracker: blockTrackerFromHookStub, + provider: providerFromHook, + }); + withController( + { + getNetworkClientById: getNetworkClientByIdStub, + }, + async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + + controller.startPollingByNetworkClientId('mainnet'); + + blockTrackerFromHookStub.emit('latest', 'blockNumber'); + + await flushPromises(); + + expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); + + expect(controller.state).toStrictEqual({ + accounts: {}, + accountsByChainId: {}, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: { + '0x1': GAS_LIMIT_HOOK, + }, + }); + + controller.stopAllPolling(); + }, + ); + }); + }); + + describe('updateAccountsAllActiveNetworks', () => { + it('updates accounts for the globally selected network and all currently polling networks', async () => { + withController(async ({ controller }) => { + const updateAccountsSpy = jest + .spyOn(controller, 'updateAccounts') + .mockResolvedValue(); + await controller.startPollingByNetworkClientId('networkClientId1'); + await controller.startPollingByNetworkClientId('networkClientId2'); + await controller.startPollingByNetworkClientId('networkClientId3'); + + expect(updateAccountsSpy).toHaveBeenCalledTimes(3); + + await controller.updateAccountsAllActiveNetworks(); + + expect(updateAccountsSpy).toHaveBeenCalledTimes(7); + expect(updateAccountsSpy).toHaveBeenNthCalledWith(4); // called with no args + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 5, + 'networkClientId1', + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 6, + 'networkClientId2', + ); + expect(updateAccountsSpy).toHaveBeenNthCalledWith( + 7, + 'networkClientId3', + ); + }); + }); + }); + + describe('updateAccounts', () => { + it('does not update accounts if completedOnBoarding is false', async () => { + withController( + { + completedOnboarding: false, + }, + async ({ controller }) => { + await controller.updateAccounts(); + + expect(controller.state).toStrictEqual({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + + describe('chain does not have single call balance address', () => { + const mockAccountsWithSelectedAddress = { + ...mockAccounts, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: '0x0', + }, + }; + const mockInitialState = { + accounts: mockAccountsWithSelectedAddress, + accountsByChainId: { + '0x999': mockAccountsWithSelectedAddress, + }, + }; + + describe('when useMultiAccountBalanceChecker is true', () => { + it('updates all accounts directly', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: true, + state: mockInitialState, + }, + async ({ controller }) => { + await controller.updateAccounts(); + + const accounts = { + [VALID_ADDRESS]: { + address: VALID_ADDRESS, + balance: UPDATE_BALANCE, + }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: UPDATE_BALANCE, + }, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: UPDATE_BALANCE, + }, + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + '0x999': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + + describe('when useMultiAccountBalanceChecker is false', () => { + it('updates only the selectedAddress directly, setting other balances to null', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: false, + state: mockInitialState, + }, + async ({ controller }) => { + await controller.updateAccounts(); + + const accounts = { + [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: null }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: null, + }, + [SELECTED_ADDRESS]: { + address: SELECTED_ADDRESS, + balance: UPDATE_BALANCE, + }, + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + '0x999': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + }); + + describe('chain does have single call balance address and network is not localhost', () => { + describe('when useMultiAccountBalanceChecker is true', () => { + it('updates all accounts via balance checker', async () => { + withController( + { + completedOnboarding: true, + useMultiAccountBalanceChecker: true, + getNetworkIdentifier: jest + .fn() + .mockReturnValue('http://not-localhost:8545'), + getSelectedAccount: jest.fn().mockReturnValue({ + id: 'accountId', + address: VALID_ADDRESS, + } as InternalAccount), + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + '0x1': { ...mockAccounts }, + }, + }, + }, + async ({ controller }) => { + await controller.updateAccounts('mainnet'); + + const accounts = { + [VALID_ADDRESS]: { + address: VALID_ADDRESS, + balance: EXPECTED_CONTRACT_BALANCE_1, + }, + [VALID_ADDRESS_TWO]: { + address: VALID_ADDRESS_TWO, + balance: EXPECTED_CONTRACT_BALANCE_2, + }, + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + '0x1': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + }); + }); + + describe('onAccountRemoved', () => { + it('should remove an account from state', () => { + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, + }, + }, + }, + ({ controller, triggerAccountRemoved }) => { + triggerAccountRemoved(VALID_ADDRESS); + + const accounts = { + [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], + }; + + expect(controller.state).toStrictEqual({ + accounts, + accountsByChainId: { + [currentChainId]: accounts, + '0x1': accounts, + '0x2': accounts, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); + + describe('clearAccounts', () => { + it('should reset state', () => { + withController( + { + state: { + accounts: { ...mockAccounts }, + accountsByChainId: { + [currentChainId]: { + ...mockAccounts, + }, + '0x1': { + ...mockAccounts, + }, + '0x2': { + ...mockAccounts, + }, + }, + }, + }, + ({ controller }) => { + controller.clearAccounts(); + + expect(controller.state).toStrictEqual({ + accounts: {}, + accountsByChainId: { + [currentChainId]: {}, + }, + currentBlockGasLimit: '', + currentBlockGasLimitByChainId: {}, + }); + }, + ); + }); + }); +}); diff --git a/app/scripts/controllers/account-tracker-controller.ts b/app/scripts/controllers/account-tracker-controller.ts new file mode 100644 index 000000000000..e2c78ea3f3f9 --- /dev/null +++ b/app/scripts/controllers/account-tracker-controller.ts @@ -0,0 +1,836 @@ +/* Account Tracker + * + * This module is responsible for tracking any number of accounts + * and caching their current balances & transaction counts. + * + * It also tracks transaction hashes, and checks their inclusion status + * on each new block. + */ + +import EthQuery from '@metamask/eth-query'; +import { v4 as random } from 'uuid'; + +import log from 'loglevel'; +import pify from 'pify'; +import { Web3Provider } from '@ethersproject/providers'; +import { Contract } from '@ethersproject/contracts'; +import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; +import { cloneDeep } from 'lodash'; +import { + BlockTracker, + NetworkClientConfiguration, + NetworkClientId, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, + Provider, +} from '@metamask/network-controller'; +import { hasProperty, Hex } from '@metamask/utils'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + AccountsControllerGetSelectedAccountAction, + AccountsControllerSelectedEvmAccountChangeEvent, +} from '@metamask/accounts-controller'; +import { KeyringControllerAccountRemovedEvent } from '@metamask/keyring-controller'; +import { InternalAccount } from '@metamask/keyring-api'; + +import { LOCALHOST_RPC_URL } from '../../../shared/constants/network'; +import { SINGLE_CALL_BALANCES_ADDRESSES } from '../constants/contracts'; +import { previousValueComparator } from '../lib/util'; +import type { + OnboardingControllerGetStateAction, + OnboardingControllerStateChangeEvent, +} from './onboarding'; +import PreferencesController from './preferences-controller'; + +// Unique name for the controller +const controllerName = 'AccountTrackerController'; + +type Account = { + address: string; + balance: string | null; +}; + +/** + * The state of the {@link AccountTrackerController} + * + * @property accounts - The accounts currently stored in this AccountTrackerController + * @property accountsByChainId - The accounts currently stored in this AccountTrackerController keyed by chain id + * @property currentBlockGasLimit - A hex string indicating the gas limit of the current block + * @property currentBlockGasLimitByChainId - A hex string indicating the gas limit of the current block keyed by chain id + */ +export type AccountTrackerControllerState = { + accounts: Record>; + currentBlockGasLimit: string; + accountsByChainId: Record; + currentBlockGasLimitByChainId: Record; +}; + +/** + * {@link AccountTrackerController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + accounts: { + persist: true, + anonymous: false, + }, + currentBlockGasLimit: { + persist: true, + anonymous: true, + }, + accountsByChainId: { + persist: true, + anonymous: false, + }, + currentBlockGasLimitByChainId: { + persist: true, + anonymous: true, + }, +}; + +/** + * Function to get default state of the {@link AccountTrackerController}. + */ +export const getDefaultAccountTrackerControllerState = + (): AccountTrackerControllerState => ({ + accounts: {}, + currentBlockGasLimit: '', + accountsByChainId: {}, + currentBlockGasLimitByChainId: {}, + }); + +/** + * Returns the state of the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AccountTrackerControllerState +>; + +/** + * Actions exposed by the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerActions = + AccountTrackerControllerGetStateAction; + +/** + * Event emitted when the state of the {@link AccountTrackerController} changes. + */ +export type AccountTrackerControllerStateChangeEvent = + ControllerStateChangeEvent< + typeof controllerName, + AccountTrackerControllerState + >; + +/** + * Events emitted by {@link AccountTrackerController}. + */ +export type AccountTrackerControllerEvents = + AccountTrackerControllerStateChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | OnboardingControllerGetStateAction + | AccountsControllerGetSelectedAccountAction + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = + | AccountsControllerSelectedEvmAccountChangeEvent + | KeyringControllerAccountRemovedEvent + | OnboardingControllerStateChangeEvent; + +/** + * Messenger type for the {@link AccountTrackerController}. + */ +export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AccountTrackerControllerActions | AllowedActions, + AccountTrackerControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +export type AccountTrackerControllerOptions = { + state: Partial; + messenger: AccountTrackerControllerMessenger; + provider: Provider; + blockTracker: BlockTracker; + getNetworkIdentifier: (config?: NetworkClientConfiguration) => string; + preferencesController: PreferencesController; +}; + +/** + * This module is responsible for tracking any number of accounts and caching their current balances & transaction + * counts. + * + * It also tracks transaction hashes, and checks their inclusion status on each new block. + * + */ +export default class AccountTrackerController extends BaseController< + typeof controllerName, + AccountTrackerControllerState, + AccountTrackerControllerMessenger +> { + #pollingTokenSets = new Map>(); + + #listeners: Record Promise> = + {}; + + #provider: Provider; + + #blockTracker: BlockTracker; + + #currentBlockNumberByChainId: Record = {}; + + #getNetworkIdentifier: AccountTrackerControllerOptions['getNetworkIdentifier']; + + #preferencesController: AccountTrackerControllerOptions['preferencesController']; + + #selectedAccount: InternalAccount; + + /** + * @param options - Options for initializing the controller + * @param options.state - Initial controller state. + * @param options.messenger - Messenger used to communicate with BaseV2 controller. + * @param options.provider - An EIP-1193 provider instance that uses the current global network + * @param options.blockTracker - A block tracker, which emits events for each new block + * @param options.getNetworkIdentifier - A function that returns the current network or passed network configuration + * @param options.preferencesController - The preferences controller + */ + constructor(options: AccountTrackerControllerOptions) { + super({ + name: controllerName, + metadata: controllerMetadata, + state: { + ...getDefaultAccountTrackerControllerState(), + ...options.state, + }, + messenger: options.messenger, + }); + + this.#provider = options.provider; + this.#blockTracker = options.blockTracker; + + this.#getNetworkIdentifier = options.getNetworkIdentifier; + this.#preferencesController = options.preferencesController; + + // subscribe to account removal + this.messagingSystem.subscribe( + 'KeyringController:accountRemoved', + (address) => this.removeAccounts([address]), + ); + + const onboardingState = this.messagingSystem.call( + 'OnboardingController:getState', + ); + this.messagingSystem.subscribe( + 'OnboardingController:stateChange', + previousValueComparator((prevState, currState) => { + const { completedOnboarding: prevCompletedOnboarding } = prevState; + const { completedOnboarding: currCompletedOnboarding } = currState; + if (!prevCompletedOnboarding && currCompletedOnboarding) { + this.updateAccountsAllActiveNetworks(); + } + return true; + }, onboardingState), + ); + + this.#selectedAccount = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ); + + this.messagingSystem.subscribe( + 'AccountsController:selectedEvmAccountChange', + (newAccount) => { + const { useMultiAccountBalanceChecker } = + this.#preferencesController.store.getState(); + + if ( + this.#selectedAccount.id !== newAccount.id && + !useMultiAccountBalanceChecker + ) { + this.#selectedAccount = newAccount; + this.updateAccountsAllActiveNetworks(); + } + }, + ); + } + + resetState(): void { + const { + accounts, + accountsByChainId, + currentBlockGasLimit, + currentBlockGasLimitByChainId, + } = getDefaultAccountTrackerControllerState(); + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + state.currentBlockGasLimit = currentBlockGasLimit; + state.currentBlockGasLimitByChainId = currentBlockGasLimitByChainId; + }); + } + + /** + * Starts polling with global selected network + */ + start(): void { + // blockTracker.currentBlock may be null + this.#currentBlockNumberByChainId = { + [this.#getCurrentChainId()]: this.#blockTracker.getCurrentBlock(), + }; + this.#blockTracker.once('latest', (blockNumber) => { + this.#currentBlockNumberByChainId[this.#getCurrentChainId()] = + blockNumber; + }); + + // remove first to avoid double add + this.#blockTracker.removeListener('latest', this.#updateForBlock); + // add listener + this.#blockTracker.addListener('latest', this.#updateForBlock); + // fetch account balances + this.updateAccounts(); + } + + /** + * Stops polling with global selected network + */ + stop(): void { + // remove listener + this.#blockTracker.removeListener('latest', this.#updateForBlock); + } + + /** + * Gets the current chain ID. + */ + #getCurrentChainId(): Hex { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return chainId; + } + + /** + * Resolves a networkClientId to a network client config + * or globally selected network config if not provided + * + * @param networkClientId - Optional networkClientId to fetch a network client with + * @returns network client config + */ + #getCorrectNetworkClient(networkClientId?: NetworkClientId): { + chainId: Hex; + provider: Provider; + blockTracker: BlockTracker; + identifier: string; + } { + if (networkClientId) { + const { configuration, provider, blockTracker } = + this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + + return { + chainId: configuration.chainId, + provider, + blockTracker, + identifier: this.#getNetworkIdentifier(configuration), + }; + } + return { + chainId: this.#getCurrentChainId(), + provider: this.#provider, + blockTracker: this.#blockTracker, + identifier: this.#getNetworkIdentifier(), + }; + } + + /** + * Starts polling for a networkClientId + * + * @param networkClientId - The networkClientId to start polling for + * @returns pollingToken + */ + startPollingByNetworkClientId(networkClientId: NetworkClientId): string { + const pollToken = random(); + + const pollingTokenSet = this.#pollingTokenSets.get(networkClientId); + if (pollingTokenSet) { + pollingTokenSet.add(pollToken); + } else { + const set = new Set(); + set.add(pollToken); + this.#pollingTokenSets.set(networkClientId, set); + this.#subscribeWithNetworkClientId(networkClientId); + } + return pollToken; + } + + /** + * Stops polling for all networkClientIds + */ + stopAllPolling(): void { + this.stop(); + this.#pollingTokenSets.forEach((tokenSet, _networkClientId) => { + tokenSet.forEach((token) => { + this.stopPollingByPollingToken(token); + }); + }); + } + + /** + * Stops polling for a networkClientId + * + * @param pollingToken - The polling token to stop polling for + */ + stopPollingByPollingToken(pollingToken: string | undefined): void { + if (!pollingToken) { + throw new Error('pollingToken required'); + } + let found = false; + this.#pollingTokenSets.forEach((tokenSet, key) => { + if (tokenSet.has(pollingToken)) { + found = true; + tokenSet.delete(pollingToken); + if (tokenSet.size === 0) { + this.#pollingTokenSets.delete(key); + this.#unsubscribeWithNetworkClientId(key); + } + } + }); + if (!found) { + throw new Error('pollingToken not found'); + } + } + + /** + * Subscribes from the block tracker for the given networkClientId if not currently subscribed + * + * @param networkClientId - network client ID to fetch a block tracker with + */ + #subscribeWithNetworkClientId(networkClientId: NetworkClientId): void { + if (this.#listeners[networkClientId]) { + return; + } + const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); + const updateForBlock = this.#updateForBlockByNetworkClientId.bind( + this, + networkClientId, + ); + blockTracker.addListener('latest', updateForBlock); + + this.#listeners[networkClientId] = updateForBlock; + + this.updateAccounts(networkClientId); + } + + /** + * Unsubscribes from the block tracker for the given networkClientId if currently subscribed + * + * @param networkClientId - The network client ID to fetch a block tracker with + */ + #unsubscribeWithNetworkClientId(networkClientId: NetworkClientId): void { + if (!this.#listeners[networkClientId]) { + return; + } + const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); + blockTracker.removeListener('latest', this.#listeners[networkClientId]); + + delete this.#listeners[networkClientId]; + } + + /** + * Returns the accounts object for the chain ID, or initializes it from the globally selected + * if it doesn't already exist. + * + * @param chainId - The chain ID + */ + #getAccountsForChainId( + chainId: Hex, + ): AccountTrackerControllerState['accounts'] { + const { accounts, accountsByChainId } = this.state; + if (accountsByChainId[chainId]) { + return cloneDeep(accountsByChainId[chainId]); + } + + const newAccounts: AccountTrackerControllerState['accounts'] = {}; + Object.keys(accounts).forEach((address) => { + newAccounts[address] = {}; + }); + return newAccounts; + } + + /** + * Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this + * AccountTrackerController. + * + * Once this AccountTrackerController accounts are up to date with those referenced by the passed addresses, each + * of these accounts are given an updated balance via EthQuery. + * + * @param addresses - The array of hex addresses for accounts with which this AccountTrackerController accounts should be + * in sync + */ + syncWithAddresses(addresses: string[]): void { + const { accounts } = this.state; + const locals = Object.keys(accounts); + + const accountsToAdd: string[] = []; + addresses.forEach((upstream) => { + if (!locals.includes(upstream)) { + accountsToAdd.push(upstream); + } + }); + + const accountsToRemove: string[] = []; + locals.forEach((local) => { + if (!addresses.includes(local)) { + accountsToRemove.push(local); + } + }); + + this.addAccounts(accountsToAdd); + this.removeAccounts(accountsToRemove); + } + + /** + * Adds new addresses to track the balances of + * given a balance as long this.#currentBlockNumberByChainId is defined for the chainId. + * + * @param addresses - An array of hex addresses of new accounts to track + */ + addAccounts(addresses: string[]): void { + const { accounts: _accounts, accountsByChainId: _accountsByChainId } = + this.state; + const accounts = cloneDeep(_accounts); + const accountsByChainId = cloneDeep(_accountsByChainId); + + // add initial state for addresses + addresses.forEach((address) => { + accounts[address] = {}; + }); + Object.keys(accountsByChainId).forEach((chainId) => { + addresses.forEach((address) => { + accountsByChainId[chainId][address] = {}; + }); + }); + // save accounts state + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); + + // fetch balances for the accounts if there is block number ready + if (this.#currentBlockNumberByChainId[this.#getCurrentChainId()]) { + this.updateAccounts(); + } + this.#pollingTokenSets.forEach((_tokenSet, networkClientId) => { + const { chainId } = this.#getCorrectNetworkClient(networkClientId); + if (this.#currentBlockNumberByChainId[chainId]) { + this.updateAccounts(networkClientId); + } + }); + } + + /** + * Removes accounts from being tracked + * + * @param addresses - An array of hex addresses to stop tracking. + */ + removeAccounts(addresses: string[]): void { + const { accounts: _accounts, accountsByChainId: _accountsByChainId } = + this.state; + const accounts = cloneDeep(_accounts); + const accountsByChainId = cloneDeep(_accountsByChainId); + + // remove each state object + addresses.forEach((address) => { + delete accounts[address]; + }); + Object.keys(accountsByChainId).forEach((chainId) => { + addresses.forEach((address) => { + delete accountsByChainId[chainId][address]; + }); + }); + // save accounts state + this.update((state) => { + state.accounts = accounts; + state.accountsByChainId = accountsByChainId; + }); + } + + /** + * Removes all addresses and associated balances + */ + clearAccounts(): void { + this.update((state) => { + state.accounts = {}; + state.accountsByChainId = { + [this.#getCurrentChainId()]: {}, + }; + }); + } + + /** + * Given a block, updates this AccountTrackerController currentBlockGasLimit and currentBlockGasLimitByChainId and then updates + * each local account's balance via EthQuery + * + * @private + * @param blockNumber - the block number to update to. + * @fires 'block' The updated state, if all account updates are successful + */ + #updateForBlock = async (blockNumber: string): Promise => { + await this.#updateForBlockByNetworkClientId(undefined, blockNumber); + }; + + /** + * Given a block, updates this AccountTrackerController currentBlockGasLimitByChainId, and then updates each local account's balance + * via EthQuery + * + * @private + * @param networkClientId - optional network client ID to use instead of the globally selected network. + * @param blockNumber - the block number to update to. + * @fires 'block' The updated state, if all account updates are successful + */ + async #updateForBlockByNetworkClientId( + networkClientId: NetworkClientId | undefined, + blockNumber: string, + ): Promise { + const { chainId, provider } = + this.#getCorrectNetworkClient(networkClientId); + this.#currentBlockNumberByChainId[chainId] = blockNumber; + + // block gasLimit polling shouldn't be in account-tracker shouldn't be here... + const currentBlock = await pify(new EthQuery(provider)).getBlockByNumber( + blockNumber, + false, + ); + if (!currentBlock) { + return; + } + const currentBlockGasLimit = currentBlock.gasLimit; + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.currentBlockGasLimit = currentBlockGasLimit; + } + state.currentBlockGasLimitByChainId[chainId] = currentBlockGasLimit; + }); + + try { + await this.updateAccounts(networkClientId); + } catch (err) { + log.error(err); + } + } + + /** + * Updates accounts for the globally selected network + * and all networks that are currently being polled. + * + */ + async updateAccountsAllActiveNetworks(): Promise { + await this.updateAccounts(); + await Promise.all( + Array.from(this.#pollingTokenSets).map(([networkClientId]) => { + return this.updateAccounts(networkClientId); + }), + ); + } + + /** + * balanceChecker is deployed on main eth (test)nets and requires a single call + * for all other networks, calls this.#updateAccount for each account in this.store + * + * @param networkClientId - optional network client ID to use instead of the globally selected network. + */ + async updateAccounts(networkClientId?: NetworkClientId): Promise { + const { completedOnboarding } = this.messagingSystem.call( + 'OnboardingController:getState', + ); + if (!completedOnboarding) { + return; + } + + const { chainId, provider, identifier } = + this.#getCorrectNetworkClient(networkClientId); + const { useMultiAccountBalanceChecker } = + this.#preferencesController.store.getState(); + + let addresses = []; + if (useMultiAccountBalanceChecker) { + const { accounts } = this.state; + + addresses = Object.keys(accounts); + } else { + const selectedAddress = this.messagingSystem.call( + 'AccountsController:getSelectedAccount', + ).address; + + addresses = [selectedAddress]; + } + + const rpcUrl = 'http://127.0.0.1:8545'; + if ( + identifier === LOCALHOST_RPC_URL || + identifier === rpcUrl || + !((id): id is keyof typeof SINGLE_CALL_BALANCES_ADDRESSES => + id in SINGLE_CALL_BALANCES_ADDRESSES)(chainId) + ) { + await Promise.all( + addresses.map((address) => + this.#updateAccount(address, provider, chainId), + ), + ); + } else { + await this.#updateAccountsViaBalanceChecker( + addresses, + SINGLE_CALL_BALANCES_ADDRESSES[chainId], + provider, + chainId, + ); + } + } + + /** + * Updates the current balance of an account. + * + * @private + * @param address - A hex address of a the account to be updated + * @param provider - The provider instance to fetch the balance with + * @param chainId - The chain ID to update in state + */ + + async #updateAccount( + address: string, + provider: Provider, + chainId: Hex, + ): Promise { + const { useMultiAccountBalanceChecker } = + this.#preferencesController.store.getState(); + + let balance = '0x0'; + + // query balance + try { + balance = await pify(new EthQuery(provider)).getBalance(address); + } catch (error) { + if ( + error && + typeof error === 'object' && + hasProperty(error, 'data') && + error.data && + hasProperty(error.data, 'request') && + error.data.request && + hasProperty(error.data.request, 'method') && + error.data.request.method !== 'eth_getBalance' + ) { + throw error; + } + } + + const result = { address, balance }; + // update accounts state + const accounts = this.#getAccountsForChainId(chainId); + // only populate if the entry is still present + if (!accounts[address]) { + return; + } + + let newAccounts = accounts; + if (!useMultiAccountBalanceChecker) { + newAccounts = {}; + Object.keys(accounts).forEach((accountAddress) => { + if (address !== accountAddress) { + newAccounts[accountAddress] = { + address: accountAddress, + balance: null, + }; + } + }); + } + + newAccounts[address] = result; + + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; + }); + } + + /** + * Updates current address balances from balanceChecker deployed contract instance + * + * @private + * @param addresses - A hex addresses of a the accounts to be updated + * @param deployedContractAddress - The contract address to fetch balances with + * @param provider - The provider instance to fetch the balance with + * @param chainId - The chain ID to update in state + */ + async #updateAccountsViaBalanceChecker( + addresses: string[], + deployedContractAddress: string, + provider: Provider, + chainId: Hex, + ): Promise { + const ethContract = await new Contract( + deployedContractAddress, + SINGLE_CALL_BALANCES_ABI, + new Web3Provider(provider), + ); + const ethBalance = ['0x0000000000000000000000000000000000000000']; + + try { + const balances = await ethContract.balances(addresses, ethBalance); + + const accounts = this.#getAccountsForChainId(chainId); + const newAccounts: AccountTrackerControllerState['accounts'] = {}; + Object.keys(accounts).forEach((address) => { + if (!addresses.includes(address)) { + newAccounts[address] = { address, balance: null }; + } + }); + addresses.forEach((address, index) => { + const balance = balances[index] ? balances[index].toHexString() : '0x0'; + newAccounts[address] = { address, balance }; + }); + + this.update((state) => { + if (chainId === this.#getCurrentChainId()) { + state.accounts = newAccounts; + } + state.accountsByChainId[chainId] = newAccounts; + }); + } catch (error) { + log.warn( + `MetaMask - Account Tracker single call balance fetch failed`, + error, + ); + Promise.allSettled( + addresses.map((address) => + this.#updateAccount(address, provider, chainId), + ), + ); + } + } +} diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 32498a1c8bce..3d8f9d176fb6 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -57,6 +57,7 @@ export default class AppStateController extends EventEmitter { trezorModel: null, currentPopupId: undefined, onboardingDate: null, + lastViewedUserSurvey: null, newPrivacyPolicyToastClickedOrClosed: null, newPrivacyPolicyToastShownDate: null, // This key is only used for checking if the user had set advancedGasFee @@ -198,6 +199,12 @@ export default class AppStateController extends EventEmitter { }); } + setLastViewedUserSurvey(id) { + this.store.updateState({ + lastViewedUserSurvey: id, + }); + } + setNewPrivacyPolicyToastClickedOrClosed() { this.store.updateState({ newPrivacyPolicyToastClickedOrClosed: true, diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts index 9c9036b87f7b..25b6eae98c33 100644 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -1,6 +1,7 @@ import nock from 'nock'; import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BridgeControllerMessenger } from './types'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; @@ -32,6 +33,28 @@ describe('BridgeController', function () { 'src-network-allowlist': [10, 534352], 'dest-network-allowlist': [137, 42161], }); + nock(BRIDGE_API_BASE_URL) + .get('/getTokens?chainId=10') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1291478912', + symbol: 'DEF', + decimals: 16, + }, + ]); + nock(SWAPS_API_V2_BASE_URL) + .get('/networks/10/topAssets') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); }); it('constructor should setup correctly', function () { @@ -51,4 +74,49 @@ describe('BridgeController', function () { expectedFeatureFlagsResponse, ); }); + + it('selectDestNetwork should set the bridge dest tokens and top assets', async function () { + await bridgeController.selectDestNetwork('0xa'); + expect(bridgeController.state.bridgeState.destTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([ + { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' }, + ]); + }); + + it('selectSrcNetwork should set the bridge src tokens and top assets', async function () { + await bridgeController.selectSrcNetwork('0xa'); + expect(bridgeController.state.bridgeState.srcTokens).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + }); + expect(bridgeController.state.bridgeState.srcTopAssets).toStrictEqual([ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + }); }); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts index 6ca076c2e060..841d735ac52c 100644 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -1,7 +1,14 @@ import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { Hex } from '@metamask/utils'; +import { + fetchBridgeFeatureFlags, + fetchBridgeTokens, + // TODO: Remove restricted import + // eslint-disable-next-line import/no-restricted-paths +} from '../../../../ui/pages/bridge/bridge.util'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import { fetchBridgeFeatureFlags } from '../../../../ui/pages/bridge/bridge.util'; +import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -32,6 +39,14 @@ export default class BridgeController extends BaseController< `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, this.setBridgeFeatureFlags.bind(this), ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectSrcNetwork`, + this.selectSrcNetwork.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:selectDestNetwork`, + this.selectDestNetwork.bind(this), + ); } resetState = () => { @@ -49,4 +64,33 @@ export default class BridgeController extends BaseController< _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; }); }; + + selectSrcNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'srcTopAssets'); + await this.#setTokens(chainId, 'srcTokens'); + }; + + selectDestNetwork = async (chainId: Hex) => { + await this.#setTopAssets(chainId, 'destTopAssets'); + await this.#setTokens(chainId, 'destTokens'); + }; + + #setTopAssets = async ( + chainId: Hex, + stateKey: 'srcTopAssets' | 'destTopAssets', + ) => { + const { bridgeState } = this.state; + const topAssets = await fetchTopAssetsList(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: topAssets }; + }); + }; + + #setTokens = async (chainId: Hex, stateKey: 'srcTokens' | 'destTokens') => { + const { bridgeState } = this.state; + const tokens = await fetchBridgeTokens(chainId); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, [stateKey]: tokens }; + }); + }; } diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts index f2932120f98d..58c7d015b7bb 100644 --- a/app/scripts/controllers/bridge/constants.ts +++ b/app/scripts/controllers/bridge/constants.ts @@ -8,4 +8,8 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }, + srcTokens: {}, + srcTopAssets: [], + destTokens: {}, + destTopAssets: [], }; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts index aa92a6597c69..2fb36e1e983e 100644 --- a/app/scripts/controllers/bridge/types.ts +++ b/app/scripts/controllers/bridge/types.ts @@ -3,6 +3,7 @@ import { RestrictedControllerMessenger, } from '@metamask/base-controller'; import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; import BridgeController from './bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './constants'; @@ -20,8 +21,16 @@ export type BridgeFeatureFlags = { export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; + srcTokens: Record; + srcTopAssets: { address: string }[]; + destTokens: Record; + destTopAssets: { address: string }[]; }; +export enum BridgeUserAction { + SELECT_SRC_NETWORK = 'selectSrcNetwork', + SELECT_DEST_NETWORK = 'selectDestNetwork', +} export enum BridgeBackgroundAction { SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', } @@ -33,7 +42,9 @@ type BridgeControllerAction = { // Maps to BridgeController function names type BridgeControllerActions = - BridgeControllerAction; + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction; type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index bfe7f79d1ac4..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; @@ -846,8 +852,8 @@ export default class MetaMetricsController { [MetaMetricsUserTrait.Theme]: metamaskState.theme || 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: metamaskState.useTokenDetection, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: - metamaskState.useNativeCurrencyAsPrimaryCurrency, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: + metamaskState.showNativeTokenAsMainBalance, [MetaMetricsUserTrait.CurrentCurrency]: metamaskState.currentCurrency, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: this.extension?.runtime?.id, @@ -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 2113efd1715b..a0505700ef01 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -1088,7 +1088,7 @@ describe('MetaMetricsController', function () { securityAlertsEnabled: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, security_providers: [], names: { [NameType.ETHEREUM_ADDRESS]: { @@ -1122,6 +1122,11 @@ describe('MetaMetricsController', function () { }, }, }, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, }); expect(traits).toStrictEqual({ @@ -1143,7 +1148,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.ThreeBoxEnabled]: false, [MetaMetricsUserTrait.Theme]: 'default', [MetaMetricsUserTrait.TokenDetectionEnabled]: true, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: true, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: true, [MetaMetricsUserTrait.SecurityProviders]: ['blockaid'], ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) [MetaMetricsUserTrait.MmiExtensionId]: 'testid', @@ -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,7 +1187,12 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1208,7 +1219,12 @@ describe('MetaMetricsController', function () { useNftDetection: false, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: false, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + showNativeTokenAsMainBalance: false, }); expect(updatedTraits).toStrictEqual({ @@ -1216,7 +1232,7 @@ describe('MetaMetricsController', function () { [MetaMetricsUserTrait.NumberOfAccounts]: 3, [MetaMetricsUserTrait.NumberOfTokens]: 1, [MetaMetricsUserTrait.OpenseaApiEnabled]: false, - [MetaMetricsUserTrait.UseNativeCurrencyAsPrimaryCurrency]: false, + [MetaMetricsUserTrait.ShowNativeTokenAsMainBalance]: false, }); }); @@ -1245,7 +1261,12 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + showNativeTokenAsMainBalance: true, }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ @@ -1267,7 +1288,12 @@ describe('MetaMetricsController', function () { useNftDetection: true, theme: 'default', useTokenDetection: true, - useNativeCurrencyAsPrimaryCurrency: true, + tokenSortConfig: { + key: 'token-sort-key', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + showNativeTokenAsMainBalance: true, }); expect(updatedTraits).toStrictEqual(null); }); diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 3a9e6cddba6a..348ccd40916b 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -99,7 +99,7 @@ describe('MMIController', function () { 'NetworkController:infuraIsUnblocked', ], }), - state: mockNetworkState({chainId: CHAIN_IDS.SEPOLIA}), + state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), infuraProjectId: 'mock-infura-project-id', }); @@ -272,7 +272,7 @@ describe('MMIController', function () { mmiController.getState = jest.fn(); mmiController.captureException = jest.fn(); - mmiController.accountTracker = { syncWithAddresses: jest.fn() }; + mmiController.accountTrackerController = { syncWithAddresses: jest.fn() }; jest.spyOn(metaMetricsController.store, 'getState').mockReturnValue({ metaMetricsId: mockMetaMetricsId, @@ -385,7 +385,7 @@ describe('MMIController', function () { mmiController.keyringController.addNewAccountForKeyring = jest.fn(); mmiController.custodyController.setAccountDetails = jest.fn(); - mmiController.accountTracker.syncWithAddresses = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); mmiController.storeCustodianSupportedChains = jest.fn(); mmiController.custodyController.storeCustodyStatusMap = jest.fn(); @@ -400,7 +400,9 @@ describe('MMIController', function () { expect( mmiController.custodyController.setAccountDetails, ).toHaveBeenCalled(); - expect(mmiController.accountTracker.syncWithAddresses).toHaveBeenCalled(); + expect( + mmiController.accountTrackerController.syncWithAddresses, + ).toHaveBeenCalled(); expect(mmiController.storeCustodianSupportedChains).toHaveBeenCalled(); expect( mmiController.custodyController.storeCustodyStatusMap, diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index cbb08308ec59..d0e905d673d8 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -39,12 +39,12 @@ import { Signature, ConnectionRequest, } from '../../../shared/constants/mmi-controller'; -import AccountTracker from '../lib/account-tracker'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; +import AccountTrackerController from './account-tracker-controller'; import PreferencesController from './preferences-controller'; import { AppStateController } from './app-state'; @@ -86,7 +86,7 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private getPendingNonce: (address: string) => Promise; - private accountTracker: AccountTracker; + private accountTrackerController: AccountTrackerController; private metaMetricsController: MetaMetricsController; @@ -148,7 +148,7 @@ export default class MMIController extends EventEmitter { this.custodyController = opts.custodyController; this.getState = opts.getState; this.getPendingNonce = opts.getPendingNonce; - this.accountTracker = opts.accountTracker; + this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; this.networkController = opts.networkController; this.permissionController = opts.permissionController; @@ -458,7 +458,7 @@ export default class MMIController extends EventEmitter { const allAccounts = await this.keyringController.getAccounts(); const accountsToTrack = [ - ...new Set( + ...new Set( oldAccounts.concat(allAccounts.map((a: string) => a.toLowerCase())), ), ]; @@ -504,7 +504,7 @@ export default class MMIController extends EventEmitter { } }); - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); for (const address of newAccounts) { try { diff --git a/app/scripts/controllers/permissions/background-api.js b/app/scripts/controllers/permissions/background-api.js index d3a29f129379..b778ff42385d 100644 --- a/app/scripts/controllers/permissions/background-api.js +++ b/app/scripts/controllers/permissions/background-api.js @@ -3,13 +3,10 @@ import { CaveatTypes, RestrictedMethods, } from '../../../../shared/constants/permissions'; -import { CaveatFactories } from './specifications'; +import { CaveatFactories, PermissionNames } from './specifications'; export function getPermissionBackgroundApiMethods(permissionController) { - const addMoreAccounts = (origin, accountOrAccounts) => { - const accounts = Array.isArray(accountOrAccounts) - ? accountOrAccounts - : [accountOrAccounts]; + const addMoreAccounts = (origin, accounts) => { const caveat = CaveatFactories.restrictReturnedAccounts(accounts); permissionController.grantPermissionsIncremental({ @@ -20,11 +17,21 @@ export function getPermissionBackgroundApiMethods(permissionController) { }); }; - return { - addPermittedAccount: (origin, account) => addMoreAccounts(origin, account), + const addMoreChains = (origin, chainIds) => { + const caveat = CaveatFactories.restrictNetworkSwitching(chainIds); + + permissionController.grantPermissionsIncremental({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.permittedChains]: { caveats: [caveat] }, + }, + }); + }; - // To add more than one account when already connected to the dapp - addMorePermittedAccounts: (origin, accounts) => + return { + addPermittedAccount: (origin, account) => + addMoreAccounts(origin, [account]), + addPermittedAccounts: (origin, accounts) => addMoreAccounts(origin, accounts), removePermittedAccount: (origin, account) => { @@ -57,6 +64,52 @@ export function getPermissionBackgroundApiMethods(permissionController) { } }, + addPermittedChain: (origin, chainId) => addMoreChains(origin, [chainId]), + addPermittedChains: (origin, chainIds) => addMoreChains(origin, chainIds), + + removePermittedChain: (origin, chainId) => { + const { value: existingChains } = permissionController.getCaveat( + origin, + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + + const remainingChains = existingChains.filter( + (existingChain) => existingChain !== chainId, + ); + + if (remainingChains.length === existingChains.length) { + return; + } + + if (remainingChains.length === 0) { + permissionController.revokePermission( + origin, + PermissionNames.permittedChains, + ); + } else { + permissionController.updateCaveat( + origin, + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + remainingChains, + ); + } + }, + + requestAccountsAndChainPermissionsWithId: async (origin) => { + const id = nanoid(); + permissionController.requestPermissions( + { origin }, + { + [PermissionNames.eth_accounts]: {}, + [PermissionNames.permittedChains]: {}, + }, + { id }, + ); + return id; + }, + requestAccountsPermissionWithId: async (origin) => { const id = nanoid(); permissionController.requestPermissions( diff --git a/app/scripts/controllers/permissions/background-api.test.js b/app/scripts/controllers/permissions/background-api.test.js index b6ba493ba7df..2a050b29a00e 100644 --- a/app/scripts/controllers/permissions/background-api.test.js +++ b/app/scripts/controllers/permissions/background-api.test.js @@ -3,15 +3,21 @@ import { RestrictedMethods, } from '../../../../shared/constants/permissions'; import { getPermissionBackgroundApiMethods } from './background-api'; -import { CaveatFactories } from './specifications'; +import { CaveatFactories, PermissionNames } from './specifications'; describe('permission background API methods', () => { - const getApprovedPermissions = (accounts) => ({ + const getEthAccountsPermissions = (accounts) => ({ [RestrictedMethods.eth_accounts]: { caveats: [CaveatFactories.restrictReturnedAccounts(accounts)], }, }); + const getPermittedChainsPermissions = (chainIds) => ({ + [PermissionNames.permittedChains]: { + caveats: [CaveatFactories.restrictNetworkSwitching(chainIds)], + }, + }); + describe('addPermittedAccount', () => { it('calls grantPermissionsIncremental with expected parameters', () => { const permissionController = { @@ -29,12 +35,12 @@ describe('permission background API methods', () => { permissionController.grantPermissionsIncremental, ).toHaveBeenCalledWith({ subject: { origin: 'foo.com' }, - approvedPermissions: getApprovedPermissions(['0x1']), + approvedPermissions: getEthAccountsPermissions(['0x1']), }); }); }); - describe('addMorePermittedAccounts', () => { + describe('addPermittedAccounts', () => { it('calls grantPermissionsIncremental with expected parameters for single account', () => { const permissionController = { grantPermissionsIncremental: jest.fn(), @@ -42,7 +48,7 @@ describe('permission background API methods', () => { getPermissionBackgroundApiMethods( permissionController, - ).addMorePermittedAccounts('foo.com', ['0x1']); + ).addPermittedAccounts('foo.com', ['0x1']); expect( permissionController.grantPermissionsIncremental, @@ -51,7 +57,7 @@ describe('permission background API methods', () => { permissionController.grantPermissionsIncremental, ).toHaveBeenCalledWith({ subject: { origin: 'foo.com' }, - approvedPermissions: getApprovedPermissions(['0x1']), + approvedPermissions: getEthAccountsPermissions(['0x1']), }); }); @@ -62,7 +68,7 @@ describe('permission background API methods', () => { getPermissionBackgroundApiMethods( permissionController, - ).addMorePermittedAccounts('foo.com', ['0x1', '0x2']); + ).addPermittedAccounts('foo.com', ['0x1', '0x2']); expect( permissionController.grantPermissionsIncremental, @@ -71,7 +77,7 @@ describe('permission background API methods', () => { permissionController.grantPermissionsIncremental, ).toHaveBeenCalledWith({ subject: { origin: 'foo.com' }, - approvedPermissions: getApprovedPermissions(['0x1', '0x2']), + approvedPermissions: getEthAccountsPermissions(['0x1', '0x2']), }); }); }); @@ -194,4 +200,191 @@ describe('permission background API methods', () => { ); }); }); + + describe('requestAccountsAndChainPermissionsWithId', () => { + it('request eth_accounts and permittedChains permissions and returns the request id', async () => { + const permissionController = { + requestPermissions: jest + .fn() + .mockImplementationOnce(async (_, __, { id }) => { + return [null, { id }]; + }), + }; + + const id = await getPermissionBackgroundApiMethods( + permissionController, + ).requestAccountsAndChainPermissionsWithId('foo.com'); + + expect(permissionController.requestPermissions).toHaveBeenCalledTimes(1); + expect(permissionController.requestPermissions).toHaveBeenCalledWith( + { origin: 'foo.com' }, + { + [PermissionNames.eth_accounts]: {}, + [PermissionNames.permittedChains]: {}, + }, + { id: expect.any(String) }, + ); + + expect(id.length > 0).toBe(true); + expect(id).toStrictEqual( + permissionController.requestPermissions.mock.calls[0][2].id, + ); + }); + }); + + describe('addPermittedChain', () => { + it('calls grantPermissionsIncremental with expected parameters', () => { + const permissionController = { + grantPermissionsIncremental: jest.fn(), + }; + + getPermissionBackgroundApiMethods(permissionController).addPermittedChain( + 'foo.com', + '0x1', + ); + + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledTimes(1); + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledWith({ + subject: { origin: 'foo.com' }, + approvedPermissions: getPermittedChainsPermissions(['0x1']), + }); + }); + }); + + describe('addPermittedChains', () => { + it('calls grantPermissionsIncremental with expected parameters for single chain', () => { + const permissionController = { + grantPermissionsIncremental: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addPermittedChains('foo.com', ['0x1']); + + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledTimes(1); + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledWith({ + subject: { origin: 'foo.com' }, + approvedPermissions: getPermittedChainsPermissions(['0x1']), + }); + }); + + it('calls grantPermissionsIncremental with expected parameters with multiple chains', () => { + const permissionController = { + grantPermissionsIncremental: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addPermittedChains('foo.com', ['0x1', '0x2']); + + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledTimes(1); + expect( + permissionController.grantPermissionsIncremental, + ).toHaveBeenCalledWith({ + subject: { origin: 'foo.com' }, + approvedPermissions: getPermittedChainsPermissions(['0x1', '0x2']), + }); + }); + }); + + describe('removePermittedChain', () => { + it('removes a permitted chain', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1', '0x2'], + }; + }), + revokePermission: jest.fn(), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).removePermittedChain('foo.com', '0x2'); + + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + + expect(permissionController.revokePermission).not.toHaveBeenCalled(); + + expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.updateCaveat).toHaveBeenCalledWith( + 'foo.com', + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ['0x1'], + ); + }); + + it('revokes the permittedChains permission if the removed chain is the only permitted chain', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }; + }), + revokePermission: jest.fn(), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).removePermittedChain('foo.com', '0x1'); + + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + + expect(permissionController.revokePermission).toHaveBeenCalledTimes(1); + expect(permissionController.revokePermission).toHaveBeenCalledWith( + 'foo.com', + PermissionNames.permittedChains, + ); + + expect(permissionController.updateCaveat).not.toHaveBeenCalled(); + }); + + it('does not call permissionController.updateCaveat if the specified chain is not permitted', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { type: CaveatTypes.restrictNetworkSwitching, value: ['0x1'] }; + }), + revokePermission: jest.fn(), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).removePermittedChain('foo.com', '0x2'); + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + PermissionNames.permittedChains, + CaveatTypes.restrictNetworkSwitching, + ); + + expect(permissionController.revokePermission).not.toHaveBeenCalled(); + expect(permissionController.updateCaveat).not.toHaveBeenCalled(); + }); + }); }); diff --git a/app/scripts/controllers/permissions/selectors.js b/app/scripts/controllers/permissions/selectors.js index 1a7fa115dd48..76e638d25b54 100644 --- a/app/scripts/controllers/permissions/selectors.js +++ b/app/scripts/controllers/permissions/selectors.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; import { CaveatTypes } from '../../../../shared/constants/permissions'; +import { PermissionNames } from './specifications'; /** * This file contains selectors for PermissionController selector event @@ -40,47 +41,71 @@ export const getPermittedAccountsByOrigin = createSelector( ); /** - * Given the current and previous exposed accounts for each PermissionController - * subject, returns a new map containing all accounts that have changed. - * The values of each map must be immutable values directly from the - * PermissionController state, or an empty array instantiated in this - * function. + * Get the permitted chains for each subject, keyed by origin. + * The values of the returned map are immutable values from the + * PermissionController state. + * + * @returns {Map} The current origin:chainIds[] map. + */ +export const getPermittedChainsByOrigin = createSelector( + getSubjects, + (subjects) => { + return Object.values(subjects).reduce((originToChainsMap, subject) => { + const caveats = + subject.permissions?.[PermissionNames.permittedChains]?.caveats || []; + + const caveat = caveats.find( + ({ type }) => type === CaveatTypes.restrictNetworkSwitching, + ); + + if (caveat) { + originToChainsMap.set(subject.origin, caveat.value); + } + return originToChainsMap; + }, new Map()); + }, +); + +/** + * Returns a map containing key/value pairs for those that have been + * added, changed, or removed between two string:string[] maps * - * @param {Map} newAccountsMap - The new origin:accounts[] map. - * @param {Map} [previousAccountsMap] - The previous origin:accounts[] map. - * @returns {Map} The origin:accounts[] map of changed accounts. + * @param {Map} currentMap - The new string:string[] map. + * @param {Map} previousMap - The previous string:string[] map. + * @returns {Map} The string:string[] map of changed key/values. */ -export const getChangedAccounts = (newAccountsMap, previousAccountsMap) => { - if (previousAccountsMap === undefined) { - return newAccountsMap; +export const diffMap = (currentMap, previousMap) => { + if (previousMap === undefined) { + return currentMap; } - const changedAccounts = new Map(); - if (newAccountsMap === previousAccountsMap) { - return changedAccounts; + const changedMap = new Map(); + if (currentMap === previousMap) { + return changedMap; } - const newOrigins = new Set([...newAccountsMap.keys()]); + const newKeys = new Set([...currentMap.keys()]); - for (const origin of previousAccountsMap.keys()) { - const newAccounts = newAccountsMap.get(origin) ?? []; + for (const key of previousMap.keys()) { + const currentValue = currentMap.get(key) ?? []; + const previousValue = previousMap.get(key); // The values of these maps are references to immutable values, which is why // a strict equality check is enough for diffing. The values are either from // PermissionController state, or an empty array initialized in the previous - // call to this function. `newAccountsMap` will never contain any empty + // call to this function. `currentMap` will never contain any empty // arrays. - if (previousAccountsMap.get(origin) !== newAccounts) { - changedAccounts.set(origin, newAccounts); + if (currentValue !== previousValue) { + changedMap.set(key, currentValue); } - newOrigins.delete(origin); + newKeys.delete(key); } - // By now, newOrigins is either empty or contains some number of previously - // unencountered origins, and all of their accounts have "changed". - for (const origin of newOrigins.keys()) { - changedAccounts.set(origin, newAccountsMap.get(origin)); + // By now, newKeys is either empty or contains some number of previously + // unencountered origins, and all of their origins have "changed". + for (const origin of newKeys.keys()) { + changedMap.set(origin, currentMap.get(origin)); } - return changedAccounts; + return changedMap; }; diff --git a/app/scripts/controllers/permissions/selectors.test.js b/app/scripts/controllers/permissions/selectors.test.js index a32eabf7738e..41264d405ab2 100644 --- a/app/scripts/controllers/permissions/selectors.test.js +++ b/app/scripts/controllers/permissions/selectors.test.js @@ -1,21 +1,25 @@ import { cloneDeep } from 'lodash'; -import { getChangedAccounts, getPermittedAccountsByOrigin } from './selectors'; +import { CaveatTypes } from '../../../../shared/constants/permissions'; +import { + diffMap, + getPermittedAccountsByOrigin, + getPermittedChainsByOrigin, +} from './selectors'; +import { PermissionNames } from './specifications'; describe('PermissionController selectors', () => { - describe('getChangedAccounts', () => { + describe('diffMap', () => { it('returns the new value if the previous value is undefined', () => { const newAccounts = new Map([['foo.bar', ['0x1']]]); - expect(getChangedAccounts(newAccounts)).toBe(newAccounts); + expect(diffMap(newAccounts)).toBe(newAccounts); }); it('returns an empty map if the new and previous values are the same', () => { const newAccounts = new Map([['foo.bar', ['0x1']]]); - expect(getChangedAccounts(newAccounts, newAccounts)).toStrictEqual( - new Map(), - ); + expect(diffMap(newAccounts, newAccounts)).toStrictEqual(new Map()); }); - it('returns a new map of the changed accounts if the new and previous values differ', () => { + it('returns a new map of the changed key/value pairs if the new and previous maps differ', () => { // We set this on the new and previous value under the key 'foo.bar' to // check that identical values are excluded. const identicalValue = ['0x1']; @@ -32,7 +36,7 @@ describe('PermissionController selectors', () => { ]); newAccounts.set('foo.bar', identicalValue); - expect(getChangedAccounts(newAccounts, previousAccounts)).toStrictEqual( + expect(diffMap(newAccounts, previousAccounts)).toStrictEqual( new Map([ ['bar.baz', ['0x1', '0x2']], ['fizz.buzz', []], @@ -113,4 +117,89 @@ describe('PermissionController selectors', () => { expect(selected2).toBe(getPermittedAccountsByOrigin(state2)); }); }); + + describe('getPermittedChainsByOrigin', () => { + it('memoizes and gets permitted chains by origin', () => { + const state1 = { + subjects: { + 'foo.bar': { + origin: 'foo.bar', + permissions: { + [PermissionNames.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + 'bar.baz': { + origin: 'bar.baz', + permissions: { + [PermissionNames.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x2'], + }, + ], + }, + }, + }, + 'baz.bizz': { + origin: 'baz.fizz', + permissions: { + [PermissionNames.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1', '0x2'], + }, + ], + }, + }, + }, + 'no.accounts': { + // we shouldn't see this in the result + permissions: { + foobar: {}, + }, + }, + }, + }; + + const expected1 = new Map([ + ['foo.bar', ['0x1']], + ['bar.baz', ['0x2']], + ['baz.fizz', ['0x1', '0x2']], + ]); + + const selected1 = getPermittedChainsByOrigin(state1); + + expect(selected1).toStrictEqual(expected1); + // The selector should return the memoized value if state.subjects is + // the same object + expect(selected1).toBe(getPermittedChainsByOrigin(state1)); + + // If we mutate the state, the selector return value should be different + // from the first. + const state2 = cloneDeep(state1); + delete state2.subjects['foo.bar']; + + const expected2 = new Map([ + ['bar.baz', ['0x2']], + ['baz.fizz', ['0x1', '0x2']], + ]); + + const selected2 = getPermittedChainsByOrigin(state2); + + expect(selected2).toStrictEqual(expected2); + expect(selected2).not.toBe(selected1); + // Since we didn't mutate the state at this point, the value should once + // again be the memoized. + expect(selected2).toBe(getPermittedChainsByOrigin(state2)); + }); + }); }); diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 2d25ab16b1e4..8a40082d4d80 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -1,7 +1,6 @@ import { constructPermission, PermissionType, - SubjectType, } from '@metamask/permission-controller'; import { caveatSpecifications as snapsCaveatsSpecifications, @@ -10,6 +9,7 @@ import { import { isValidHexAddress } from '@metamask/utils'; import { CaveatTypes, + EndowmentTypes, RestrictedMethods, } from '../../../../shared/constants/permissions'; @@ -25,7 +25,7 @@ import { */ export const PermissionNames = Object.freeze({ ...RestrictedMethods, - permittedChains: 'endowment:permitted-chains', + ...EndowmentTypes, }); /** @@ -209,9 +209,13 @@ export const getPermissionSpecifications = ({ permissionType: PermissionType.Endowment, targetName: PermissionNames.permittedChains, allowedCaveats: [CaveatTypes.restrictNetworkSwitching], - subjectTypes: [SubjectType.Website], factory: (permissionOptions, requestData) => { + if (requestData === undefined) { + return constructPermission({ + ...permissionOptions, + }); + } if (!requestData.approvedChainIds) { throw new Error( `${PermissionNames.permittedChains}: No approved networks specified.`, diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index cd4cd5e1a5fa..eb126b176a41 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -96,6 +96,7 @@ export type Preferences = { showFiatInTestnets: boolean; showTestNetworks: boolean; smartTransactionsOptInStatus: boolean | null; + showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; petnamesEnabled: boolean; @@ -105,6 +106,12 @@ export type Preferences = { showMultiRpcModal: boolean; isRedesignedConfirmationsDeveloperEnabled: boolean; showConfirmationAdvancedDetails: boolean; + tokenSortConfig: { + key: string; + order: string; + sortCallback: string; + }; + shouldShowAggregatedBalancePopover: boolean; }; export type PreferencesControllerState = { @@ -122,7 +129,9 @@ export type PreferencesControllerState = { useRequestQueue: boolean; openSeaEnabled: boolean; securityAlertsEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; addSnapAccountEnabled: boolean; @@ -223,6 +232,7 @@ export default class PreferencesController { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, @@ -232,6 +242,12 @@ 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 ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, diff --git a/app/scripts/controllers/swaps/swaps.test.ts b/app/scripts/controllers/swaps/swaps.test.ts index b79cc421c85e..d102db6aa3f1 100644 --- a/app/scripts/controllers/swaps/swaps.test.ts +++ b/app/scripts/controllers/swaps/swaps.test.ts @@ -26,6 +26,7 @@ const MOCK_FETCH_PARAMS: FetchTradesInfoParams = { fromAddress: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078', exchangeList: 'zeroExV1', balanceError: false, + enableGasIncludedQuotes: false, }; const TEST_AGG_ID_1 = 'TEST_AGG_1'; @@ -1096,6 +1097,7 @@ describe('SwapsController', function () { oldState.swapsState.swapsStxGetTransactionsRefreshTime, swapsStxBatchStatusRefreshTime: oldState.swapsState.swapsStxBatchStatusRefreshTime, + swapsStxStatusDeadline: oldState.swapsState.swapsStxStatusDeadline, }); }); @@ -1163,6 +1165,7 @@ describe('SwapsController', function () { fromAddress: '', exchangeList: 'zeroExV1', balanceError: false, + enableGasIncludedQuotes: false, metaData: {} as FetchTradesInfoParamsMetadata, }; const swapsFeatureIsLive = false; diff --git a/app/scripts/controllers/swaps/swaps.types.ts b/app/scripts/controllers/swaps/swaps.types.ts index 44e4d4939742..ca059723277a 100644 --- a/app/scripts/controllers/swaps/swaps.types.ts +++ b/app/scripts/controllers/swaps/swaps.types.ts @@ -308,6 +308,7 @@ export type FetchTradesInfoParams = { fromAddress: string; exchangeList: string; balanceError: boolean; + enableGasIncludedQuotes: boolean; }; export type FetchTradesInfoParamsMetadata = { diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js deleted file mode 100644 index 6dbd13f1c2df..000000000000 --- a/app/scripts/lib/account-tracker.js +++ /dev/null @@ -1,629 +0,0 @@ -/* Account Tracker - * - * This module is responsible for tracking any number of accounts - * and caching their current balances & transaction counts. - * - * It also tracks transaction hashes, and checks their inclusion status - * on each new block. - */ - -import EthQuery from '@metamask/eth-query'; -import { v4 as random } from 'uuid'; - -import { ObservableStore } from '@metamask/obs-store'; -import log from 'loglevel'; -import pify from 'pify'; -import { Web3Provider } from '@ethersproject/providers'; -import { Contract } from '@ethersproject/contracts'; -import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; -import { cloneDeep } from 'lodash'; -import { LOCALHOST_RPC_URL } from '../../../shared/constants/network'; - -import { SINGLE_CALL_BALANCES_ADDRESSES } from '../constants/contracts'; -import { previousValueComparator } from './util'; - -/** - * This module is responsible for tracking any number of accounts and caching their current balances & transaction - * counts. - * - * It also tracks transaction hashes, and checks their inclusion status on each new block. - * - * @typedef {object} AccountTracker - * @property {object} store The stored object containing all accounts to track, as well as the current block's gas limit. - * @property {object} store.accounts The accounts currently stored in this AccountTracker - * @property {object} store.accountsByChainId The accounts currently stored in this AccountTracker keyed by chain id - * @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block - * @property {string} store.currentBlockGasLimitByChainId A hex string indicating the gas limit of the current block keyed by chain id - */ -export default class AccountTracker { - /** - * @param {object} opts - Options for initializing the controller - * @param {object} opts.provider - An EIP-1193 provider instance that uses the current global network - * @param {object} opts.blockTracker - A block tracker, which emits events for each new block - * @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network - * @param {Function} opts.getNetworkClientById - Gets the network client with the given id from the NetworkController. - * @param {Function} opts.getNetworkIdentifier - A function that returns the current network or passed nework configuration - * @param {Function} opts.onAccountRemoved - Allows subscribing to keyring controller accountRemoved event - */ - #pollingTokenSets = new Map(); - - #listeners = {}; - - #provider = null; - - #blockTracker = null; - - #currentBlockNumberByChainId = {}; - - constructor(opts = {}) { - const initState = { - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, - }; - this.store = new ObservableStore({ ...initState, ...opts.initState }); - - this.resetState = () => { - this.store.updateState(initState); - }; - - this.#provider = opts.provider; - this.#blockTracker = opts.blockTracker; - - this.getCurrentChainId = opts.getCurrentChainId; - this.getNetworkClientById = opts.getNetworkClientById; - this.getNetworkIdentifier = opts.getNetworkIdentifier; - this.preferencesController = opts.preferencesController; - this.onboardingController = opts.onboardingController; - this.controllerMessenger = opts.controllerMessenger; - - // subscribe to account removal - opts.onAccountRemoved((address) => this.removeAccounts([address])); - - this.controllerMessenger.subscribe( - 'OnboardingController:stateChange', - previousValueComparator((prevState, currState) => { - const { completedOnboarding: prevCompletedOnboarding } = prevState; - const { completedOnboarding: currCompletedOnboarding } = currState; - if (!prevCompletedOnboarding && currCompletedOnboarding) { - this.updateAccountsAllActiveNetworks(); - } - }, this.onboardingController.state), - ); - - this.selectedAccount = this.controllerMessenger.call( - 'AccountsController:getSelectedAccount', - ); - - this.controllerMessenger.subscribe( - 'AccountsController:selectedEvmAccountChange', - (newAccount) => { - const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); - - if ( - this.selectedAccount.id !== newAccount.id && - !useMultiAccountBalanceChecker - ) { - this.selectedAccount = newAccount; - this.updateAccountsAllActiveNetworks(); - } - }, - ); - } - - /** - * Starts polling with global selected network - */ - start() { - // blockTracker.currentBlock may be null - this.#currentBlockNumberByChainId = { - [this.getCurrentChainId()]: this.#blockTracker.getCurrentBlock(), - }; - this.#blockTracker.once('latest', (blockNumber) => { - this.#currentBlockNumberByChainId[this.getCurrentChainId()] = blockNumber; - }); - - // remove first to avoid double add - this.#blockTracker.removeListener('latest', this.#updateForBlock); - // add listener - this.#blockTracker.addListener('latest', this.#updateForBlock); - // fetch account balances - this.updateAccounts(); - } - - /** - * Stops polling with global selected network - */ - stop() { - // remove listener - this.#blockTracker.removeListener('latest', this.#updateForBlock); - } - - /** - * Resolves a networkClientId to a network client config - * or globally selected network config if not provided - * - * @param networkClientId - Optional networkClientId to fetch a network client with - * @returns network client config - */ - #getCorrectNetworkClient(networkClientId) { - if (networkClientId) { - const networkClient = this.getNetworkClientById(networkClientId); - - return { - chainId: networkClient.configuration.chainId, - provider: networkClient.provider, - blockTracker: networkClient.blockTracker, - identifier: this.getNetworkIdentifier(networkClient.configuration), - }; - } - return { - chainId: this.getCurrentChainId(), - provider: this.#provider, - blockTracker: this.#blockTracker, - identifier: this.getNetworkIdentifier(), - }; - } - - /** - * Starts polling for a networkClientId - * - * @param networkClientId - The networkClientId to start polling for - * @returns pollingToken - */ - startPollingByNetworkClientId(networkClientId) { - const pollToken = random(); - - const pollingTokenSet = this.#pollingTokenSets.get(networkClientId); - if (pollingTokenSet) { - pollingTokenSet.add(pollToken); - } else { - const set = new Set(); - set.add(pollToken); - this.#pollingTokenSets.set(networkClientId, set); - this.#subscribeWithNetworkClientId(networkClientId); - } - return pollToken; - } - - /** - * Stops polling for all networkClientIds - */ - stopAllPolling() { - this.stop(); - this.#pollingTokenSets.forEach((tokenSet, _networkClientId) => { - tokenSet.forEach((token) => { - this.stopPollingByPollingToken(token); - }); - }); - } - - /** - * Stops polling for a networkClientId - * - * @param pollingToken - The polling token to stop polling for - */ - stopPollingByPollingToken(pollingToken) { - if (!pollingToken) { - throw new Error('pollingToken required'); - } - let found = false; - this.#pollingTokenSets.forEach((tokenSet, key) => { - if (tokenSet.has(pollingToken)) { - found = true; - tokenSet.delete(pollingToken); - if (tokenSet.size === 0) { - this.#pollingTokenSets.delete(key); - this.#unsubscribeWithNetworkClientId(key); - } - } - }); - if (!found) { - throw new Error('pollingToken not found'); - } - } - - /** - * Subscribes from the block tracker for the given networkClientId if not currently subscribed - * - * @param {string} networkClientId - network client ID to fetch a block tracker with - */ - #subscribeWithNetworkClientId(networkClientId) { - if (this.#listeners[networkClientId]) { - return; - } - const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); - const updateForBlock = this.#updateForBlockByNetworkClientId.bind( - this, - networkClientId, - ); - blockTracker.addListener('latest', updateForBlock); - - this.#listeners[networkClientId] = updateForBlock; - - this.updateAccounts(networkClientId); - } - - /** - * Unsubscribes from the block tracker for the given networkClientId if currently subscribed - * - * @param {string} networkClientId - The network client ID to fetch a block tracker with - */ - #unsubscribeWithNetworkClientId(networkClientId) { - if (!this.#listeners[networkClientId]) { - return; - } - const { blockTracker } = this.#getCorrectNetworkClient(networkClientId); - blockTracker.removeListener('latest', this.#listeners[networkClientId]); - - delete this.#listeners[networkClientId]; - } - - /** - * Returns the accounts object for the chain ID, or initializes it from the globally selected - * if it doesn't already exist. - * - * @private - * @param {string} chainId - The chain ID - */ - #getAccountsForChainId(chainId) { - const { accounts, accountsByChainId } = this.store.getState(); - if (accountsByChainId[chainId]) { - return cloneDeep(accountsByChainId[chainId]); - } - - const newAccounts = {}; - Object.keys(accounts).forEach((address) => { - newAccounts[address] = {}; - }); - return newAccounts; - } - - /** - * Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this - * AccountTracker. - * - * Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each - * of these accounts are given an updated balance via EthQuery. - * - * @param {Array} addresses - The array of hex addresses for accounts with which this AccountTracker's accounts should be - * in sync - */ - syncWithAddresses(addresses) { - const { accounts } = this.store.getState(); - const locals = Object.keys(accounts); - - const accountsToAdd = []; - addresses.forEach((upstream) => { - if (!locals.includes(upstream)) { - accountsToAdd.push(upstream); - } - }); - - const accountsToRemove = []; - locals.forEach((local) => { - if (!addresses.includes(local)) { - accountsToRemove.push(local); - } - }); - - this.addAccounts(accountsToAdd); - this.removeAccounts(accountsToRemove); - } - - /** - * Adds new addresses to track the balances of - * given a balance as long this.#currentBlockNumberByChainId is defined for the chainId. - * - * @param {Array} addresses - An array of hex addresses of new accounts to track - */ - addAccounts(addresses) { - const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); - const accounts = cloneDeep(_accounts); - const accountsByChainId = cloneDeep(_accountsByChainId); - - // add initial state for addresses - addresses.forEach((address) => { - accounts[address] = {}; - }); - Object.keys(accountsByChainId).forEach((chainId) => { - addresses.forEach((address) => { - accountsByChainId[chainId][address] = {}; - }); - }); - // save accounts state - this.store.updateState({ accounts, accountsByChainId }); - - // fetch balances for the accounts if there is block number ready - if (this.#currentBlockNumberByChainId[this.getCurrentChainId()]) { - this.updateAccounts(); - } - this.#pollingTokenSets.forEach((_tokenSet, networkClientId) => { - const { chainId } = this.#getCorrectNetworkClient(networkClientId); - if (this.#currentBlockNumberByChainId[chainId]) { - this.updateAccounts(networkClientId); - } - }); - } - - /** - * Removes accounts from being tracked - * - * @param {Array} addresses - An array of hex addresses to stop tracking. - */ - removeAccounts(addresses) { - const { accounts: _accounts, accountsByChainId: _accountsByChainId } = - this.store.getState(); - const accounts = cloneDeep(_accounts); - const accountsByChainId = cloneDeep(_accountsByChainId); - - // remove each state object - addresses.forEach((address) => { - delete accounts[address]; - }); - Object.keys(accountsByChainId).forEach((chainId) => { - addresses.forEach((address) => { - delete accountsByChainId[chainId][address]; - }); - }); - // save accounts state - this.store.updateState({ accounts, accountsByChainId }); - } - - /** - * Removes all addresses and associated balances - */ - clearAccounts() { - this.store.updateState({ - accounts: {}, - accountsByChainId: { - [this.getCurrentChainId()]: {}, - }, - }); - } - - /** - * Given a block, updates this AccountTracker's currentBlockGasLimit and currentBlockGasLimitByChainId and then updates - * each local account's balance via EthQuery - * - * @private - * @param {number} blockNumber - the block number to update to. - * @fires 'block' The updated state, if all account updates are successful - */ - #updateForBlock = async (blockNumber) => { - await this.#updateForBlockByNetworkClientId(null, blockNumber); - }; - - /** - * Given a block, updates this AccountTracker's currentBlockGasLimitByChainId, and then updates each local account's balance - * via EthQuery - * - * @private - * @param {string} networkClientId - optional network client ID to use instead of the globally selected network. - * @param {number} blockNumber - the block number to update to. - * @fires 'block' The updated state, if all account updates are successful - */ - async #updateForBlockByNetworkClientId(networkClientId, blockNumber) { - const { chainId, provider } = - this.#getCorrectNetworkClient(networkClientId); - this.#currentBlockNumberByChainId[chainId] = blockNumber; - - // block gasLimit polling shouldn't be in account-tracker shouldn't be here... - const currentBlock = await pify(new EthQuery(provider)).getBlockByNumber( - blockNumber, - false, - ); - if (!currentBlock) { - return; - } - const currentBlockGasLimit = currentBlock.gasLimit; - const { currentBlockGasLimitByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { - currentBlockGasLimit, - }), - currentBlockGasLimitByChainId: { - ...currentBlockGasLimitByChainId, - [chainId]: currentBlockGasLimit, - }, - }); - - try { - await this.updateAccounts(networkClientId); - } catch (err) { - log.error(err); - } - } - - /** - * Updates accounts for the globally selected network - * and all networks that are currently being polled. - * - * @returns {Promise} after all account balances updated - */ - async updateAccountsAllActiveNetworks() { - await this.updateAccounts(); - await Promise.all( - Array.from(this.#pollingTokenSets).map(([networkClientId]) => { - return this.updateAccounts(networkClientId); - }), - ); - } - - /** - * balanceChecker is deployed on main eth (test)nets and requires a single call - * for all other networks, calls this.#updateAccount for each account in this.store - * - * @param {string} networkClientId - optional network client ID to use instead of the globally selected network. - * @returns {Promise} after all account balances updated - */ - async updateAccounts(networkClientId) { - const { completedOnboarding } = this.onboardingController.state; - if (!completedOnboarding) { - return; - } - - const { chainId, provider, identifier } = - this.#getCorrectNetworkClient(networkClientId); - const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); - - let addresses = []; - if (useMultiAccountBalanceChecker) { - const { accounts } = this.store.getState(); - - addresses = Object.keys(accounts); - } else { - const selectedAddress = this.controllerMessenger.call( - 'AccountsController:getSelectedAccount', - ).address; - - addresses = [selectedAddress]; - } - - const rpcUrl = 'http://127.0.0.1:8545'; - const singleCallBalancesAddress = SINGLE_CALL_BALANCES_ADDRESSES[chainId]; - if ( - identifier === LOCALHOST_RPC_URL || - identifier === rpcUrl || - !singleCallBalancesAddress - ) { - await Promise.all( - addresses.map((address) => - this.#updateAccount(address, provider, chainId), - ), - ); - } else { - await this.#updateAccountsViaBalanceChecker( - addresses, - singleCallBalancesAddress, - provider, - chainId, - ); - } - } - - /** - * Updates the current balance of an account. - * - * @private - * @param {string} address - A hex address of a the account to be updated - * @param {object} provider - The provider instance to fetch the balance with - * @param {string} chainId - The chain ID to update in state - * @returns {Promise} after the account balance is updated - */ - - async #updateAccount(address, provider, chainId) { - const { useMultiAccountBalanceChecker } = - this.preferencesController.store.getState(); - - let balance = '0x0'; - - // query balance - try { - balance = await pify(new EthQuery(provider)).getBalance(address); - } catch (error) { - if (error.data?.request?.method !== 'eth_getBalance') { - throw error; - } - } - - const result = { address, balance }; - // update accounts state - const accounts = this.#getAccountsForChainId(chainId); - // only populate if the entry is still present - if (!accounts[address]) { - return; - } - - let newAccounts = accounts; - if (!useMultiAccountBalanceChecker) { - newAccounts = {}; - Object.keys(accounts).forEach((accountAddress) => { - if (address !== accountAddress) { - newAccounts[accountAddress] = { - address: accountAddress, - balance: null, - }; - } - }); - } - - newAccounts[address] = result; - - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, - }); - } - - /** - * Updates current address balances from balanceChecker deployed contract instance - * - * @private - * @param {Array} addresses - A hex addresses of a the accounts to be updated - * @param {string} deployedContractAddress - The contract address to fetch balances with - * @param {object} provider - The provider instance to fetch the balance with - * @param {string} chainId - The chain ID to update in state - * @returns {Promise} after the account balance is updated - */ - async #updateAccountsViaBalanceChecker( - addresses, - deployedContractAddress, - provider, - chainId, - ) { - const ethContract = await new Contract( - deployedContractAddress, - SINGLE_CALL_BALANCES_ABI, - new Web3Provider(provider), - ); - const ethBalance = ['0x0000000000000000000000000000000000000000']; - - try { - const balances = await ethContract.balances(addresses, ethBalance); - - const accounts = this.#getAccountsForChainId(chainId); - const newAccounts = {}; - Object.keys(accounts).forEach((address) => { - if (!addresses.includes(address)) { - newAccounts[address] = { address, balance: null }; - } - }); - addresses.forEach((address, index) => { - const balance = balances[index] ? balances[index].toHexString() : '0x0'; - newAccounts[address] = { address, balance }; - }); - - const { accountsByChainId } = this.store.getState(); - this.store.updateState({ - ...(chainId === this.getCurrentChainId() && { - accounts: newAccounts, - }), - accountsByChainId: { - ...accountsByChainId, - [chainId]: newAccounts, - }, - }); - } catch (error) { - log.warn( - `MetaMask - Account Tracker single call balance fetch failed`, - error, - ); - Promise.allSettled( - addresses.map((address) => - this.#updateAccount(address, provider, chainId), - ), - ); - } - } -} diff --git a/app/scripts/lib/account-tracker.test.js b/app/scripts/lib/account-tracker.test.js deleted file mode 100644 index 4bd73a472811..000000000000 --- a/app/scripts/lib/account-tracker.test.js +++ /dev/null @@ -1,729 +0,0 @@ -import EventEmitter from 'events'; -import { ControllerMessenger } from '@metamask/base-controller'; - -import { flushPromises } from '../../../test/lib/timer-helpers'; -import { createTestProviderTools } from '../../../test/stub/provider'; -import AccountTracker from './account-tracker'; - -const noop = () => true; -const currentNetworkId = '5'; -const currentChainId = '0x5'; -const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; -const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; - -const SELECTED_ADDRESS = '0x123'; - -const INITIAL_BALANCE_1 = '0x1'; -const INITIAL_BALANCE_2 = '0x2'; -const UPDATE_BALANCE = '0xabc'; -const UPDATE_BALANCE_HOOK = '0xabcd'; - -const GAS_LIMIT = '0x111111'; -const GAS_LIMIT_HOOK = '0x222222'; - -// The below three values were generated by running MetaMask in the browser -// The response to eth_call, which is called via `ethContract.balances` -// in `_updateAccountsViaBalanceChecker` of account-tracker.js, needs to be properly -// formatted or else ethers will throw an error. -const ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN = - '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800600000000000000000000000000000000000000000000000000000000000186a0'; -const EXPECTED_CONTRACT_BALANCE_1 = '0x038d7ea4c68006'; -const EXPECTED_CONTRACT_BALANCE_2 = '0x0186a0'; - -const mockAccounts = { - [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: INITIAL_BALANCE_1 }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: INITIAL_BALANCE_2, - }, -}; - -function buildMockBlockTracker({ shouldStubListeners = true } = {}) { - const blockTrackerStub = new EventEmitter(); - blockTrackerStub.getCurrentBlock = noop; - blockTrackerStub.getLatestBlock = noop; - if (shouldStubListeners) { - jest.spyOn(blockTrackerStub, 'addListener').mockImplementation(); - jest.spyOn(blockTrackerStub, 'removeListener').mockImplementation(); - } - return blockTrackerStub; -} - -function buildAccountTracker({ - completedOnboarding = false, - useMultiAccountBalanceChecker = false, - ...accountTrackerOptions -} = {}) { - const { provider } = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT }, - }, - networkId: currentNetworkId, - chainId: currentNetworkId, - }); - const blockTrackerStub = buildMockBlockTracker(); - - const providerFromHook = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE_HOOK, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, - }, - networkId: '0x1', - chainId: '0x1', - }).provider; - - const blockTrackerFromHookStub = buildMockBlockTracker(); - - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - - const controllerMessenger = new ControllerMessenger(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => ({ - id: 'accountId', - address: SELECTED_ADDRESS, - }), - ); - - const accountTracker = new AccountTracker({ - provider, - blockTracker: blockTrackerStub, - getNetworkClientById: getNetworkClientByIdStub, - getNetworkIdentifier: jest.fn(), - preferencesController: { - store: { - getState: () => ({ - useMultiAccountBalanceChecker, - }), - subscribe: noop, - }, - }, - onboardingController: { - state: { - completedOnboarding, - }, - }, - controllerMessenger, - onAccountRemoved: noop, - getCurrentChainId: () => currentChainId, - ...accountTrackerOptions, - }); - - return { accountTracker, blockTrackerFromHookStub, blockTrackerStub }; -} - -describe('Account Tracker', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('start', () => { - it('restarts the subscription to the block tracker and update accounts', async () => { - const { accountTracker, blockTrackerStub } = buildAccountTracker(); - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.start(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(1); // called first time with no args - - accountTracker.start(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 2, - 'latest', - expect.any(Function), - ); - expect(blockTrackerStub.addListener).toHaveBeenNthCalledWith( - 2, - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(2); // called second time with no args - - accountTracker.stop(); - }); - }); - - describe('stop', () => { - it('ends the subscription to the block tracker', async () => { - const { accountTracker, blockTrackerStub } = buildAccountTracker(); - - accountTracker.stop(); - - expect(blockTrackerStub.removeListener).toHaveBeenNthCalledWith( - 1, - 'latest', - expect.any(Function), - ); - }); - }); - - describe('startPollingByNetworkClientId', () => { - it('should subscribe to the block tracker and update accounts if not already using the networkClientId', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub.addListener).toHaveBeenCalledTimes(1); - expect(updateAccountsSpy).toHaveBeenCalledTimes(1); - - accountTracker.stopAllPolling(); - }); - - it('should subscribe to the block tracker and update accounts for each networkClientId', async () => { - const blockTrackerFromHookStub1 = buildMockBlockTracker(); - const blockTrackerFromHookStub2 = buildMockBlockTracker(); - const blockTrackerFromHookStub3 = buildMockBlockTracker(); - const getNetworkClientByIdStub = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub1, - }; - case 'goerli': - return { - configuration: { - chainId: '0x5', - }, - blockTracker: blockTrackerFromHookStub2, - }; - case 'networkClientId1': - return { - configuration: { - chainId: '0xa', - }, - blockTracker: blockTrackerFromHookStub3, - }; - default: - throw new Error('unexpected networkClientId'); - } - }); - const { accountTracker } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - expect(blockTrackerFromHookStub1.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - accountTracker.startPollingByNetworkClientId('goerli'); - - expect(blockTrackerFromHookStub2.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('goerli'); - - accountTracker.startPollingByNetworkClientId('networkClientId1'); - - expect(blockTrackerFromHookStub3.addListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(updateAccountsSpy).toHaveBeenCalledWith('networkClientId1'); - - accountTracker.stopAllPolling(); - }); - }); - - describe('stopPollingByPollingToken', () => { - it('should unsubscribe from the block tracker when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - const pollingToken = - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.stopPollingByPollingToken(pollingToken); - - expect(blockTrackerFromHookStub.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - }); - - it('should not unsubscribe from the block tracker if called with one of multiple active polling tokens for a given networkClient', async () => { - const { accountTracker, blockTrackerFromHookStub } = - buildAccountTracker(); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - const pollingToken1 = - accountTracker.startPollingByNetworkClientId('mainnet'); - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.stopPollingByPollingToken(pollingToken1); - - expect(blockTrackerFromHookStub.removeListener).not.toHaveBeenCalled(); - - accountTracker.stopAllPolling(); - }); - - it('should error if no pollingToken is passed', () => { - const { accountTracker } = buildAccountTracker(); - - expect(() => { - accountTracker.stopPollingByPollingToken(undefined); - }).toThrow('pollingToken required'); - }); - - it('should error if no matching pollingToken is found', () => { - const { accountTracker } = buildAccountTracker(); - - expect(() => { - accountTracker.stopPollingByPollingToken('potato'); - }).toThrow('pollingToken not found'); - }); - }); - - describe('stopAll', () => { - it('should end all subscriptions', async () => { - const blockTrackerFromHookStub1 = buildMockBlockTracker(); - const blockTrackerFromHookStub2 = buildMockBlockTracker(); - const getNetworkClientByIdStub = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub1, - }; - case 'goerli': - return { - configuration: { - chainId: '0x5', - }, - blockTracker: blockTrackerFromHookStub2, - }; - default: - throw new Error('unexpected networkClientId'); - } - }); - const { accountTracker, blockTrackerStub } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - jest.spyOn(accountTracker, 'updateAccounts').mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - accountTracker.startPollingByNetworkClientId('goerli'); - - accountTracker.stopAllPolling(); - - expect(blockTrackerStub.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(blockTrackerFromHookStub1.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - expect(blockTrackerFromHookStub2.removeListener).toHaveBeenCalledWith( - 'latest', - expect.any(Function), - ); - }); - }); - - describe('blockTracker "latest" events', () => { - it('updates currentBlockGasLimit, currentBlockGasLimitByChainId, and accounts when polling is initiated via `start`', async () => { - const blockTrackerStub = buildMockBlockTracker({ - shouldStubListeners: false, - }); - const { accountTracker } = buildAccountTracker({ - blockTracker: blockTrackerStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.start(); - blockTrackerStub.emit('latest', 'blockNumber'); - - await flushPromises(); - - expect(updateAccountsSpy).toHaveBeenCalledWith(null); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: {}, - currentBlockGasLimit: GAS_LIMIT, - currentBlockGasLimitByChainId: { - [currentChainId]: GAS_LIMIT, - }, - }); - - accountTracker.stop(); - }); - - it('updates only the currentBlockGasLimitByChainId and accounts when polling is initiated via `startPollingByNetworkClientId`', async () => { - const blockTrackerFromHookStub = buildMockBlockTracker({ - shouldStubListeners: false, - }); - const providerFromHook = createTestProviderTools({ - scaffold: { - eth_getBalance: UPDATE_BALANCE_HOOK, - eth_call: ETHERS_CONTRACT_BALANCES_ETH_CALL_RETURN, - eth_getBlockByNumber: { gasLimit: GAS_LIMIT_HOOK }, - }, - networkId: '0x1', - chainId: '0x1', - }).provider; - const getNetworkClientByIdStub = jest.fn().mockReturnValue({ - configuration: { - chainId: '0x1', - }, - blockTracker: blockTrackerFromHookStub, - provider: providerFromHook, - }); - const { accountTracker } = buildAccountTracker({ - getNetworkClientById: getNetworkClientByIdStub, - }); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - - accountTracker.startPollingByNetworkClientId('mainnet'); - - blockTrackerFromHookStub.emit('latest', 'blockNumber'); - - await flushPromises(); - - expect(updateAccountsSpy).toHaveBeenCalledWith('mainnet'); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: {}, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: { - '0x1': GAS_LIMIT_HOOK, - }, - }); - - accountTracker.stopAllPolling(); - }); - }); - - describe('updateAccountsAllActiveNetworks', () => { - it('updates accounts for the globally selected network and all currently polling networks', async () => { - const { accountTracker } = buildAccountTracker(); - - const updateAccountsSpy = jest - .spyOn(accountTracker, 'updateAccounts') - .mockResolvedValue(); - await accountTracker.startPollingByNetworkClientId('networkClientId1'); - await accountTracker.startPollingByNetworkClientId('networkClientId2'); - await accountTracker.startPollingByNetworkClientId('networkClientId3'); - - expect(updateAccountsSpy).toHaveBeenCalledTimes(3); - - await accountTracker.updateAccountsAllActiveNetworks(); - - expect(updateAccountsSpy).toHaveBeenCalledTimes(7); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(4); // called with no args - expect(updateAccountsSpy).toHaveBeenNthCalledWith(5, 'networkClientId1'); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(6, 'networkClientId2'); - expect(updateAccountsSpy).toHaveBeenNthCalledWith(7, 'networkClientId3'); - }); - }); - - describe('updateAccounts', () => { - it('does not update accounts if completedOnBoarding is false', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: false, - }); - - await accountTracker.updateAccounts(); - - const state = accountTracker.store.getState(); - expect(state).toStrictEqual({ - accounts: {}, - currentBlockGasLimit: '', - accountsByChainId: {}, - currentBlockGasLimitByChainId: {}, - }); - }); - - describe('chain does not have single call balance address', () => { - const getCurrentChainIdStub = () => '0x999'; // chain without single call balance address - const mockAccountsWithSelectedAddress = { - ...mockAccounts, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: '0x0', - }, - }; - const mockInitialState = { - accounts: mockAccountsWithSelectedAddress, - accountsByChainId: { - '0x999': mockAccountsWithSelectedAddress, - }, - }; - - describe('when useMultiAccountBalanceChecker is true', () => { - it('updates all accounts directly', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: true, - getCurrentChainId: getCurrentChainIdStub, - }); - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts(); - - const accounts = { - [VALID_ADDRESS]: { - address: VALID_ADDRESS, - balance: UPDATE_BALANCE, - }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: UPDATE_BALANCE, - }, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: UPDATE_BALANCE, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x999': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - - describe('when useMultiAccountBalanceChecker is false', () => { - it('updates only the selectedAddress directly, setting other balances to null', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: false, - getCurrentChainId: getCurrentChainIdStub, - }); - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts(); - - const accounts = { - [VALID_ADDRESS]: { address: VALID_ADDRESS, balance: null }, - [VALID_ADDRESS_TWO]: { address: VALID_ADDRESS_TWO, balance: null }, - [SELECTED_ADDRESS]: { - address: SELECTED_ADDRESS, - balance: UPDATE_BALANCE, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x999': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - }); - - describe('chain does have single call balance address and network is not localhost', () => { - const getNetworkIdentifierStub = jest - .fn() - .mockReturnValue('http://not-localhost:8545'); - const controllerMessenger = new ControllerMessenger(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => ({ - id: 'accountId', - address: VALID_ADDRESS, - }), - ); - const getCurrentChainIdStub = () => '0x1'; // chain with single call balance address - const mockInitialState = { - accounts: { ...mockAccounts }, - accountsByChainId: { - '0x1': { ...mockAccounts }, - }, - }; - - describe('when useMultiAccountBalanceChecker is true', () => { - it('updates all accounts via balance checker', async () => { - const { accountTracker } = buildAccountTracker({ - completedOnboarding: true, - useMultiAccountBalanceChecker: true, - controllerMessenger, - getNetworkIdentifier: getNetworkIdentifierStub, - getCurrentChainId: getCurrentChainIdStub, - }); - - accountTracker.store.updateState(mockInitialState); - - await accountTracker.updateAccounts('mainnet'); - - const accounts = { - [VALID_ADDRESS]: { - address: VALID_ADDRESS, - balance: EXPECTED_CONTRACT_BALANCE_1, - }, - [VALID_ADDRESS_TWO]: { - address: VALID_ADDRESS_TWO, - balance: EXPECTED_CONTRACT_BALANCE_2, - }, - }; - - const newState = accountTracker.store.getState(); - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - '0x1': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - }); - }); - - describe('onAccountRemoved', () => { - it('should remove an account from state', () => { - let accountRemovedListener; - const { accountTracker } = buildAccountTracker({ - onAccountRemoved: (callback) => { - accountRemovedListener = callback; - }, - }); - accountTracker.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, - }, - }, - }); - - accountRemovedListener(VALID_ADDRESS); - - const newState = accountTracker.store.getState(); - - const accounts = { - [VALID_ADDRESS_TWO]: mockAccounts[VALID_ADDRESS_TWO], - }; - - expect(newState).toStrictEqual({ - accounts, - accountsByChainId: { - [currentChainId]: accounts, - '0x1': accounts, - '0x2': accounts, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); - - describe('clearAccounts', () => { - it('should reset state', () => { - const { accountTracker } = buildAccountTracker(); - - accountTracker.store.updateState({ - accounts: { ...mockAccounts }, - accountsByChainId: { - [currentChainId]: { - ...mockAccounts, - }, - '0x1': { - ...mockAccounts, - }, - '0x2': { - ...mockAccounts, - }, - }, - }); - - accountTracker.clearAccounts(); - - const newState = accountTracker.store.getState(); - - expect(newState).toStrictEqual({ - accounts: {}, - accountsByChainId: { - [currentChainId]: {}, - }, - currentBlockGasLimit: '', - currentBlockGasLimitByChainId: {}, - }); - }); - }); -}); 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/createTracingMiddleware.test.ts b/app/scripts/lib/createTracingMiddleware.test.ts index cafe97b71181..717b17697e74 100644 --- a/app/scripts/lib/createTracingMiddleware.test.ts +++ b/app/scripts/lib/createTracingMiddleware.test.ts @@ -31,7 +31,7 @@ describe('createTracingMiddleware', () => { }); it('does not add trace context to request if method not supported', async () => { - request.method = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4; + request.method = 'unsupportedMethod'; await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); diff --git a/app/scripts/lib/createTracingMiddleware.ts b/app/scripts/lib/createTracingMiddleware.ts index 27e928a95199..1b2daadec842 100644 --- a/app/scripts/lib/createTracingMiddleware.ts +++ b/app/scripts/lib/createTracingMiddleware.ts @@ -4,6 +4,19 @@ import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { trace, TraceName } from '../../../shared/lib/trace'; +const METHOD_TYPE_TO_TRACE_NAME: Record = { + [MESSAGE_TYPE.ETH_SEND_TRANSACTION]: TraceName.Transaction, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V1]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: TraceName.Signature, + [MESSAGE_TYPE.PERSONAL_SIGN]: TraceName.Signature, +}; + +const METHOD_TYPE_TO_TAGS: Record> = { + [MESSAGE_TYPE.ETH_SEND_TRANSACTION]: { source: 'dapp' }, +}; + export default function createTracingMiddleware() { return async function tracingMiddleware( req: any, @@ -12,11 +25,13 @@ export default function createTracingMiddleware() { ) { const { id, method } = req; - if (method === MESSAGE_TYPE.ETH_SEND_TRANSACTION) { + const traceName = METHOD_TYPE_TO_TRACE_NAME[method]; + + if (traceName) { req.traceContext = await trace({ - name: TraceName.Transaction, + name: traceName, id, - tags: { source: 'dapp' }, + tags: METHOD_TYPE_TO_TAGS[method], }); await trace({ diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts index fce667714ebe..93925bf63a0c 100644 --- a/app/scripts/lib/manifestFlags.ts +++ b/app/scripts/lib/manifestFlags.ts @@ -9,7 +9,10 @@ export type ManifestFlags = { nodeIndex?: number; prNumber?: number; }; - doNotForceSentryForThisTest?: boolean; + sentry?: { + tracesSampleRate?: number; + forceEnable?: boolean; + }; }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- you can't extend a type, we want this to be an interface diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index 857f55af5579..aafde70f2072 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -1,6 +1,6 @@ import { type Hex, JsonRpcResponseStruct } from '@metamask/utils'; - import { detectSIWE, SIWEMessage } from '@metamask/controller-utils'; + import { CHAIN_IDS } from '../../../../shared/constants/network'; import { @@ -8,7 +8,6 @@ import { BlockaidResultType, } from '../../../../shared/constants/security-provider'; import { flushPromises } from '../../../../test/lib/timer-helpers'; - import { createPPOMMiddleware, PPOMMiddlewareRequest } from './ppom-middleware'; import { generateSecurityAlertId, @@ -19,7 +18,10 @@ import { import { SecurityAlertResponse } from './types'; jest.mock('./ppom-util'); -jest.mock('@metamask/controller-utils'); +jest.mock('@metamask/controller-utils', () => ({ + ...jest.requireActual('@metamask/controller-utils'), + detectSIWE: jest.fn(), +})); const SECURITY_ALERT_ID_MOCK = '123'; const INTERNAL_ACCOUNT_ADDRESS = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; @@ -122,6 +124,7 @@ describe('PPOMMiddleware', () => { generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK); handlePPOMErrorMock.mockReturnValue(SECURITY_ALERT_RESPONSE_MOCK); isChainSupportedMock.mockResolvedValue(true); + detectSIWEMock.mockReturnValue({ isSIWEMessage: false } as SIWEMessage); globalThis.sentry = { withIsolationScope: jest 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 adf596824bdd..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,7 @@ const addEthereumChain = { getCurrentChainIdForDomain: true, getCaveat: true, requestPermittedChainsPermission: true, - getChainPermissionsFeatureFlag: true, + grantPermittedChainsPermissionIncremental: true, }, }; @@ -45,7 +45,7 @@ async function addEthereumChainHandler( getCurrentChainIdForDomain, getCaveat, requestPermittedChainsPermission, - getChainPermissionsFeatureFlag, + grantPermittedChainsPermissionIncremental, }, ) { let validParams; @@ -65,9 +65,6 @@ async function addEthereumChainHandler( const { origin } = req; const currentChainIdForDomain = getCurrentChainIdForDomain(origin); - const currentNetworkConfiguration = getNetworkConfigurationByChainId( - currentChainIdForDomain, - ); const existingNetwork = getNetworkConfigurationByChainId(chainId); if ( @@ -196,28 +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, - { - getChainPermissionsFeatureFlag, - setActiveNetwork, - requestUserApproval, - getCaveat, - requestPermittedChainsPermission, - endApprovalFlow, - }, - ); + 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 d04037949e87..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), @@ -70,6 +64,7 @@ describe('addEthereumChainHandler', () => { setActiveNetwork: jest.fn(), requestUserApproval: jest.fn().mockResolvedValue(123), requestPermittedChainsPermission: jest.fn(), + grantPermittedChainsPermissionIncremental: jest.fn(), getCaveat: jest.fn().mockReturnValue({ value: permissionedChainIds }), startApprovalFlow: () => ({ id: 'approvalFlowId' }), endApprovalFlow: jest.fn(), @@ -91,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', @@ -117,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'], @@ -140,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', @@ -171,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() @@ -257,7 +246,6 @@ describe('addEthereumChainHandler', () => { }; const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { getNetworkConfigurationByChainId: jest .fn() @@ -304,7 +292,6 @@ describe('addEthereumChainHandler', () => { const existingNetwork = createMockMainnetConfiguration(); const mocks = makeMocks({ - permissionsFeatureFlagIsActive: false, overrides: { // Start on sepolia getCurrentChainIdForDomain: jest @@ -348,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( @@ -379,7 +364,6 @@ describe('addEthereumChainHandler', () => { const mocks = makeMocks({ permissionedChainIds: [], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -411,10 +395,12 @@ describe('addEthereumChainHandler', () => { ); expect(mocks.addNetwork).toHaveBeenCalledWith(nonInfuraConfiguration); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledWith([ - createMockNonInfuraConfiguration().chainId, - ]); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledWith([createMockNonInfuraConfiguration().chainId]); expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); }); @@ -424,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() @@ -462,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 @@ -497,12 +481,12 @@ describe('addEthereumChainHandler', () => { ); expect(mocks.updateNetwork).toHaveBeenCalledTimes(1); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes( - 1, - ); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledWith([ - NON_INFURA_CHAIN_ID, - ]); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledWith([NON_INFURA_CHAIN_ID]); expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); }); }); @@ -513,7 +497,6 @@ describe('addEthereumChainHandler', () => { createMockOptimismConfiguration().chainId, CHAIN_IDS.MAINNET, ], - permissionsFeatureFlagIsActive: true, overrides: { getCurrentChainIdForDomain: jest .fn() @@ -559,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'; @@ -601,13 +582,12 @@ 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 .fn() .mockReturnValue(CHAIN_IDS.SEPOLIA), - requestPermittedChainsPermission: jest + grantPermittedChainsPermissionIncremental: jest .fn() .mockRejectedValue(mockError), }, @@ -636,7 +616,9 @@ describe('addEthereumChainHandler', () => { mocks, ); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledTimes(1); expect(mockEnd).toHaveBeenCalledWith(mockError); expect(mocks.setActiveNetwork).not.toHaveBeenCalled(); }); @@ -644,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() @@ -686,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 89415d471468..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,40 +155,34 @@ export function validateAddEthereumChainParams(params, end) { export async function switchChain( res, end, - origin, chainId, - requestData, networkClientId, approvalFlowId, { - getChainPermissionsFeatureFlag, + isAddFlow, 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) - ) { + 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.js b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js deleted file mode 100644 index 70dbb7b16cfa..000000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.js +++ /dev/null @@ -1,49 +0,0 @@ -import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; - -/** - * This RPC method gets background state relevant to the provider. - * The background sends RPC notifications on state changes, but the provider - * first requests state on initialization. - */ - -const getProviderState = { - methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE], - implementation: getProviderStateHandler, - hookNames: { - getProviderState: true, - }, -}; -export default getProviderState; - -/** - * @typedef {object} ProviderStateHandlerResult - * @property {string} chainId - The current chain ID. - * @property {boolean} isUnlocked - Whether the extension is unlocked or not. - * @property {string} networkVersion - The current network ID. - */ - -/** - * @typedef {object} ProviderStateHandlerOptions - * @property {() => ProviderStateHandlerResult} getProviderState - A function that - * gets the current provider state. - */ - -/** - * @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 {ProviderStateHandlerOptions} options - */ -async function getProviderStateHandler( - req, - res, - _next, - end, - { getProviderState: _getProviderState }, -) { - res.result = { - ...(await _getProviderState(req.origin)), - }; - return end(); -} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts new file mode 100644 index 000000000000..f3d76a09f5fc --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.test.ts @@ -0,0 +1,56 @@ +import { PendingJsonRpcResponse } from '@metamask/utils'; +import { JsonRpcEngineEndCallback } from 'json-rpc-engine'; +import getProviderState, { + GetProviderState, + ProviderStateHandlerResult, +} from './get-provider-state'; +import { HandlerRequestType } from './types'; + +describe('getProviderState', () => { + let mockEnd: JsonRpcEngineEndCallback; + let mockGetProviderState: GetProviderState; + + beforeEach(() => { + mockEnd = jest.fn(); + mockGetProviderState = jest.fn().mockResolvedValue({ + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }); + }); + + it('should call getProviderState when the handler is invoked', async () => { + const req: HandlerRequestType = { + origin: 'testOrigin', + params: [], + id: '22', + jsonrpc: '2.0', + method: 'metamask_getProviderState', + }; + + const res: PendingJsonRpcResponse = { + id: '22', + jsonrpc: '2.0', + result: { + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }, + }; + + await getProviderState.implementation(req, res, jest.fn(), mockEnd, { + getProviderState: mockGetProviderState, + }); + + expect(mockGetProviderState).toHaveBeenCalledWith(req.origin); + expect(res.result).toStrictEqual({ + chainId: '0x539', + isUnlocked: true, + networkVersion: '', + accounts: [], + }); + expect(mockEnd).toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 000000000000..c95b66e1a20d --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/get-provider-state.ts @@ -0,0 +1,80 @@ +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from 'json-rpc-engine'; +import type { + PendingJsonRpcResponse, + JsonRpcParams, + Hex, +} from '@metamask/utils'; +import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; +import { + HandlerWrapper, + HandlerRequestType as ProviderStateHandlerRequest, +} from './types'; + +/** + * @property chainId - The current chain ID. + * @property isUnlocked - Whether the extension is unlocked or not. + * @property networkVersion - The current network ID. + * @property accounts - List of permitted accounts for the specified origin. + */ +export type ProviderStateHandlerResult = { + chainId: Hex; + isUnlocked: boolean; + networkVersion: string; + accounts: string[]; +}; + +export type GetProviderState = ( + origin: string, +) => Promise; + +type GetProviderStateConstraint = + { + implementation: ( + _req: ProviderStateHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { _getProviderState }: Record, + ) => Promise; + } & HandlerWrapper; + +/** + * This RPC method gets background state relevant to the provider. + * The background sends RPC notifications on state changes, but the provider + * first requests state on initialization. + */ +const getProviderState = { + methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE], + implementation: getProviderStateHandler, + hookNames: { + getProviderState: true, + }, +} satisfies GetProviderStateConstraint; + +export default getProviderState; + +/** + * @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.getProviderState - An async function that gets the current provider state. + */ +async function getProviderStateHandler< + Params extends JsonRpcParams = JsonRpcParams, +>( + req: ProviderStateHandlerRequest, + res: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + { getProviderState: _getProviderState }: Record, +): Promise { + res.result = { + ...(await _getProviderState(req.origin)), + }; + return end(); +} 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 5b7a2a7494d4..91fa9c0dd1cc 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/types.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/types.ts @@ -1,6 +1,12 @@ +import { JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; import { MessageType } from '../../../../../shared/constants/app'; export type HandlerWrapper = { methodNames: [MessageType] | MessageType[]; hookNames: Record; }; + +export type HandlerRequestType = + Required> & { + origin: string; + }; diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 55d04bfb0754..d440578144cc 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -115,8 +115,19 @@ function getTracesSampleRate(sentryTarget) { const flags = getManifestFlags(); + // Grab the tracesSampleRate that may have come in from a git message + // 0 is a valid value, so must explicitly check for undefined + if (flags.sentry?.tracesSampleRate !== undefined) { + return flags.sentry.tracesSampleRate; + } + if (flags.circleci) { - return 0.003; + // Report very frequently on develop branch, and never on other branches + // (Unless you use a `flags = {"sentry": {"tracesSampleRate": x.xx}}` override) + if (flags.circleci.branch === 'develop') { + return 0.03; + } + return 0; } if (METAMASK_DEBUG) { @@ -227,7 +238,7 @@ function getSentryEnvironment() { function getSentryTarget() { if ( - getManifestFlags().doNotForceSentryForThisTest || + !getManifestFlags().sentry?.forceEnable || (process.env.IN_TEST && !SENTRY_DSN_DEV) ) { return SENTRY_DSN_FAKE; @@ -261,7 +272,7 @@ async function getMetaMetricsEnabled() { if ( METAMASK_BUILD_TYPE === 'mmi' || - (flags.circleci && !flags.doNotForceSentryForThisTest) + (flags.circleci && flags.sentry.forceEnable) ) { return true; } @@ -415,7 +426,6 @@ export function rewriteReport(report) { } report.extra.appState = appState; - if (browser.runtime && browser.runtime.id) { report.extra.extensionId = browser.runtime.id; } diff --git a/app/scripts/lib/signature/util.ts b/app/scripts/lib/signature/util.ts new file mode 100644 index 000000000000..b74a35a55998 --- /dev/null +++ b/app/scripts/lib/signature/util.ts @@ -0,0 +1,64 @@ +import type { SignatureController } from '@metamask/signature-controller'; +import type { + OriginalRequest, + TypedMessageParams, +} from '@metamask/message-manager'; +import { endTrace, TraceName } from '../../../../shared/lib/trace'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; + +export type SignatureParams = [TypedMessageParams, OriginalRequest]; + +export type MessageType = keyof typeof MESSAGE_TYPE; + +export type AddSignatureMessageRequest = { + signatureParams: SignatureParams; + signatureController: SignatureController; +}; + +async function handleSignature( + signatureParams: SignatureParams, + signatureController: SignatureController, + functionName: keyof SignatureController, +) { + const [, signatureRequest] = signatureParams; + const { id } = signatureRequest; + const actionId = id?.toString(); + + endTrace({ name: TraceName.Middleware, id: actionId }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: Expected 4-5 arguments, but got 2. + const hash = await signatureController[functionName](...signatureParams); + + endTrace({ name: TraceName.Signature, id: actionId }); + + return hash; +} + +export async function addTypedMessage({ + signatureParams, + signatureController, +}: { + signatureParams: SignatureParams; + signatureController: SignatureController; +}) { + return handleSignature( + signatureParams, + signatureController, + 'newUnsignedTypedMessage', + ); +} + +export async function addPersonalMessage({ + signatureParams, + signatureController, +}: { + signatureParams: SignatureParams; + signatureController: SignatureController; +}) { + return handleSignature( + signatureParams, + signatureController, + 'newUnsignedPersonalMessage', + ); +} diff --git a/app/scripts/lib/signature/utils.test.ts b/app/scripts/lib/signature/utils.test.ts new file mode 100644 index 000000000000..d0aecb68d2ca --- /dev/null +++ b/app/scripts/lib/signature/utils.test.ts @@ -0,0 +1,67 @@ +import type { SignatureController } from '@metamask/signature-controller'; +import type { + OriginalRequest, + TypedMessageParams, +} from '@metamask/message-manager'; +import { endTrace, TraceName } from '../../../../shared/lib/trace'; +import { addTypedMessage } from './util'; +import type { AddSignatureMessageRequest, SignatureParams } from './util'; + +jest.mock('../../../../shared/lib/trace', () => ({ + ...jest.requireActual('../../../../shared/lib/trace'), + endTrace: jest.fn(), +})); + +describe('addSignatureMessage', () => { + const idMock = 1234; + const hashMock = 'hash-mock'; + const messageParamsMock = { + from: '0x12345', + } as TypedMessageParams; + + const originalRequestMock = { + id: idMock, + } as OriginalRequest; + + const signatureParamsMock: SignatureParams = [ + messageParamsMock, + originalRequestMock, + ]; + const signatureControllerMock: SignatureController = { + newUnsignedTypedMessage: jest.fn(() => hashMock), + newUnsignedPersonalMessage: jest.fn(() => hashMock), + } as unknown as SignatureController; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a hash when called with valid parameters', async () => { + const request: AddSignatureMessageRequest = { + signatureParams: signatureParamsMock, + signatureController: signatureControllerMock, + }; + + const result = await addTypedMessage(request); + expect(result).toBe(hashMock); + }); + + it('should call endTrace with correct parameters', async () => { + const request: AddSignatureMessageRequest = { + signatureParams: signatureParamsMock, + signatureController: signatureControllerMock, + }; + + await addTypedMessage(request); + + expect(endTrace).toHaveBeenCalledTimes(2); + expect(endTrace).toHaveBeenCalledWith({ + name: TraceName.Middleware, + id: idMock.toString(), + }); + expect(endTrace).toHaveBeenCalledWith({ + name: TraceName.Signature, + id: idMock.toString(), + }); + }); +}); 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 1e7bdf4a7cf0..96a081e3308d 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) @@ -274,7 +276,7 @@ import MMIController from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; -import AccountTracker from './lib/account-tracker'; +import AccountTrackerController from './controllers/account-tracker-controller'; import createDupeReqFilterStream from './lib/createDupeReqFilterStream'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; import { @@ -312,10 +314,11 @@ import { CaveatFactories, CaveatMutatorFactories, getCaveatSpecifications, - getChangedAccounts, + diffMap, getPermissionBackgroundApiMethods, getPermissionSpecifications, getPermittedAccountsByOrigin, + getPermittedChainsByOrigin, NOTIFICATION_NAMES, PermissionNames, unrestrictedMethods, @@ -332,6 +335,9 @@ import { snapKeyringBuilder, getAccountsBySnapId } from './lib/snap-keyring'; ///: END:ONLY_INCLUDE_IF import { encryptorFactory } from './lib/encryptor-factory'; import { addDappTransaction, addTransaction } from './lib/transaction/util'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { addTypedMessage, addPersonalMessage } from './lib/signature/util'; +///: END:ONLY_INCLUDE_IF import { LatticeKeyringOffscreen } from './lib/offscreen-bridge/lattice-offscreen-keyring'; import PREINSTALLED_SNAPS from './snaps/preinstalled-snaps'; import { WeakRefObjectMap } from './lib/WeakRefObjectMap'; @@ -343,7 +349,10 @@ import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import { BridgeBackgroundAction } from './controllers/bridge/types'; +import { + BridgeUserAction, + BridgeBackgroundAction, +} from './controllers/bridge/types'; import BridgeController from './controllers/bridge/bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { @@ -1218,7 +1227,7 @@ export default class MetamaskController extends EventEmitter { const internalAccountCount = internalAccounts.length; const accountTrackerCount = Object.keys( - this.accountTracker.store.getState().accounts || {}, + this.accountTrackerController.state.accounts || {}, ).length; captureException( @@ -1651,11 +1660,24 @@ export default class MetamaskController extends EventEmitter { }); // account tracker watches balances, nonces, and any code at their address - this.accountTracker = new AccountTracker({ + this.accountTrackerController = new AccountTrackerController({ + state: { accounts: {} }, + messenger: this.controllerMessenger.getRestricted({ + name: 'AccountTrackerController', + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'OnboardingController:getState', + ], + allowedEvents: [ + 'AccountsController:selectedEvmAccountChange', + 'OnboardingController:stateChange', + 'KeyringController:accountRemoved', + ], + }), provider: this.provider, blockTracker: this.blockTracker, - getCurrentChainId: () => - getCurrentChainId({ metamask: this.networkController.state }), getNetworkIdentifier: (providerConfig) => { const { type, rpcUrl } = providerConfig ?? @@ -1665,20 +1687,6 @@ export default class MetamaskController extends EventEmitter { return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, preferencesController: this.preferencesController, - onboardingController: this.onboardingController, - controllerMessenger: this.controllerMessenger.getRestricted({ - name: 'AccountTracker', - allowedEvents: [ - 'AccountsController:selectedEvmAccountChange', - 'OnboardingController:stateChange', - ], - allowedActions: ['AccountsController:getSelectedAccount'], - }), - initState: { accounts: {} }, - onAccountRemoved: this.controllerMessenger.subscribe.bind( - this.controllerMessenger, - 'KeyringController:accountRemoved', - ), }); // start and stop polling for balances based on activeControllerConnections @@ -1848,6 +1856,10 @@ export default class MetamaskController extends EventEmitter { getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { + etherscanApiKeysByChainId: { + [CHAIN_IDS.MAINNET]: process.env.ETHERSCAN_API_KEY, + [CHAIN_IDS.SEPOLIA]: process.env.ETHERSCAN_API_KEY, + }, includeTokenTransfers: false, isEnabled: () => Boolean( @@ -1956,6 +1968,7 @@ export default class MetamaskController extends EventEmitter { getAllState: this.getState.bind(this), getCurrentChainId: () => getCurrentChainId({ metamask: this.networkController.state }), + trace, }); this.signatureController.hub.on( @@ -1996,7 +2009,7 @@ export default class MetamaskController extends EventEmitter { custodyController: this.custodyController, getState: this.getState.bind(this), getPendingNonce: this.getPendingNonce.bind(this), - accountTracker: this.accountTracker, + accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, networkController: this.networkController, permissionController: this.permissionController, @@ -2205,11 +2218,11 @@ export default class MetamaskController extends EventEmitter { this._onUserOperationTransactionUpdated.bind(this), ); - // ensure accountTracker updates balances after network change + // ensure AccountTrackerController updates balances after network change networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', () => { - this.accountTracker.updateAccounts(); + this.accountTrackerController.updateAccounts(); }, ); @@ -2248,22 +2261,27 @@ export default class MetamaskController extends EventEmitter { ), // msg signing ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - processTypedMessage: - this.signatureController.newUnsignedTypedMessage.bind( - this.signatureController, - ), - processTypedMessageV3: - this.signatureController.newUnsignedTypedMessage.bind( - this.signatureController, - ), - processTypedMessageV4: - this.signatureController.newUnsignedTypedMessage.bind( - this.signatureController, - ), - processPersonalMessage: - this.signatureController.newUnsignedPersonalMessage.bind( - this.signatureController, - ), + + processTypedMessage: (...args) => + addTypedMessage({ + signatureController: this.signatureController, + signatureParams: args, + }), + processTypedMessageV3: (...args) => + addTypedMessage({ + signatureController: this.signatureController, + signatureParams: args, + }), + processTypedMessageV4: (...args) => + addTypedMessage({ + signatureController: this.signatureController, + signatureParams: args, + }), + processPersonalMessage: (...args) => + addPersonalMessage({ + signatureController: this.signatureController, + signatureParams: args, + }), ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -2316,7 +2334,7 @@ export default class MetamaskController extends EventEmitter { * On chrome profile re-start, they will be re-initialized. */ const resetOnRestartStore = { - AccountTracker: this.accountTracker.store, + AccountTracker: this.accountTrackerController, TokenRatesController: this.tokenRatesController, DecryptMessageController: this.decryptMessageController, EncryptionPublicKeyController: this.encryptionPublicKeyController, @@ -2441,7 +2459,9 @@ export default class MetamaskController extends EventEmitter { // if this is the first time, clear the state of by calling these methods const resetMethods = [ - this.accountTracker.resetState, + this.accountTrackerController.resetState.bind( + this.accountTrackerController, + ), this.decryptMessageController.resetState.bind( this.decryptMessageController, ), @@ -2541,7 +2561,7 @@ export default class MetamaskController extends EventEmitter { } triggerNetworkrequests() { - this.accountTracker.start(); + this.accountTrackerController.start(); this.txController.startIncomingTransactionPolling(); this.tokenDetectionController.enable(); @@ -2560,7 +2580,7 @@ export default class MetamaskController extends EventEmitter { } stopNetworkRequests() { - this.accountTracker.stop(); + this.accountTrackerController.stop(); this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); @@ -2844,7 +2864,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.subscribe( `${this.permissionController.name}:stateChange`, async (currentValue, previousValue) => { - const changedAccounts = getChangedAccounts(currentValue, previousValue); + const changedAccounts = diffMap(currentValue, previousValue); for (const [origin, accounts] of changedAccounts.entries()) { this._notifyAccountsChange(origin, accounts); @@ -2853,6 +2873,40 @@ export default class MetamaskController extends EventEmitter { getPermittedAccountsByOrigin, ); + this.controllerMessenger.subscribe( + `${this.permissionController.name}:stateChange`, + async (currentValue, previousValue) => { + const changedChains = diffMap(currentValue, previousValue); + + // This operates under the assumption that there will be at maximum + // one origin permittedChains value change per event handler call + for (const [origin, chains] of changedChains.entries()) { + const currentNetworkClientIdForOrigin = + this.selectedNetworkController.getNetworkClientIdForDomain(origin); + const { chainId: currentChainIdForOrigin } = + this.networkController.getNetworkConfigurationByNetworkClientId( + currentNetworkClientIdForOrigin, + ); + // if(chains.length === 0) { + // TODO: This particular case should also occur at the same time + // that eth_accounts is revoked. When eth_accounts is revoked, + // the networkClientId for that origin should be reset to track + // the globally selected network. + // } + if (chains.length > 0 && !chains.includes(currentChainIdForOrigin)) { + const networkClientId = + this.networkController.findNetworkClientIdByChainId(chains[0]); + this.selectedNetworkController.setNetworkClientIdForDomain( + origin, + networkClientId, + ); + this.networkController.setActiveNetwork(networkClientId); + } + } + }, + getPermittedChainsByOrigin, + ); + this.controllerMessenger.subscribe( 'NetworkController:networkDidChange', async () => { @@ -3217,6 +3271,13 @@ export default class MetamaskController extends EventEmitter { getProviderConfig({ metamask: this.networkController.state, }), + grantPermissionsIncremental: + this.permissionController.grantPermissionsIncremental.bind( + this.permissionController, + ), + grantPermissions: this.permissionController.grantPermissions.bind( + this.permissionController, + ), setSecurityAlertsEnabled: preferencesController.setSecurityAlertsEnabled.bind( preferencesController, @@ -3480,6 +3541,8 @@ export default class MetamaskController extends EventEmitter { ), setOnboardingDate: appStateController.setOnboardingDate.bind(appStateController), + setLastViewedUserSurvey: + appStateController.setLastViewedUserSurvey.bind(appStateController), setNewPrivacyPolicyToastClickedOrClosed: appStateController.setNewPrivacyPolicyToastClickedOrClosed.bind( appStateController, @@ -3546,6 +3609,7 @@ export default class MetamaskController extends EventEmitter { createCancelTransaction: this.createCancelTransaction.bind(this), createSpeedUpTransaction: this.createSpeedUpTransaction.bind(this), estimateGas: this.estimateGas.bind(this), + estimateGasFee: txController.estimateGasFee.bind(txController), getNextNonce: this.getNextNonce.bind(this), addTransaction: (transactionParams, transactionOptions) => addTransaction( @@ -3834,6 +3898,15 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, ), + [BridgeUserAction.SELECT_SRC_NETWORK]: this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_SRC_NETWORK}`, + ), + [BridgeUserAction.SELECT_DEST_NETWORK]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.SELECT_DEST_NETWORK}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( @@ -3965,6 +4038,10 @@ export default class MetamaskController extends EventEmitter { userStorageController.syncInternalAccountsWithUserStorage.bind( userStorageController, ), + deleteAccountSyncingDataFromUserStorage: + userStorageController.performDeleteStorageAllFeatureEntries.bind( + userStorageController, + ), // NotificationServicesController checkAccountsPresence: @@ -4062,7 +4139,7 @@ export default class MetamaskController extends EventEmitter { const { tokens } = this.tokensController.state; const staticTokenListDetails = - STATIC_MAINNET_TOKEN_LIST[address.toLowerCase()] || {}; + STATIC_MAINNET_TOKEN_LIST[address?.toLowerCase()] || {}; const tokenListDetails = tokenList[address.toLowerCase()] || {}; const userDefinedTokenDetails = tokens.find(({ address: _address }) => @@ -4218,8 +4295,8 @@ export default class MetamaskController extends EventEmitter { // Clear notification state this.notificationController.clear(); - // clear accounts in accountTracker - this.accountTracker.clearAccounts(); + // clear accounts in AccountTrackerController + this.accountTrackerController.clearAccounts(); this.txController.clearUnapprovedTransactions(); @@ -4316,14 +4393,14 @@ export default class MetamaskController extends EventEmitter { } /** - * Get an account balance from the AccountTracker or request it directly from the network. + * Get an account balance from the AccountTrackerController or request it directly from the network. * * @param {string} address - The account address * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network */ getBalance(address, ethQuery) { return new Promise((resolve, reject) => { - const cached = this.accountTracker.store.getState().accounts[address]; + const cached = this.accountTrackerController.state.accounts[address]; if (cached && cached.balance) { resolve(cached.balance); @@ -4381,9 +4458,9 @@ export default class MetamaskController extends EventEmitter { // Automatic login via config password await this.submitPassword(password); - // Updating accounts in this.accountTracker before starting UI syncing ensure that + // Updating accounts in this.accountTrackerController before starting UI syncing ensure that // state has account balance before it is synced with UI - await this.accountTracker.updateAccountsAllActiveNetworks(); + await this.accountTrackerController.updateAccountsAllActiveNetworks(); } finally { this._startUISync(); } @@ -4560,7 +4637,7 @@ export default class MetamaskController extends EventEmitter { oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), ), ]; - this.accountTracker.syncWithAddresses(accountsToTrack); + this.accountTrackerController.syncWithAddresses(accountsToTrack); return accounts; } @@ -4876,6 +4953,7 @@ export default class MetamaskController extends EventEmitter { transactionParams, transactionOptions, dappRequest, + ...otherParams }) { return { internalAccounts: this.accountsController.listAccounts(), @@ -4895,6 +4973,7 @@ export default class MetamaskController extends EventEmitter { securityAlertsEnabled: this.preferencesController.store.getState()?.securityAlertsEnabled, updateSecurityAlertResponse: this.updateSecurityAlertResponse.bind(this), + ...otherParams, }; } @@ -5667,7 +5746,12 @@ export default class MetamaskController extends EventEmitter { this.permissionController.requestPermissions.bind( this.permissionController, { origin }, - { eth_accounts: {} }, + { + eth_accounts: {}, + ...(!isSnapId(origin) && { + [PermissionNames.permittedChains]: {}, + }), + }, ), requestPermittedChainsPermission: (chainIds) => this.permissionController.requestPermissionsIncremental( @@ -5682,10 +5766,31 @@ export default class MetamaskController extends EventEmitter { }, }, ), - requestPermissionsForOrigin: - this.permissionController.requestPermissions.bind( - this.permissionController, + grantPermittedChainsPermissionIncremental: (chainIds) => + this.permissionController.grantPermissionsIncremental({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.permittedChains]: { + caveats: [ + CaveatFactories[CaveatTypes.restrictNetworkSwitching]( + chainIds, + ), + ], + }, + }, + }), + requestPermissionsForOrigin: (requestedPermissions) => + this.permissionController.requestPermissions( { origin }, + { + ...(requestedPermissions[PermissionNames.eth_accounts] && { + [PermissionNames.permittedChains]: {}, + }), + ...(requestedPermissions[PermissionNames.permittedChains] && { + [PermissionNames.eth_accounts]: {}, + }), + ...requestedPermissions, + }, ), revokePermissionsForOrigin: (permissionKeys) => { try { @@ -5719,8 +5824,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); @@ -6083,7 +6186,7 @@ export default class MetamaskController extends EventEmitter { return; } - this.accountTracker.syncWithAddresses(addresses); + this.accountTrackerController.syncWithAddresses(addresses); } /** diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 4121160a45af..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'; @@ -738,19 +783,23 @@ describe('MetaMaskController', () => { }); describe('#getBalance', () => { - it('should return the balance known by accountTracker', async () => { + it('should return the balance known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; accounts[TEST_ADDRESS] = { balance }; - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance(TEST_ADDRESS); expect(balance).toStrictEqual(gotten); }); - it('should ask the network for a balance when not known by accountTracker', async () => { + it('should ask the network for a balance when not known by accountTrackerController', async () => { const accounts = {}; const balance = '0x14ced5122ce0a000'; const ethQuery = new EthQuery(); @@ -758,7 +807,11 @@ describe('MetaMaskController', () => { callback(undefined, balance); }); - metamaskController.accountTracker.store.putState({ accounts }); + jest + .spyOn(metamaskController.accountTrackerController, 'state', 'get') + .mockReturnValue({ + accounts, + }); const gotten = await metamaskController.getBalance( TEST_ADDRESS, @@ -1687,21 +1740,27 @@ describe('MetaMaskController', () => { it('should do nothing if there are no keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); await metamaskController._onKeyringControllerUpdate({ keyrings: [] }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).not.toHaveBeenCalled(); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should sync addresses if there are keyrings in state', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1714,14 +1773,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('should NOT update selected address if already unlocked', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1735,14 +1797,17 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); it('filter out non-EVM addresses prior to calling syncWithAddresses', async () => { jest - .spyOn(metamaskController.accountTracker, 'syncWithAddresses') + .spyOn( + metamaskController.accountTrackerController, + 'syncWithAddresses', + ) .mockReturnValue(); const oldState = metamaskController.getState(); @@ -1759,7 +1824,7 @@ describe('MetaMaskController', () => { }); expect( - metamaskController.accountTracker.syncWithAddresses, + metamaskController.accountTrackerController.syncWithAddresses, ).toHaveBeenCalledWith(accounts); expect(metamaskController.getState()).toStrictEqual(oldState); }); 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/128.test.ts b/app/scripts/migrations/128.test.ts new file mode 100644 index 000000000000..f2658bfc6bd9 --- /dev/null +++ b/app/scripts/migrations/128.test.ts @@ -0,0 +1,39 @@ +import { migrate, version } from './128'; + +const oldVersion = 127; + +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('Removes useNativeCurrencyAsPrimaryCurrency from the PreferencesController.preferences state', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + delete ( + oldState.PreferencesController.preferences as { + useNativeCurrencyAsPrimaryCurrency?: boolean; + } + ).useNativeCurrencyAsPrimaryCurrency; + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/128.ts b/app/scripts/migrations/128.ts new file mode 100644 index 000000000000..89f14606af7f --- /dev/null +++ b/app/scripts/migrations/128.ts @@ -0,0 +1,42 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 128; + +/** + * This migration removes `useNativeCurrencyAsPrimaryCurrency` from preferences in PreferencesController. + * + * @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) && + isObject(state.PreferencesController.preferences) + ) { + delete state.PreferencesController.preferences + .useNativeCurrencyAsPrimaryCurrency; + } + + return state; +} diff --git a/app/scripts/migrations/129.test.ts b/app/scripts/migrations/129.test.ts new file mode 100644 index 000000000000..740add0e7e4e --- /dev/null +++ b/app/scripts/migrations/129.test.ts @@ -0,0 +1,60 @@ +import { migrate, version } from './129'; + +const oldVersion = 128; + +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('Adds shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its undefined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual({ + ...oldState, + PreferencesController: { + ...oldState.PreferencesController, + preferences: { + ...oldState.PreferencesController.preferences, + shouldShowAggregatedBalancePopover: true, + }, + }, + }); + }); + + it('Does not add shouldShowAggregatedBalancePopover to the PreferencesController.preferences state when its defined', async () => { + const oldState = { + PreferencesController: { + preferences: { + hideZeroBalanceTokens: false, + showTestNetworks: true, + shouldShowAggregatedBalancePopover: false, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/129.ts b/app/scripts/migrations/129.ts new file mode 100644 index 000000000000..b4323798a006 --- /dev/null +++ b/app/scripts/migrations/129.ts @@ -0,0 +1,47 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 129; + +/** + * This migration adds `shouldShowAggregatedBalancePopover` to preferences in PreferencesController and set it to true when its undefined. + * + * @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) && + isObject(state.PreferencesController.preferences) + ) { + if ( + state.PreferencesController.preferences + .shouldShowAggregatedBalancePopover === undefined + ) { + state.PreferencesController.preferences.shouldShowAggregatedBalancePopover = + true; + } + } + + 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 80407ecf232e..a72fd34c3c28 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -146,7 +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/builds.yml b/builds.yml index acee49063822..a69bf611a322 100644 --- a/builds.yml +++ b/builds.yml @@ -137,6 +137,7 @@ features: # env object supports both declarations (- FOO), and definitions (- FOO: BAR). # Variables that were declared have to be defined somewhere in the load chain before usage env: + - ACCOUNTS_USE_DEV_APIS: false - BRIDGE_USE_DEV_APIS: false - SWAPS_USE_DEV_APIS: false - PORTFOLIO_URL: https://portfolio.metamask.io @@ -272,8 +273,10 @@ env: - SECURITY_ALERTS_API_ENABLED: '' # URL of security alerts API used to validate dApp requests - SECURITY_ALERTS_API_URL: 'http://localhost:3000' + # API key to authenticate Etherscan requests to avoid rate limiting + - ETHERSCAN_API_KEY: '' - # Enables the notifications feature within the build: + # Enables the notifications feature within the build: - NOTIFICATIONS: '' - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io @@ -290,6 +293,7 @@ env: ### - EIP_4337_ENTRYPOINT: null + ### # Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render # This should NEVER be enabled in production since it slows down react diff --git a/coverage.json b/coverage.json new file mode 100644 index 000000000000..9887e06e2db6 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{ "coverage": 71 } diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index d5063250db16..7ffbd68472d1 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -63,7 +63,6 @@ "app/scripts/inpage.js", "app/scripts/lib/ComposableObservableStore.js", "app/scripts/lib/ComposableObservableStore.test.js", - "app/scripts/lib/account-tracker.js", "app/scripts/lib/cleanErrorStack.js", "app/scripts/lib/cleanErrorStack.test.js", "app/scripts/lib/createLoggerMiddleware.js", @@ -1416,9 +1415,6 @@ "ui/pages/settings/advanced-tab/advanced-tab.container.js", "ui/pages/settings/advanced-tab/advanced-tab.stories.js", "ui/pages/settings/advanced-tab/index.js", - "ui/pages/settings/alerts-tab/alerts-tab.js", - "ui/pages/settings/alerts-tab/alerts-tab.test.js", - "ui/pages/settings/alerts-tab/index.js", "ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js", "ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js", "ui/pages/settings/contact-list-tab/add-contact/index.js", diff --git a/jest.config.js b/jest.config.js index be304e027ace..f1d38ab4aea3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,10 @@ module.exports = { collectCoverageFrom: [ - '/app/**/*.(js|ts|tsx)', - '/development/**/*.(js|ts|tsx)', - '/offscreen/**/*.(js|ts|tsx)', + '/app/scripts/**/*.(js|ts|tsx)', '/shared/**/*.(js|ts|tsx)', - '/test/**/*.(js|ts|tsx)', - '/types/**/*.(js|ts|tsx)', '/ui/**/*.(js|ts|tsx)', + '/development/build/transforms/**/*.js', + '/test/unit-global/**/*.test.(js|ts|tsx)', ], coverageDirectory: './coverage/unit', coveragePathIgnorePatterns: ['.stories.*', '.snap'], @@ -24,7 +22,11 @@ module.exports = { // TODO: enable resetMocks // resetMocks: true, restoreMocks: true, - setupFiles: ['/test/setup.js', '/test/env.js'], + setupFiles: [ + 'jest-canvas-mock', + '/test/setup.js', + '/test/env.js', + ], setupFilesAfterEnv: ['/test/jest/setup.js'], testMatch: [ '/app/scripts/**/*.test.(js|ts|tsx)', diff --git a/jest.integration.config.js b/jest.integration.config.js index e6635bd5b695..6f5d79484386 100644 --- a/jest.integration.config.js +++ b/jest.integration.config.js @@ -18,6 +18,7 @@ module.exports = { ], restoreMocks: true, setupFiles: [ + 'jest-canvas-mock', '/test/integration/config/setup.js', '/test/integration/config/env.js', ], diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index bf47338c68a1..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 } @@ -4511,6 +4503,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4601,6 +4601,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5088,37 +5115,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5135,13 +5184,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index bf47338c68a1..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 } @@ -4511,6 +4503,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4601,6 +4601,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5088,37 +5115,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5135,13 +5184,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index bf47338c68a1..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 } @@ -4511,6 +4503,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4601,6 +4601,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5088,37 +5115,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5135,13 +5184,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 54d61c2e245c..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 } @@ -4603,6 +4595,14 @@ "define": true } }, + "history": { + "globals": { + "console": true, + "define": true, + "document.defaultView": true, + "document.querySelector": true + } + }, "https-browserify": { "packages": { "browserify>url": true, @@ -4693,6 +4693,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true @@ -5156,37 +5183,59 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>history": { + "react-router-dom-v5-compat": { "globals": { + "FormData": true, + "URL": true, + "URLSearchParams": true, + "__reactRouterVersion": "write", "addEventListener": true, "confirm": true, + "define": true, "document": true, - "history": true, - "location": true, - "navigator.userAgent": true, - "removeEventListener": true + "history.scrollRestoration": true, + "location.href": true, + "removeEventListener": true, + "scrollTo": true, + "scrollY": true, + "sessionStorage.getItem": true, + "sessionStorage.setItem": true, + "setTimeout": true }, "packages": { - "react-router-dom>history>resolve-pathname": true, - "react-router-dom>history>value-equal": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true + "history": true, + "react": true, + "react-dom": true, + "react-router-dom": true, + "react-router-dom-v5-compat>@remix-run/router": true, + "react-router-dom-v5-compat>react-router": true } }, - "react-router-dom>react-router": { + "react-router-dom-v5-compat>@remix-run/router": { + "globals": { + "AbortController": true, + "DOMException": true, + "FormData": true, + "Headers": true, + "Request": true, + "Response": true, + "URL": true, + "URLSearchParams": true, + "console": true, + "document.defaultView": true + } + }, + "react-router-dom-v5-compat>react-router": { + "globals": { + "console.error": true, + "define": true + }, "packages": { - "prop-types": true, - "prop-types>react-is": true, "react": true, - "react-redux>hoist-non-react-statics": true, - "react-router-dom>react-router>history": true, - "react-router-dom>react-router>mini-create-react-context": true, - "react-router-dom>tiny-invariant": true, - "react-router-dom>tiny-warning": true, - "serve-handler>path-to-regexp": true + "react-router-dom-v5-compat>@remix-run/router": true } }, - "react-router-dom>react-router>history": { + "react-router-dom>history": { "globals": { "addEventListener": true, "confirm": true, @@ -5203,13 +5252,16 @@ "react-router-dom>tiny-warning": true } }, - "react-router-dom>react-router>mini-create-react-context": { + "react-router-dom>react-router": { "packages": { - "@babel/runtime": true, "prop-types": true, + "prop-types>react-is": true, "react": true, - "react-router-dom>react-router>mini-create-react-context>gud": true, - "react-router-dom>tiny-warning": true + "react-redux>hoist-non-react-statics": true, + "react-router-dom>history": true, + "react-router-dom>tiny-invariant": true, + "react-router-dom>tiny-warning": true, + "serve-handler>path-to-regexp": true } }, "react-router-dom>tiny-warning": { diff --git a/package.json b/package.json index bde7fa9853b8..f9488dc992e6 100644 --- a/package.json +++ b/package.json @@ -297,14 +297,14 @@ "@metamask-institutional/transaction-update": "^0.2.5", "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", - "@metamask/account-watcher": "^4.1.0", - "@metamask/accounts-controller": "^18.2.1", + "@metamask/account-watcher": "^4.1.1", + "@metamask/accounts-controller": "^18.2.2", "@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.0", + "@metamask/bitcoin-wallet-snap": "^0.6.1", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", @@ -316,17 +316,17 @@ "@metamask/eth-ledger-bridge-keyring": "^3.0.1", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^7.0.1", - "@metamask/eth-snap-keyring": "^4.3.3", + "@metamask/eth-snap-keyring": "^4.3.6", "@metamask/eth-token-tracker": "^8.0.0", - "@metamask/eth-trezor-keyring": "^3.1.0", + "@metamask/eth-trezor-keyring": "^3.1.3", "@metamask/etherscan-link": "^3.0.0", "@metamask/ethjs": "^0.6.0", "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", "@metamask/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", - "@metamask/keyring-api": "^8.1.0", - "@metamask/keyring-controller": "^17.2.1", + "@metamask/keyring-api": "^8.1.3", + "@metamask/keyring-controller": "^17.2.2", "@metamask/logging-controller": "^6.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^10.1.0", @@ -352,16 +352,16 @@ "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^18.0.1", - "@metamask/signature-controller": "^19.0.0", + "@metamask/signature-controller": "^19.1.0", "@metamask/smart-transactions-controller": "^13.0.0", "@metamask/snaps-controllers": "^9.7.0", "@metamask/snaps-execution-environments": "^6.7.2", "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", "@metamask/snaps-utils": "^8.1.1", - "@metamask/transaction-controller": "^37.0.0", + "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", - "@metamask/utils": "^9.1.0", + "@metamask/utils": "^9.3.0", "@ngraveio/bc-ur": "^1.1.12", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.4.0", @@ -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", @@ -397,6 +398,7 @@ "fast-json-patch": "^3.1.1", "fuse.js": "^3.2.0", "he": "^1.2.0", + "history": "^5.3.0", "human-standard-token-abi": "^2.0.0", "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", @@ -407,6 +409,7 @@ "localforage": "^1.9.0", "lodash": "^4.17.21", "loglevel": "^1.8.1", + "lottie-web": "^5.12.2", "luxon": "^3.2.1", "nanoid": "^2.1.6", "pify": "^5.0.0", @@ -426,7 +429,8 @@ "react-popper": "^2.2.3", "react-redux": "^7.2.9", "react-responsive-carousel": "^3.2.21", - "react-router-dom": "^5.1.2", + "react-router-dom": "^5.3.4", + "react-router-dom-v5-compat": "^6.26.2", "react-simple-file-input": "^2.0.0", "react-tippy": "^1.2.2", "react-toggle-button": "^2.2.0", @@ -603,7 +607,6 @@ "gulp-stylelint": "^13.0.0", "gulp-watch": "^5.0.1", "gulp-zip": "^5.1.0", - "history": "^5.0.0", "html-bundler-webpack-plugin": "^3.17.3", "https-browserify": "^1.0.0", "husky": "^8.0.3", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 48c96582cab0..2516654f1803 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,4 +1,5 @@ [ + "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", "api.lens.dev", @@ -6,6 +7,7 @@ "api.web3modal.com", "app.ens.domains", "arbitrum-mainnet.infura.io", + "authentication.api.cx.metamask.io", "bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link", "bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link", "bridge.api.cx.metamask.io", @@ -13,6 +15,7 @@ "cdn.segment.io", "cdnjs.cloudflare.com", "chainid.network", + "client-side-detection.api.cx.metamask.io", "configuration.dev.metamask-institutional.io", "configuration.metamask-institutional.io", "connect.trezor.io", @@ -24,19 +27,23 @@ "gas.api.cx.metamask.io", "github.com", "goerli.infura.io", + "lattice.gridplus.io", "localhost:8000", "localhost:8545", "mainnet.infura.io", "metamask.eth", "metamask.github.io", + "metametrics.metamask.test", "min-api.cryptocompare.com", "nft.api.cx.metamask.io", + "oidc.api.cx.metamask.io", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "phishing-detection.api.cx.metamask.io", "portfolio.metamask.io", "price.api.cx.metamask.io", - "on-ramp-content.api.cx.metamask.io", - "on-ramp-content.uat-api.cx.metamask.io", "proxy.api.cx.metamask.io", + "proxy.dev-api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", "responsive-rpc.test", @@ -50,14 +57,8 @@ "token.api.cx.metamask.io", "tokens.api.cx.metamask.io", "tx-sentinel-ethereum-mainnet.api.cx.metamask.io", - "unresponsive-rpc.url", - "www.4byte.directory", - "lattice.gridplus.io", "unresponsive-rpc.test", - "authentication.api.cx.metamask.io", - "oidc.api.cx.metamask.io", - "price.api.cx.metamask.io", - "token.api.cx.metamask.io", - "client-side-detection.api.cx.metamask.io", - "user-storage.api.cx.metamask.io" + "unresponsive-rpc.url", + "user-storage.api.cx.metamask.io", + "www.4byte.directory" ] diff --git a/shared/constants/accounts.ts b/shared/constants/accounts.ts new file mode 100644 index 000000000000..1c63c19e1442 --- /dev/null +++ b/shared/constants/accounts.ts @@ -0,0 +1,6 @@ +export const ACCOUNTS_DEV_API_BASE_URL = + 'https://accounts.dev-api.cx.metamask.io'; +export const ACCOUNTS_PROD_API_BASE_URL = 'https://accounts.api.cx.metamask.io'; +export const ACCOUNTS_API_BASE_URL = process.env.ACCOUNTS_USE_DEV_APIS + ? ACCOUNTS_DEV_API_BASE_URL + : ACCOUNTS_PROD_API_BASE_URL; diff --git a/shared/constants/app.ts b/shared/constants/app.ts index f3717b7cece9..12b340d35fa5 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -124,3 +124,11 @@ export const FIREFOX_BUILD_IDS = [ ] as const; export const UNKNOWN_TICKER_SYMBOL = 'UNKNOWN'; + +export const TRACE_ENABLED_SIGN_METHODS = [ + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V1, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, + MESSAGE_TYPE.PERSONAL_SIGN, +]; diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 9994599e73cf..b3e6f252d23d 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -446,9 +446,9 @@ export enum MetaMetricsUserTrait { */ TokenDetectionEnabled = 'token_detection_enabled', /** - * Identified when the user enables native currency. + * Identified when show native token as main balance is toggled. */ - UseNativeCurrencyAsPrimaryCurrency = 'use_native_currency_as_primary_currency', + ShowNativeTokenAsMainBalance = 'show_native_token_as_main_balance', /** * Identified when the security provider feature is enabled. */ @@ -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', } /** @@ -536,6 +540,7 @@ export enum MetaMetricsEventName { EncryptionPublicKeyApproved = 'Encryption Approved', EncryptionPublicKeyRejected = 'Encryption Rejected', EncryptionPublicKeyRequested = 'Encryption Requested', + ErrorOccured = 'Error occured', ExternalLinkClicked = 'External Link Clicked', KeyExportSelected = 'Key Export Selected', KeyExportRequested = 'Key Export Requested', @@ -552,6 +557,7 @@ export enum MetaMetricsEventName { MarkAllNotificationsRead = 'Notifications Marked All as Read', MetricsOptIn = 'Metrics Opt In', MetricsOptOut = 'Metrics Opt Out', + MetricsDataDeletionRequest = 'Delete MetaMetrics Data Request Submitted', NavAccountMenuOpened = 'Account Menu Opened', NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', @@ -580,6 +586,7 @@ export enum MetaMetricsEventName { OnboardingWalletImportAttempted = 'Wallet Import Attempted', OnboardingWalletVideoPlay = 'SRP Intro Video Played', OnboardingTwitterClick = 'External Link Clicked', + OnboardingWalletSetupComplete = 'Wallet Setup Complete', OnrampProviderSelected = 'On-ramp Provider Selected', PermissionsApproved = 'Permissions Approved', PermissionsRejected = 'Permissions Rejected', @@ -620,6 +627,7 @@ export enum MetaMetricsEventName { SrpCopiedToClipboard = 'Copies SRP to clipboard', SrpToConfirmBackup = 'SRP Backup Confirm Displayed', StakingEntryPointClicked = 'Stake Button Clicked', + SurveyToast = 'Survey Toast', SupportLinkClicked = 'Support Link Clicked', TermsOfUseShown = 'Terms of Use Shown', TermsOfUseAccepted = 'Terms of Use Accepted', @@ -627,12 +635,13 @@ 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', TokenImportCanceled = 'Token Import Canceled', TokenImportClicked = 'Token Import Clicked', - UseNativeCurrencyAsPrimaryCurrency = 'Use Native Currency as Primary Currency', + ShowNativeTokenAsMainBalance = 'Show native token as main balance', WalletSetupStarted = 'Wallet Setup Selected', WalletSetupCanceled = 'Wallet Setup Canceled', WalletSetupFailed = 'Wallet Setup Failed', @@ -779,6 +788,7 @@ export enum MetaMetricsEventCategory { Retention = 'Retention', Send = 'Send', Settings = 'Settings', + Feedback = 'Feedback', Snaps = 'Snaps', Swaps = 'Swaps', Tokens = 'Tokens', diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 50cc26ef5541..e61d7ed807cd 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -12,7 +12,7 @@ import PreferencesController from '../../app/scripts/controllers/preferences-con import { AppStateController } from '../../app/scripts/controllers/app-state'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths -import AccountTracker from '../../app/scripts/lib/account-tracker'; +import AccountTrackerController from '../../app/scripts/controllers/account-tracker-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; @@ -35,7 +35,7 @@ export type MMIControllerOptions = { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getPendingNonce: (address: string) => Promise; - accountTracker: AccountTracker; + accountTrackerController: AccountTrackerController; metaMetricsController: MetaMetricsController; networkController: NetworkController; // TODO: Replace `any` with type diff --git a/shared/constants/permissions.ts b/shared/constants/permissions.ts index 0829d772e854..efcf5bd46872 100644 --- a/shared/constants/permissions.ts +++ b/shared/constants/permissions.ts @@ -3,6 +3,10 @@ export const CaveatTypes = Object.freeze({ restrictNetworkSwitching: 'restrictNetworkSwitching' as const, }); +export const EndowmentTypes = Object.freeze({ + permittedChains: 'endowment:permitted-chains', +}); + export const RestrictedEthMethods = Object.freeze({ eth_accounts: 'eth_accounts', }); 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..4c1bab12d03b 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -1,49 +1,95 @@ -import { isBtcMainnetAddress, isBtcTestnetAddress } from './multichain'; +import { KnownCaipNamespace } from '@metamask/utils'; +import { + getCaipNamespaceFromAddress, + 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)( - 'returns true if address is compatible with BTC mainnet: %s', - (address: string) => { - expect(isBtcMainnetAddress(address)).toBe(true); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each([...TESTNET_ADDRESSES, ...ETH_ADDRESSES])( - 'returns false if address is not compatible with BTC mainnet: %s', - (address: string) => { - expect(isBtcMainnetAddress(address)).toBe(false); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each(TESTNET_ADDRESSES)( - 'returns true if address is compatible with BTC testnet: %s', - (address: string) => { - expect(isBtcTestnetAddress(address)).toBe(true); - }, - ); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each([...MAINNET_ADDRESSES, ...ETH_ADDRESSES])( - 'returns false if address is compatible with BTC testnet: %s', - (address: string) => { - expect(isBtcTestnetAddress(address)).toBe(false); - }, - ); + describe('isBtcMainnetAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(BTC_MAINNET_ADDRESSES)( + 'returns true if address is compatible with BTC mainnet: %s', + (address: string) => { + expect(isBtcMainnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + 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); + }, + ); + }); + + describe('isBtcTestnetAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(BTC_TESTNET_ADDRESSES)( + 'returns true if address is compatible with BTC testnet: %s', + (address: string) => { + expect(isBtcTestnetAddress(address)).toBe(true); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + 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); + }, + ); + }); + + describe('getChainTypeFromAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([...BTC_MAINNET_ADDRESSES, ...BTC_TESTNET_ADDRESSES])( + 'returns ChainType.Bitcoin for bitcoin address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Bip122, + ); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(ETH_ADDRESSES)( + 'returns ChainType.Ethereum for ethereum address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Eip155, + ); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(SOL_ADDRESSES)( + 'returns ChainType.Ethereum for non-supported address: %s', + (address: string) => { + expect(getCaipNamespaceFromAddress(address)).toBe( + KnownCaipNamespace.Eip155, + ); + }, + ); + }); }); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index fec52295eada..942a9ce6c964 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,6 +1,5 @@ -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { isEthAddress } from '../../app/scripts/lib/multichain/address'; +import { CaipNamespace, KnownCaipNamespace } from '@metamask/utils'; +import { validate, Network } from 'bitcoin-address-validation'; /** * Returns whether an address is on the Bitcoin mainnet. @@ -14,10 +13,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 +25,19 @@ 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); +} + +/** + * Returns the associated chain's type for the given address. + * + * @param address - The address to check. + * @returns The chain's type for that address. + */ +export function getCaipNamespaceFromAddress(address: string): CaipNamespace { + if (isBtcMainnetAddress(address) || isBtcTestnetAddress(address)) { + return KnownCaipNamespace.Bip122; + } + // Defaults to "Ethereum" for all other cases for now. + return KnownCaipNamespace.Eip155; } diff --git a/shared/lib/swaps-utils.js b/shared/lib/swaps-utils.js index d80d70902810..c51a3ac1198e 100644 --- a/shared/lib/swaps-utils.js +++ b/shared/lib/swaps-utils.js @@ -265,6 +265,7 @@ export async function fetchTradesInfo( value, fromAddress, exchangeList, + enableGasIncludedQuotes, }, { chainId }, ) { @@ -275,6 +276,7 @@ export async function fetchTradesInfo( slippage, timeout: SECOND * 10, walletAddress: fromAddress, + enableGasIncludedQuotes, }; if (exchangeList) { diff --git a/shared/lib/swaps-utils.test.js b/shared/lib/swaps-utils.test.js index 06080a8f55e7..891c1c5fb961 100644 --- a/shared/lib/swaps-utils.test.js +++ b/shared/lib/swaps-utils.test.js @@ -87,6 +87,7 @@ describe('Swaps Utils', () => { sourceDecimals: TOKENS[0].decimals, sourceTokenInfo: { ...TOKENS[0] }, destinationTokenInfo: { ...TOKENS[1] }, + enableGasIncludedQuotes: false, }, { chainId: CHAIN_IDS.MAINNET }, ); diff --git a/shared/lib/trace.test.ts b/shared/lib/trace.test.ts index 7cd39eba03d1..ff55ec0f2df0 100644 --- a/shared/lib/trace.test.ts +++ b/shared/lib/trace.test.ts @@ -170,6 +170,27 @@ describe('Trace', () => { 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); + }); }); describe('endTrace', () => { @@ -264,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 b3e6e9f90168..5ca256371502 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -21,6 +21,7 @@ export enum TraceName { NotificationDisplay = 'Notification Display', PPOMValidation = 'PPOM Validation', SetupStore = 'Setup Store', + Signature = 'Signature', Transaction = 'Transaction', UIStartup = 'UI Startup', } diff --git a/shared/lib/transactions-controller-utils.js b/shared/lib/transactions-controller-utils.js index 88b7015b2090..437fbc1063b9 100644 --- a/shared/lib/transactions-controller-utils.js +++ b/shared/lib/transactions-controller-utils.js @@ -36,11 +36,11 @@ export function toPrecisionWithoutTrailingZeros(n, precision) { /** * @param {number|string|BigNumber} value - * @param {number} decimals + * @param {number=} decimals * @returns {BigNumber} */ -export function calcTokenAmount(value, decimals = 0) { - const divisor = new BigNumber(10).pow(decimals); +export function calcTokenAmount(value, decimals) { + const divisor = new BigNumber(10).pow(decimals ?? 0); return new BigNumber(String(value)).div(divisor); } diff --git a/shared/modules/currency-display.utils.test.ts b/shared/modules/currency-display.utils.test.ts deleted file mode 100644 index b2fdbc456593..000000000000 --- a/shared/modules/currency-display.utils.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - showPrimaryCurrency, - showSecondaryCurrency, -} from './currency-display.utils'; - -describe('showPrimaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is true', () => { - const result = showPrimaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showPrimaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency and isOriginalNativeSymbol are false', () => { - const result = showPrimaryCurrency(false, false); - expect(result).toBe(false); - }); -}); - -describe('showSecondaryCurrency', () => { - it('should return true when useNativeCurrencyAsPrimaryCurrency is false', () => { - const result = showSecondaryCurrency(true, false); - expect(result).toBe(true); - }); - - it('should return true when isOriginalNativeSymbol is true', () => { - const result = showSecondaryCurrency(true, true); - expect(result).toBe(true); - }); - - it('should return false when useNativeCurrencyAsPrimaryCurrency is true and isOriginalNativeSymbol is false', () => { - const result = showSecondaryCurrency(false, true); - expect(result).toBe(false); - }); -}); diff --git a/shared/modules/currency-display.utils.ts b/shared/modules/currency-display.utils.ts deleted file mode 100644 index 3f50a2364e6d..000000000000 --- a/shared/modules/currency-display.utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const showPrimaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the primary currency in this case , so we have to display it always - if (useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the primary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; - -export const showSecondaryCurrency = ( - isOriginalNativeSymbol: boolean, - useNativeCurrencyAsPrimaryCurrency: boolean, -): boolean => { - // crypto is the secondary currency in this case , so we have to display it always - if (!useNativeCurrencyAsPrimaryCurrency) { - return true; - } - // if the secondary currency corresponds to a fiat value, check that the symbol is correct. - if (isOriginalNativeSymbol) { - return true; - } - - return false; -}; diff --git a/shared/modules/metametrics.test.ts b/shared/modules/metametrics.test.ts index 93d423fb3d2d..9d6d17bc8040 100644 --- a/shared/modules/metametrics.test.ts +++ b/shared/modules/metametrics.test.ts @@ -72,6 +72,9 @@ const createTransactionMeta = () => { }, hash: txHash, error: null, + swapMetaData: { + gas_included: true, + }, }; }; @@ -107,6 +110,7 @@ describe('getSmartTransactionMetricsProperties', () => { ); expect(result).toStrictEqual({ + gas_included: true, is_smart_transaction: true, smart_transaction_duplicated: true, smart_transaction_proxied: true, @@ -132,7 +136,7 @@ describe('getSmartTransactionMetricsProperties', () => { }); }); - it('returns "is_smart_transaction: true" only if it is a smart transaction, but does not have statusMetadata', () => { + it('returns "is_smart_transaction" and "gas_included" params only if it is a smart transaction, but does not have statusMetadata', () => { const transactionMetricsRequest = createTransactionMetricsRequest({ getIsSmartTransaction: () => true, getSmartTransactionByMinedTxHash: () => { @@ -152,6 +156,7 @@ describe('getSmartTransactionMetricsProperties', () => { expect(result).toStrictEqual({ is_smart_transaction: true, + gas_included: true, }); }); }); diff --git a/shared/modules/metametrics.ts b/shared/modules/metametrics.ts index c60c7a0e44cd..b689891da1fb 100644 --- a/shared/modules/metametrics.ts +++ b/shared/modules/metametrics.ts @@ -5,6 +5,7 @@ import { TransactionMetricsRequest } from '../../app/scripts/lib/transaction/met type SmartTransactionMetricsProperties = { is_smart_transaction: boolean; + gas_included: boolean; smart_transaction_duplicated?: boolean; smart_transaction_timed_out?: boolean; smart_transaction_proxied?: boolean; @@ -21,6 +22,7 @@ export const getSmartTransactionMetricsProperties = ( if (!isSmartTransaction) { return properties; } + properties.gas_included = transactionMeta.swapMetaData?.gas_included; const smartTransaction = transactionMetricsRequest.getSmartTransactionByMinedTxHash( transactionMeta.hash, diff --git a/sonar-project.properties b/sonar-project.properties index a965dab30d7e..ad18a60d6fc7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,9 @@ sonar.organization=consensys # Source sonar.sources=app,development,offscreen,shared,types,ui -sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data + +# Exclude tests and stories from all analysis (to avoid code coverage, duplicate code, security issues, etc.) +sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data,**/*.stories.js,**/*.stories.tsx # Tests sonar.tests=app,development,offscreen,shared,test,types,ui diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 17666ff0aba1..49a6e1aad1ab 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -173,43 +173,3 @@ export const genUnapprovedContractInteractionConfirmation = ({ return confirmation; }; - -export const genUnapprovedApproveConfirmation = ({ - address = CONTRACT_INTERACTION_SENDER_ADDRESS, - chainId = CHAIN_ID, -}: { - address?: Hex; - chainId?: string; -} = {}) => ({ - ...genUnapprovedContractInteractionConfirmation({ chainId }), - txParams: { - from: address, - data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', - }, - type: TransactionType.tokenMethodApprove, -}); - -export const genUnapprovedSetApprovalForAllConfirmation = ({ - address = CONTRACT_INTERACTION_SENDER_ADDRESS, - chainId = CHAIN_ID, -}: { - address?: Hex; - chainId?: string; -} = {}) => ({ - ...genUnapprovedContractInteractionConfirmation({ chainId }), - txParams: { - from: address, - data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', - }, - type: TransactionType.tokenMethodSetApprovalForAll, -}); diff --git a/test/data/confirmations/helper.ts b/test/data/confirmations/helper.ts index 9eb8bb234768..6669c043d0ea 100644 --- a/test/data/confirmations/helper.ts +++ b/test/data/confirmations/helper.ts @@ -1,18 +1,17 @@ import { ApprovalType } from '@metamask/controller-utils'; import { merge } from 'lodash'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import { Confirmation, SignatureRequestType, } from '../../../ui/pages/confirmations/types/confirm'; import mockState from '../mock-state.json'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import { - genUnapprovedApproveConfirmation, - genUnapprovedContractInteractionConfirmation, - genUnapprovedSetApprovalForAllConfirmation, -} from './contract-interaction'; +import { genUnapprovedContractInteractionConfirmation } from './contract-interaction'; import { unapprovedPersonalSignMsg } from './personal_sign'; +import { genUnapprovedSetApprovalForAllConfirmation } from './set-approval-for-all'; +import { genUnapprovedApproveConfirmation } from './token-approve'; +import { genUnapprovedTokenTransferConfirmation } from './token-transfer'; import { unapprovedTypedSignMsgV4 } from './typed_sign'; type RootState = { metamask: Record } & Record< @@ -183,3 +182,16 @@ export const getMockSetApprovalForAllConfirmState = () => { genUnapprovedSetApprovalForAllConfirmation({ chainId: '0x5' }), ); }; + +export const getMockTokenTransferConfirmState = ({ + isWalletInitiatedConfirmation = false, +}: { + isWalletInitiatedConfirmation?: boolean; +}) => { + return getMockConfirmStateForTransaction( + genUnapprovedTokenTransferConfirmation({ + chainId: '0x5', + isWalletInitiatedConfirmation, + }), + ); +}; diff --git a/test/data/confirmations/set-approval-for-all.ts b/test/data/confirmations/set-approval-for-all.ts new file mode 100644 index 000000000000..ca997f6212af --- /dev/null +++ b/test/data/confirmations/set-approval-for-all.ts @@ -0,0 +1,27 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedSetApprovalForAllConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, +}: { + address?: Hex; + chainId?: string; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodSetApprovalForAll, +}); diff --git a/test/data/confirmations/token-approve.ts b/test/data/confirmations/token-approve.ts new file mode 100644 index 000000000000..c77d59101a99 --- /dev/null +++ b/test/data/confirmations/token-approve.ts @@ -0,0 +1,27 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedApproveConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, +}: { + address?: Hex; + chainId?: string; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodApprove, +}); diff --git a/test/data/confirmations/token-transfer.ts b/test/data/confirmations/token-transfer.ts new file mode 100644 index 000000000000..22d0cb2d00b4 --- /dev/null +++ b/test/data/confirmations/token-transfer.ts @@ -0,0 +1,32 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { + CHAIN_ID, + CONTRACT_INTERACTION_SENDER_ADDRESS, + genUnapprovedContractInteractionConfirmation, +} from './contract-interaction'; + +export const genUnapprovedTokenTransferConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + chainId = CHAIN_ID, + isWalletInitiatedConfirmation = false, +}: { + address?: Hex; + chainId?: string; + isWalletInitiatedConfirmation?: boolean; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation({ chainId }), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodTransfer, + origin: isWalletInitiatedConfirmation + ? 'metamask' + : 'https://metamask.github.io', +}); diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 59d24a1f5f54..96cd95cfbd84 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -12,6 +12,7 @@ "appState": { "networkDropdownOpen": false, "importNftsModal": { "open": false }, + "showPermittedNetworkToastOpen": false, "gasIsLoading": false, "isLoading": false, "importTokensModalOpen": false, @@ -130,8 +131,7 @@ "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, - "showTestNetworks": true, - "useNativeCurrencyAsPrimaryCurrency": true + "showTestNetworks": true }, "seedPhraseBackedUp": null, "ensResolutionsByAddress": {}, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 5a86bbb4970b..654e915a1305 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -14,6 +14,7 @@ "importNftsModal": { "open": false }, + "showPermittedNetworkToastOpen": false, "gasIsLoading": false, "isLoading": false, "modal": { @@ -366,12 +367,17 @@ "preferences": { "hideZeroBalanceTokens": false, "isRedesignedConfirmationsDeveloperEnabled": false, + "petnamesEnabled": false, "showExtensionInFullSizeView": false, "showFiatInTestnets": false, + "showNativeTokenAsMainBalance": true, "showTestNetworks": true, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, - "petnamesEnabled": false + "tokenSortConfig": { + "key": "tokenFiatAmount", + "order": "dsc", + "sortCallback": "stringNumeric" + } }, "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, @@ -615,8 +621,8 @@ "developer": "Metamask", "website": "https://www.consensys.io/", "auditUrls": ["auditUrl1", "auditUrl2"], - "version": "1.0.0", - "lastUpdated": "April 20, 2023" + "version": "1.1.6", + "lastUpdated": "September 26, 2024" } }, "notifications": { 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/accounts/remove-account-snap.spec.ts b/test/e2e/accounts/remove-account-snap.spec.ts deleted file mode 100644 index f4b8e025c62d..000000000000 --- a/test/e2e/accounts/remove-account-snap.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { strict as assert } from 'assert'; -import { Suite } from 'mocha'; -import FixtureBuilder from '../fixture-builder'; -import { WINDOW_TITLES, defaultGanacheOptions, withFixtures } from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { installSnapSimpleKeyring, makeNewAccountAndSwitch } from './common'; - -describe('Remove Account Snap', function (this: Suite) { - it('disable a snap and remove it', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); - - await makeNewAccountAndSwitch(driver); - - // Check accounts after adding the snap account. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsWithSnapAdded = await driver.findElements( - '.multichain-account-list-item', - ); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Navigate to settings. - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - - await driver.clickElement({ text: 'Snaps', tag: 'div' }); - await driver.clickElement({ - text: 'MetaMask Simple Snap Keyring', - tag: 'p', - }); - - // Disable the snap. - await driver.clickElement('.toggle-button > div'); - - // Remove the snap. - const removeButton = await driver.findElement( - '[data-testid="remove-snap-button"]', - ); - await driver.scrollToElement(removeButton); - await driver.clickElement('[data-testid="remove-snap-button"]'); - - await driver.clickElement({ - text: 'Continue', - tag: 'button', - }); - - await driver.fill( - '[data-testid="remove-snap-confirmation-input"]', - 'MetaMask Simple Snap Keyring', - ); - - await driver.clickElement({ - text: 'Remove Snap', - tag: 'button', - }); - - // Checking result modal - await driver.findVisibleElement({ - text: 'MetaMask Simple Snap Keyring removed', - tag: 'p', - }); - - // Assert that the snap was removed. - await driver.findElement({ - css: '.mm-box', - text: "You don't have any snaps installed.", - tag: 'p', - }); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Assert that an account was removed. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsAfterRemoval = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal( - accountMenuItemsAfterRemoval.length, - accountMenuItemsWithSnapAdded.length - 1, - ); - }, - ); - }); -}); 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/constants.ts b/test/e2e/constants.ts index e7fef587f533..7e92a28cf463 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -20,7 +20,7 @@ export const BUNDLER_URL = 'http://localhost:3000/rpc'; /* URL of the 4337 account snap site. */ export const ERC_4337_ACCOUNT_SNAP_URL = - 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.1/'; + 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.2/'; /* Salt used to generate the 4337 account. */ export const ERC_4337_ACCOUNT_SALT = '0x1'; @@ -31,7 +31,7 @@ export const SIMPLE_ACCOUNT_FACTORY = /* URL of the Snap Simple Keyring site. */ export const TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL = - 'https://metamask.github.io/snap-simple-keyring/1.1.2/'; + 'https://metamask.github.io/snap-simple-keyring/1.1.6/'; /* Address of the VerifyingPaymaster smart contract deployed to Ganache. */ export const VERIFYING_PAYMASTER = '0xbdbDEc38ed168331b1F7004cc9e5392A2272C1D7'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 470e260ff959..2c0dfe9a23cb 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -127,6 +127,10 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { srcNetworkAllowlist: ['0x1', '0xa', '0xe708'], destNetworkAllowlist: ['0x1', '0xa', '0xe708'], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }, CurrencyController: { @@ -206,11 +210,17 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + shouldShowAggregatedBalancePopover: true, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index cc30c261d22d..f1e9a7e5ae1d 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -72,11 +72,17 @@ function onboardingFixture() { showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: false, - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, isRedesignedConfirmationsDeveloperEnabled: false, showConfirmationAdvancedDetails: false, + tokenSortConfig: { + key: 'tokenFiatAmount', + order: 'dsc', + sortCallback: 'stringNumeric', + }, + shouldShowAggregatedBalancePopover: true, }, useExternalServices: true, theme: 'light', @@ -186,6 +192,14 @@ class FixtureBuilder { }); } + withShowFiatTestnetEnabled() { + return this.withPreferencesController({ + preferences: { + showFiatInTestnets: true, + }, + }); + } + withConversionRateEnabled() { return this.withPreferencesController({ useCurrencyRateCheck: true, @@ -391,6 +405,10 @@ class FixtureBuilder { extensionSupport: false, srcNetworkAllowlist: [], }, + destTokens: {}, + destTopAssets: [], + srcTokens: {}, + srcTopAssets: [], }, }; return this; @@ -594,6 +612,14 @@ class FixtureBuilder { }); } + withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() { + return this.withPreferencesController({ + preferences: { + showNativeTokenAsMainBalance: false, + }, + }); + } + withPreferencesControllerTxSimulationsDisabled() { return this.withPreferencesController({ useTransactionSimulations: false, 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..bf55c7bbf52c 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -546,15 +546,27 @@ const onboardingRevealAndConfirmSRP = async (driver) => { */ const onboardingCompleteWalletCreation = async (driver) => { // complete - await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); + await driver.findElement({ text: 'Congratulations', tag: 'h2' }); await driver.clickElement('[data-testid="onboarding-complete-done"]'); }; +/** + * Move through the steps of pinning extension after successful onboarding + * + * @param {WebDriver} driver + */ +const onboardingPinExtension = async (driver) => { + // pin extension + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); +}; + const onboardingCompleteWalletCreationWithOptOut = async (driver) => { // wait for h2 to appear - await driver.findElement({ text: 'Wallet creation successful', tag: 'h2' }); + await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ text: 'Manage default settings', tag: 'button' }); + await driver.clickElement({ text: 'General', tag: 'p' }); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); @@ -568,19 +580,12 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { ) ).map((toggle) => toggle.click()), ); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement('[data-testid="privacy-settings-back-button"]'); + // complete onboarding await driver.clickElement({ text: 'Done', tag: 'button' }); -}; - -/** - * Move through the steps of pinning extension after successful onboarding - * - * @param {WebDriver} driver - */ -const onboardingPinExtension = async (driver) => { - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); + await onboardingPinExtension(driver); }; const completeCreateNewWalletOnboardingFlowWithOptOut = async ( @@ -755,12 +760,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/mock-e2e.js b/test/e2e/mock-e2e.js index 929a424a36ef..abc536ef6059 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -4,6 +4,10 @@ const { BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, } = require('../../shared/constants/bridge'); +const { + ACCOUNTS_DEV_API_BASE_URL, + ACCOUNTS_PROD_API_BASE_URL, +} = require('../../shared/constants/accounts'); const { GAS_API_BASE_URL, SWAPS_API_V2_BASE_URL, @@ -313,6 +317,52 @@ async function setupMocking( }), ); + [ + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-id/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-fd20/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/test-metrics-id/surveys`, + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/invalid-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-fd20/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/test-metrics-id/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/invalid-metrics-id/surveys`, + ].forEach( + async (url) => + await server.forGet(url).thenCallback(() => { + return { + statusCode: 200, + json: { + userId: '0x123', + surveys: {}, + }, + }; + }), + ); + + let surveyCallCount = 0; + [ + `${ACCOUNTS_DEV_API_BASE_URL}/v1/users/fake-metrics-id-power-user/surveys`, + `${ACCOUNTS_PROD_API_BASE_URL}/v1/users/fake-metrics-id-power-user/surveys`, + ].forEach( + async (url) => + await server.forGet(url).thenCallback(() => { + const surveyId = surveyCallCount > 2 ? 2 : surveyCallCount; + surveyCallCount += 1; + return { + statusCode: 200, + json: { + userId: '0x123', + surveys: { + url: 'https://example.com', + description: `Test survey ${surveyId}`, + cta: 'Take survey', + id: surveyId, + }, + }, + }; + }), + ); + await server .forGet(`https://token.api.cx.metamask.io/tokens/${chainId}`) .thenCallback(() => { 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/flows/snap-simple-keyring.flow.ts b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts new file mode 100644 index 000000000000..68febce34b6b --- /dev/null +++ b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts @@ -0,0 +1,23 @@ +import { Driver } from '../../webdriver/driver'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../../constants'; + +/** + * Go to the Snap Simple Keyring page and install the snap. + * + * @param driver - The WebDriver instance used to interact with the browser. + * @param isSyncFlow - Indicates whether to toggle on the use synchronous approval option on the snap. Defaults to true. + */ +export async function installSnapSimpleKeyring( + driver: Driver, + isSyncFlow: boolean = true, +) { + await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); + + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.check_pageIsLoaded(); + await snapSimpleKeyringPage.installSnap(); + if (isSyncFlow) { + await snapSimpleKeyringPage.toggleUseSyncApproval(); + } +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 05c394444c36..03bdeef1579d 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,69 +1,58 @@ import { Driver } from '../../webdriver/driver'; class AccountListPage { - private driver: Driver; + private readonly driver: Driver; - private accountListItem: string; + private readonly accountListItem = + '.multichain-account-menu-popover__list--menu-item'; - private accountMenuButton: string; + private readonly accountMenuButton = + '[data-testid="account-list-menu-details"]'; - private accountNameInput: string; + private readonly accountNameInput = '#account-name'; - private accountOptionsMenuButton: string; + private readonly accountOptionsMenuButton = + '[data-testid="account-list-item-menu-button"]'; - private addAccountConfirmButton: string; + private readonly addAccountConfirmButton = + '[data-testid="submit-add-account-with-name"]'; - private addEthereumAccountButton: string; + private readonly addEthereumAccountButton = + '[data-testid="multichain-account-menu-popover-add-account"]'; - private addSnapAccountButton: object; + private readonly addSnapAccountButton = { + text: 'Add account Snap', + tag: 'button', + }; - private closeAccountModalButton: string; + private readonly closeAccountModalButton = 'button[aria-label="Close"]'; - private createAccountButton: string; + private readonly createAccountButton = + '[data-testid="multichain-account-menu-popover-action-button"]'; - private editableLabelButton: string; + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; - private editableLabelInput: string; + private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private hideUnhideAccountButton: string; + private readonly hideUnhideAccountButton = + '[data-testid="account-list-menu-hide"]'; - private hiddenAccountOptionsMenuButton: string; + private readonly hiddenAccountOptionsMenuButton = + '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - private hiddenAccountsList: string; + private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - private pinUnpinAccountButton: string; + private readonly pinUnpinAccountButton = + '[data-testid="account-list-menu-pin"]'; - private pinnedIcon: string; + private readonly pinnedIcon = '[data-testid="account-pinned-icon"]'; - private saveAccountLabelButton: string; + private readonly saveAccountLabelButton = + '[data-testid="save-account-label-input"]'; constructor(driver: Driver) { this.driver = driver; - this.accountListItem = '.multichain-account-menu-popover__list--menu-item'; - this.accountMenuButton = '[data-testid="account-list-menu-details"]'; - this.accountNameInput = '#account-name'; - this.accountOptionsMenuButton = - '[data-testid="account-list-item-menu-button"]'; - this.addAccountConfirmButton = - '[data-testid="submit-add-account-with-name"]'; - this.addEthereumAccountButton = - '[data-testid="multichain-account-menu-popover-add-account"]'; - this.addSnapAccountButton = { - text: 'Add account Snap', - tag: 'button', - }; - this.closeAccountModalButton = 'button[aria-label="Close"]'; - this.createAccountButton = - '[data-testid="multichain-account-menu-popover-action-button"]'; - this.editableLabelButton = '[data-testid="editable-label-button"]'; - this.editableLabelInput = '[data-testid="editable-input"] input'; - this.hideUnhideAccountButton = '[data-testid="account-list-menu-hide"]'; - this.hiddenAccountOptionsMenuButton = - '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - this.hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - this.pinUnpinAccountButton = '[data-testid="account-list-menu-pin"]'; - this.pinnedIcon = '[data-testid="account-pinned-icon"]'; - this.saveAccountLabelButton = '[data-testid="save-account-label-input"]'; } async check_pageIsLoaded(): Promise { @@ -179,9 +168,21 @@ class AccountListPage { }); } - async check_accountIsDisplayed(): Promise { - console.log(`Check that account is displayed in account list`); - await this.driver.waitForSelector(this.accountListItem); + /** + * Checks that the account with the specified label is not displayed in the account list. + * + * @param expectedLabel - The label of the account that should not be displayed. + */ + async check_accountIsNotDisplayedInAccountList( + expectedLabel: string, + ): Promise { + console.log( + `Check that account label ${expectedLabel} is not displayed in account list`, + ); + await this.driver.assertElementNotPresent({ + css: this.accountListItem, + text: expectedLabel, + }); } async check_accountIsPinned(): Promise { diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 742b37a5c48f..495f7ddbf3c8 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -9,18 +9,28 @@ class HeaderNavbar { private lockMetaMaskButton: string; + private mmiPortfolioButton: string; + private settingsButton: string; + private accountSnapButton: object; + constructor(driver: Driver) { this.driver = driver; this.accountMenuButton = '[data-testid="account-menu-icon"]'; this.accountOptionMenu = '[data-testid="account-options-menu-button"]'; this.lockMetaMaskButton = '[data-testid="global-menu-lock"]'; + this.mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; this.settingsButton = '[data-testid="global-menu-settings"]'; + this.accountSnapButton = { text: 'Snaps', tag: 'div' }; } async lockMetaMask(): Promise { await this.driver.clickElement(this.accountOptionMenu); + // fix race condition with mmi build + if (process.env.MMI) { + await this.driver.waitForSelector(this.mmiPortfolioButton); + } await this.driver.clickElement(this.lockMetaMaskButton); } @@ -28,9 +38,19 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openSnapListPage(): Promise { + console.log('Open account snap page'); + await this.driver.clickElement(this.accountOptionMenu); + await this.driver.clickElement(this.accountSnapButton); + } + async openSettingsPage(): Promise { console.log('Open settings page'); await this.driver.clickElement(this.accountOptionMenu); + // fix race condition with mmi build + if (process.env.MMI) { + await this.driver.waitForSelector(this.mmiPortfolioButton); + } await this.driver.clickElement(this.settingsButton); } 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/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 89678c8712ac..547f9e43a34e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -29,7 +29,7 @@ class SettingsPage { } async goToExperimentalSettings(): Promise { - console.log('Navigating to Experimental Settings'); + console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } } diff --git a/test/e2e/page-objects/pages/snap-list-page.ts b/test/e2e/page-objects/pages/snap-list-page.ts new file mode 100644 index 000000000000..b293340ea637 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-list-page.ts @@ -0,0 +1,80 @@ +import { Driver } from '../../webdriver/driver'; + +class SnapListPage { + private readonly driver: Driver; + + private readonly closeModalButton = 'button[aria-label="Close"]'; + + private readonly continueRemoveSnapButton = { + tag: 'button', + text: 'Continue', + }; + + private readonly continueRemoveSnapModalMessage = { + tag: 'p', + text: 'Removing this Snap removes these accounts from MetaMask', + }; + + private readonly noSnapInstalledMessage = { + tag: 'p', + text: "You don't have any snaps installed.", + }; + + private readonly removeSnapButton = '[data-testid="remove-snap-button"]'; + + private readonly removeSnapConfirmationInput = + '[data-testid="remove-snap-confirmation-input"]'; + + private readonly removeSnapConfirmButton = { + tag: 'button', + text: 'Remove Snap', + }; + + // this selector needs to be combined with snap name to be unique. + private readonly snapListItem = '.snap-list-item'; + + constructor(driver: Driver) { + this.driver = driver; + } + + /** + * Removes a snap by its name from the snap list. + * + * @param snapName - The name of the snap to be removed. + */ + async removeSnapByName(snapName: string): Promise { + console.log('Removing snap on snap list page with name: ', snapName); + await this.driver.clickElement({ text: snapName, css: this.snapListItem }); + + const removeButton = await this.driver.findElement(this.removeSnapButton); + // The need to scroll to the element before clicking it is due to a bug in the Snap test dapp page. + // This bug has been fixed in the Snap test dapp page (PR here: https://github.com/MetaMask/snaps/pull/2782), which should mitigate the flaky issue of scrolling and clicking elements in the Snap test dapp. + // TODO: Once the Snaps team releases the new version with the fix, we'll be able to remove these scrolling steps and just use clickElement (which already handles scrolling). + await this.driver.scrollToElement(removeButton); + await this.driver.clickElement(this.removeSnapButton); + + await this.driver.waitForSelector(this.continueRemoveSnapModalMessage); + await this.driver.clickElement(this.continueRemoveSnapButton); + + console.log(`Fill confirmation input to confirm snap removal`); + await this.driver.waitForSelector(this.removeSnapConfirmationInput); + await this.driver.fill(this.removeSnapConfirmationInput, snapName); + await this.driver.clickElementAndWaitToDisappear( + this.removeSnapConfirmButton, + ); + + console.log(`Check snap removal success message is displayed`); + await this.driver.waitForSelector({ + text: `${snapName} removed`, + tag: 'p', + }); + await this.driver.clickElementAndWaitToDisappear(this.closeModalButton); + } + + async check_noSnapInstalledMessageIsDisplayed(): Promise { + console.log('Verifying no snaps is installed for current account'); + await this.driver.waitForSelector(this.noSnapInstalledMessage); + } +} + +export default SnapListPage; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts new file mode 100644 index 000000000000..fd4ae9d1ecc1 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -0,0 +1,226 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; + +class SnapSimpleKeyringPage { + private readonly driver: Driver; + + private readonly accountCreatedMessage = { + text: 'Account created', + tag: 'h3', + }; + + private readonly accountSupportedMethods = { + text: 'Account Supported Methods', + tag: 'p', + }; + + private readonly addtoMetamaskMessage = { + text: 'Add to MetaMask', + 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"]'; + + private readonly confirmCompleteButton = { + text: 'OK', + tag: 'button', + }; + + private readonly confirmConnectionButton = { + text: 'Connect', + tag: 'button', + }; + + private readonly connectButton = '#connectButton'; + + private readonly createAccountButton = { + text: 'Create Account', + tag: 'button', + }; + + private readonly createAccountMessage = + '[data-testid="create-snap-account-content-title"]'; + + private readonly createAccountSection = { + text: 'Create account', + tag: 'div', + }; + + private readonly createSnapAccountName = '#account-name'; + + private readonly errorRequestMessage = { + text: 'Error request', + tag: 'p', + }; + + private readonly installationCompleteMessage = { + text: 'Installation complete', + tag: 'h2', + }; + + private readonly pageTitle = { + text: 'Snap Simple Keyring', + tag: 'p', + }; + + private readonly snapConnectedMessage = '#snapConnected'; + + private readonly snapInstallScrollButton = + '[data-testid="snap-install-scroll"]'; + + private readonly submitAddAccountWithNameButton = + '[data-testid="submit-add-account-with-name"]'; + + private readonly useSyncApprovalToggle = + '[data-testid="use-sync-flow-toggle"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.pageTitle, + this.useSyncApprovalToggle, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for Snap Simple Keyring page to be loaded', + e, + ); + throw e; + } + 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( + accountName: string = 'SSK Account', + isFirstAccount: boolean = true, + ): Promise { + console.log('Create new account on Snap Simple Keyring page'); + 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); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_accountSupportedMethodsDisplayed(); + } + + /** + * Installs the Simple Keyring Snap and checks the snap is connected. + */ + async installSnap(): Promise { + console.log('Install Simple Keyring Snap'); + await this.driver.clickElement(this.connectButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.clickElement(this.confirmConnectionButton); + + await this.driver.waitForSelector(this.addtoMetamaskMessage); + await this.driver.clickElementSafe(this.snapInstallScrollButton, 200); + await this.driver.waitForSelector(this.confirmAddtoMetamask); + await this.driver.clickElement(this.confirmAddtoMetamask); + + await this.driver.waitForSelector(this.installationCompleteMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmCompleteButton, + ); + + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + 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); + } + + async check_accountSupportedMethodsDisplayed(): Promise { + console.log( + 'Check new created account supported methods are displayed on simple keyring snap page', + ); + await this.driver.waitForSelector(this.accountSupportedMethods); + } + + async check_errorRequestMessageDisplayed(): Promise { + console.log( + 'Check error request message is displayed on snap simple keyring page', + ); + await this.driver.waitForSelector(this.errorRequestMessage); + } + + async check_simpleKeyringSnapConnected(): Promise { + console.log('Check simple keyring snap is connected'); + await this.driver.waitForSelector(this.snapConnectedMessage); + } +} + +export default SnapSimpleKeyringPage; diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 0f400e8a34e7..846acc8164cd 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -36,8 +36,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true + "smartTransactionsOptInStatus": false }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index 6e1c16efa82d..290e8b863a9e 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,4 +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}`; @@ -7,6 +9,84 @@ function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : 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(); + + const newFlags = regexSearchForFlags(gitMessage, 'git message'); + + if (newFlags) { + // Use lodash merge to do a deep merge (spread operator is shallow) + merge(flags, newFlags); + } +} + // Alter the manifest with CircleCI environment variables and custom flags export function setManifestFlags(flags: ManifestFlags = {}) { if (process.env.CIRCLECI) { @@ -20,6 +100,17 @@ export function setManifestFlags(flags: ManifestFlags = {}) { process.env.CIRCLE_PULL_REQUEST?.split('/').pop(), // The CIRCLE_PR_NUMBER variable is only available on forked Pull Requests ), }; + + addFlagsFromPrBody(flags); + addFlagsFromGitMessage(flags); + + // Set `flags.sentry.forceEnable` to true by default + if (flags.sentry === undefined) { + flags.sentry = {}; + } + if (flags.sentry.forceEnable === undefined) { + flags.sentry.forceEnable = true; + } } const manifest = JSON.parse( diff --git a/test/e2e/snaps/test-snap-installed.spec.js b/test/e2e/snaps/test-snap-installed.spec.js index e9697fff806e..5c7a3394966f 100644 --- a/test/e2e/snaps/test-snap-installed.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -35,7 +35,7 @@ describe('Test Snap Installed', function () { const confirmButton = await driver.findElement('#connectdialogs'); await driver.scrollToElement(confirmButton); - await driver.delay(500); + await driver.delay(1000); await driver.clickElement('#connectdialogs'); // switch to metamask extension and click connect 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/account-details.spec.js b/test/e2e/tests/account/account-details.spec.js index a21b9fad2e7d..f40560cb27fd 100644 --- a/test/e2e/tests/account/account-details.spec.js +++ b/test/e2e/tests/account/account-details.spec.js @@ -60,8 +60,7 @@ describe('Show account details', function () { ); await driver.clickElement('[data-testid="account-list-menu-details"'); - const qrCode = await driver.findElement('.qr-code__wrapper'); - assert.equal(await qrCode.isDisplayed(), true); + await driver.waitForSelector('.qr-code__wrapper'); }, ); }); @@ -198,11 +197,10 @@ describe('Show account details', function () { await driver.press('#account-details-authenticate', driver.Key.ENTER); // Display error when password is incorrect - const passwordErrorIsDisplayed = await driver.isElementPresent({ + await driver.waitForSelector({ css: '.mm-help-text', text: 'Incorrect Password.', }); - assert.equal(passwordErrorIsDisplayed, true); }, ); }); diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js index a7136837b01d..c1a6136cc47d 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -74,6 +74,11 @@ describe('Add account', function () { // Create 2nd account await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); diff --git a/test/e2e/tests/account/create-remove-account-snap.spec.ts b/test/e2e/tests/account/create-remove-account-snap.spec.ts new file mode 100644 index 000000000000..5d8517f66b26 --- /dev/null +++ b/test/e2e/tests/account/create-remove-account-snap.spec.ts @@ -0,0 +1,50 @@ +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 SnapListPage from '../../page-objects/pages/snap-list-page'; +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 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(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to account snaps list page. + await headerNavbar.openSnapListPage(); + const snapListPage = new SnapListPage(driver); + + // Remove the snap and check snap is successfully removed + await snapListPage.removeSnapByName('MetaMask Simple Snap Keyring'); + await snapListPage.check_noSnapInstalledMessageIsDisplayed(); + + // Assert that the snap account is removed from the account list + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); +}); 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/account/import-flow.spec.js b/test/e2e/tests/account/import-flow.spec.js index 045600cff4f4..d2c84bfdc2b3 100644 --- a/test/e2e/tests/account/import-flow.spec.js +++ b/test/e2e/tests/account/import-flow.spec.js @@ -58,6 +58,11 @@ describe('Import flow @no-mmi', function () { // Show account information await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="account-list-item-menu-button"]', ); @@ -99,6 +104,11 @@ describe('Import flow @no-mmi', function () { // choose Create account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -182,6 +192,11 @@ describe('Import flow @no-mmi', function () { // Show account information await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="account-list-item-menu-button"]', ); @@ -226,6 +241,11 @@ describe('Import flow @no-mmi', function () { await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -249,6 +269,11 @@ describe('Import flow @no-mmi', function () { text: 'Imported', }); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 4', + tag: 'span', + }); // Imports Account 5 with private key await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', @@ -307,6 +332,11 @@ describe('Import flow @no-mmi', function () { await logInWithBalanceValidation(driver, ganacheServer); // Imports an account with JSON file await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -372,6 +402,11 @@ describe('Import flow @no-mmi', function () { // choose Import Account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); @@ -406,6 +441,12 @@ describe('Import flow @no-mmi', function () { // choose Connect hardware wallet from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + + // Wait until account list is loaded to mitigate race condition + await driver.waitForSelector({ + text: 'Account 1', + tag: 'span', + }); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 596c7623208d..40bb8c6bd97f 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -120,7 +120,17 @@ const mockServer = }; }), ); - return Promise.all(featureFlagMocks); + const portfolioMock = async () => + await mockServer_ + .forGet('https://portfolio.metamask.io/bridge') + .always() + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }); + return Promise.all([...featureFlagMocks, portfolioMock]); }; export const getBridgeFixtures = ( diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts index 9c74714c82e6..3aa8ecc88ebc 100644 --- a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import FixtureBuilder from '../../../fixture-builder'; import { PRIVATE_KEY, @@ -46,35 +45,21 @@ describe('Alert for insufficient funds @no-mmi', function () { await mintNft(driver); await verifyAlertForInsufficientBalance(driver); - - await verifyConfirmationIsDisabled(driver); }, ); }); }); -async function verifyConfirmationIsDisabled(driver: Driver) { - const confirmButton = await driver.findElement( - '[data-testid="confirm-footer-button"]', - ); - assert.equal(await confirmButton.isEnabled(), false); -} - async function verifyAlertForInsufficientBalance(driver: Driver) { - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); await driver.clickElement('[data-testid="inline-alert"]'); - const alertDescription = await driver.findElement( - '[data-testid="alert-modal__selected-alert"]', - ); - const alertDescriptionText = await alertDescription.getText(); - assert.equal( - alertDescriptionText, - 'You do not have enough ETH in your account to pay for transaction fees.', - ); - await driver.clickElement('[data-testid="alert-modal-close-button"]'); + await displayAlertForInsufficientBalance(driver); + await driver.clickElement('[data-testid="alert-modal-button"]'); } async function mintNft(driver: Driver) { @@ -84,3 +69,10 @@ async function mintNft(driver: Driver) { await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); } + +async function displayAlertForInsufficientBalance(driver: Driver) { + await driver.waitForSelector({ + css: '[data-testid="alert-modal__selected-alert"]', + text: 'You do not have enough ETH in your account to pay for network fees.', + }); +} diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index 8d195656dc44..747ba15872b3 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -1,12 +1,11 @@ -import { strict as assert } from 'assert'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { Suite } from 'mocha'; +import { By } from 'selenium-webdriver'; import { DAPP_HOST_ADDRESS, - WINDOW_TITLES, openDapp, - regularDelayMs, unlockWallet, + WINDOW_TITLES, } from '../../helpers'; import { Driver } from '../../webdriver/driver'; import { withRedesignConfirmationFixtures } from './helpers'; @@ -68,7 +67,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Transaction Sending ETH is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="next-page"]'); @@ -80,7 +79,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Sign Typed Data v3 confirmation is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="previous-page"]'); @@ -98,11 +97,11 @@ describe('Navigation Signature - Different signature types', function (this: Sui await unlockWallet(driver); await openDapp(driver); await queueSignatures(driver); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="confirm-nav__reject-all"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-nav__reject-all"]', + ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await verifyRejectionResults(driver, '#signTypedDataResult'); @@ -114,95 +113,79 @@ describe('Navigation Signature - Different signature types', function (this: Sui }); async function verifySignTypedData(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const message = await driver.findElement({ text: 'Hi, Alice!' }); - - // Verify Sign Typed Data confirmation is displayed - assert.ok(origin, 'origin'); - assert.ok(message, 'message'); + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ text: 'Hi, Alice!' }); } async function verifyRejectionResults(driver: Driver, verifyResultId: string) { - const rejectionResult = await driver.findElement(verifyResultId); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: verifyResultId, + text: 'Error: User rejected the request.', + }); } async function verifySignedTypeV3Confirmation(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const fromAddress = driver.findElement({ + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ css: '.name__value', text: '0xCD2a3...DD826', }); - const toAddress = driver.findElement({ + await driver.waitForSelector({ css: '.name__value', text: '0xbBbBB...bBBbB', }); - const contents = driver.findElement({ text: 'Hello, Bob!' }); - - assert.ok(await origin, 'origin'); - assert.ok(await fromAddress, 'fromAddress'); - assert.ok(await toAddress, 'toAddress'); - assert.ok(await contents, 'contents'); + await driver.waitForSelector({ text: 'Hello, Bob!' }); } async function verifySignedTypeV4Confirmation(driver: Driver) { verifySignedTypeV3Confirmation(driver); - const attachment = driver.findElement({ text: '0x' }); - assert.ok(await attachment, 'attachment'); + await driver.waitForSelector({ text: '0x' }); } async function queueSignatures(driver: Driver) { // There is a race condition which changes the order in which signatures are displayed (#25251) // We fix it deterministically by waiting for an element in the screen for each signature await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Hi, Alice!' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Reject all' }); - + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function queueSignaturesAndTransactions(driver: Driver) { await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); // Delay needed due to a race condition - // To be fixed in https://github.com/MetaMask/metamask-extension/issues/25251 - - await driver.waitUntilXWindowHandles(3); + await driver.waitForSelector({ + tag: 'p', + text: 'Hi, Alice!', + }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#sendButton'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function verifyTransaction( driver: Driver, expectedTransactionType: string, ) { - const transactionType = await driver.findElement( - '.confirm-page-container-summary__action__name', - ); - assert.equal(await transactionType.getText(), expectedTransactionType); + await driver.waitForSelector({ + tag: 'span', + text: expectedTransactionType, + }); } diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index bde56537e43f..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({ @@ -99,6 +98,8 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this await scrollAndConfirmAndAssertConfirm(driver); + await acknowledgeAlert(driver); + await driver.clickElement( '[data-testid="confirm-alert-modal-cancel-button"]', ); @@ -122,8 +123,8 @@ describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this expectedProps: { alert_action_clicked: [], alert_key_clicked: [], - alert_resolved: [], - alert_resolved_count: 0, + alert_resolved: ['requestFrom'], + alert_resolved_count: 1, alert_triggered: ['requestFrom'], alert_triggered_count: 1, alert_visualized: ['requestFrom'], @@ -150,8 +151,10 @@ async function acknowledgeAlert(driver: Driver) { async function verifyAlertIsDisplayed(driver: Driver) { await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElement('[data-testid="inline-alert"]'); } @@ -159,6 +162,8 @@ async function assertVerifiedMessage(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/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index cd27f359aa87..c9c4ca9399f4 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -46,11 +46,6 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await clickHeaderInfoBtn(driver); await assertHeaderInfoBalance(driver); - await assertAccountDetailsMetrics( - driver, - mockedEndpoints as MockedEndpoint[], - 'eth_signTypedData_v4', - ); await copyAddressAndPasteWalletAddress(driver); await assertPastedAddress(driver); @@ -60,6 +55,12 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await scrollAndConfirmAndAssertConfirm(driver); await driver.delay(1000); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); + await assertSignatureConfirmedMetrics({ driver, mockedEndpoints: mockedEndpoints as MockedEndpoint[], @@ -93,11 +94,10 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement('#signPermitResult'); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + tag: 'span', + text: 'Error: User rejected the request.', + }); await assertSignatureRejectedMetrics({ driver, @@ -126,7 +126,7 @@ async function assertInfoValues(driver: Driver) { css: '.name__value', text: '0x5B38D...eddC4', }); - const value = driver.findElement({ text: '3,000' }); + const value = driver.findElement({ text: '<0.000001' }); const nonce = driver.findElement({ text: '0' }); const deadline = driver.findElement({ text: '09 June 3554, 16:53' }); @@ -145,31 +145,32 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signPermitVerify'); - const verifyResult = await driver.findElement('#signPermitResult'); - const verifyResultR = await driver.findElement('#signPermitResultR'); - const verifyResultS = await driver.findElement('#signPermitResultS'); - const verifyResultV = await driver.findElement('#signPermitResultV'); + await driver.waitForSelector({ + css: '#signPermitVerifyResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#signPermitResult', + text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', + }); + + await driver.waitForSelector({ + css: '#signPermitResultR', + text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', + }); + await driver.waitForSelector({ + css: '#signPermitResultS', + text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', + }); + + await driver.waitForSelector({ + css: '#signPermitResultV', + text: 'v: 28', + }); await driver.waitForSelector({ css: '#signPermitVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signPermitVerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', - ); - assert.equal( - await verifyResultR.getText(), - 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', - ); - assert.equal( - await verifyResultS.getText(), - 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', - ); - assert.equal(await verifyResultV.getText(), 'v: 28'); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index 429881bf2f23..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({ @@ -116,17 +115,18 @@ async function assertVerifiedPersonalMessage( await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#personalSignVerify'); - const verifySigUtil = await driver.findElement( - '#personalSignVerifySigUtilResult', - ); await driver.waitForSelector({ css: '#personalSignVerifyECRecoverResult', text: publicAddress, }); - const verifyECRecover = await driver.findElement( - '#personalSignVerifyECRecoverResult', - ); - assert.equal(await verifySigUtil.getText(), publicAddress); - assert.equal(await verifyECRecover.getText(), publicAddress); + await driver.waitForSelector({ + css: '#personalSignVerifySigUtilResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#personalSignVerifyECRecoverResult', + text: publicAddress, + }); } 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 0eb1d2c698b1..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, @@ -96,13 +94,10 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement( - '#signTypedDataV3Result', - ); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: '#signTypedDataV3Result', + text: 'Error: User rejected the request.', + }); }, mockSignatureRejected, ); @@ -144,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 d78acb511ce9..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, @@ -154,18 +152,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4Verify'); - const verifyResult = await driver.findElement('#signTypedDataV4Result'); + await driver.waitForSelector({ + css: '#signTypedDataV4Result', + text: '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', + }); + await driver.waitForSelector({ css: '#signTypedDataV4VerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataV4VerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } 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 358d6b112cfc..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, @@ -116,18 +115,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataVerify'); - const result = await driver.findElement('#signTypedDataResult'); + await driver.waitForSelector({ + css: '#signTypedDataResult', + text: '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', + }); + await driver.waitForSelector({ css: '#signTypedDataVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataVerifyResult', - ); - - assert.equal( - await result.getText(), - '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index 029499f230ec..9b87e5b4e9cc 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; import { MockedEndpoint } from 'mockttp'; +import { Key } from 'selenium-webdriver/lib/input'; import { WINDOW_TITLES, getEventPayloads, @@ -209,17 +210,18 @@ function assertEventPropertiesMatch( export async function clickHeaderInfoBtn(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement( - 'button[data-testid="header-info__account-details-button"]', + + const accountDetailsButton = await driver.findElement( + '[data-testid="header-info__account-details-button"]', ); + await accountDetailsButton.sendKeys(Key.RETURN); } 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/confirmations/transactions/erc20-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts index 60a141144833..baa3638330b6 100644 --- a/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc20-approve-redesign.spec.ts @@ -87,9 +87,10 @@ describe('Confirmation Redesign ERC20 Approve Component', function () { }); }); -async function mocked4Bytes(mockServer: MockttpServer) { +export async function mocked4BytesApprove(mockServer: MockttpServer) { return await mockServer .forGet('https://www.4byte.directory/api/v1/signatures/') + .always() .withQuery({ hex_signature: '0x095ea7b3' }) .thenCallback(() => ({ statusCode: 200, @@ -111,7 +112,7 @@ async function mocked4Bytes(mockServer: MockttpServer) { } async function mocks(server: MockttpServer) { - return [await mocked4Bytes(server)]; + return [await mocked4BytesApprove(server)]; } export async function importTST(driver: Driver) { diff --git a/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts index 2571a69107b3..4eed23b20f44 100644 --- a/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/increase-token-allowance-redesign.spec.ts @@ -108,11 +108,27 @@ function generateFixtureOptionsForEIP1559Tx(mochaContext: Mocha.Context) { }; } +async function createAndAssertIncreaseAllowanceSubmission( + driver: Driver, + newSpendingCap: string, + contractRegistry?: GanacheContractAddressRegistry, +) { + await openDAppWithContract(driver, contractRegistry, SMART_CONTRACTS.HST); + + await createERC20IncreaseAllowanceTransaction(driver); + + await editSpendingCap(driver, newSpendingCap); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, newSpendingCap); +} + async function mocks(server: Mockttp) { return [await mocked4BytesIncreaseAllowance(server)]; } -async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { +export async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { return await mockServer .forGet('https://www.4byte.directory/api/v1/signatures/') .always() @@ -131,7 +147,6 @@ async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { text_signature: 'increaseAllowance(address,uint256)', hex_signature: '0x39509351', bytes_signature: '9P“Q', - test: 'Priya', }, ], }, @@ -139,28 +154,12 @@ async function mocked4BytesIncreaseAllowance(mockServer: Mockttp) { }); } -async function createAndAssertIncreaseAllowanceSubmission( - driver: Driver, - newSpendingCap: string, - contractRegistry?: GanacheContractAddressRegistry, -) { - await openDAppWithContract(driver, contractRegistry, SMART_CONTRACTS.HST); - - await createERC20IncreaseAllowanceTransaction(driver); - - await editSpendingCap(driver, newSpendingCap); - - await scrollAndConfirmAndAssertConfirm(driver); - - await assertChangedSpendingCap(driver, newSpendingCap); -} - async function createERC20IncreaseAllowanceTransaction(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#increaseTokenAllowance'); } -async function editSpendingCap(driver: Driver, newSpendingCap: string) { +export async function editSpendingCap(driver: Driver, newSpendingCap: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement('[data-testid="edit-spending-cap-icon"'); @@ -177,7 +176,7 @@ async function editSpendingCap(driver: Driver, newSpendingCap: string) { await driver.delay(veryLargeDelayMs * 2); } -async function assertChangedSpendingCap( +export async function assertChangedSpendingCap( driver: Driver, newSpendingCap: string, ) { diff --git a/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts new file mode 100644 index 000000000000..ba97d9cda4cd --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/revoke-allowance-redesign.spec.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { MockttpServer } from 'mockttp'; +import { WINDOW_TITLES } from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; +import { scrollAndConfirmAndAssertConfirm } from '../helpers'; +import { mocked4BytesApprove } from './erc20-approve-redesign.spec'; +import { + assertChangedSpendingCap, + editSpendingCap, +} from './increase-token-allowance-redesign.spec'; +import { openDAppWithContract, TestSuiteArguments } from './shared'; + +const { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + withFixtures, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); + +describe('Confirmation Redesign ERC20 Revoke Allowance', function () { + const smartContract = SMART_CONTRACTS.HST; + + describe('Submit an revoke transaction @no-mmi', function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + testSpecificMock: mocks, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createERC20ApproveTransaction(driver); + + const NEW_SPENDING_CAP = '0'; + await editSpendingCap(driver, NEW_SPENDING_CAP); + + await driver.waitForSelector({ + css: 'h2', + text: 'Remove permission', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, NEW_SPENDING_CAP); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + testSpecificMock: mocks, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createERC20ApproveTransaction(driver); + + const NEW_SPENDING_CAP = '0'; + await editSpendingCap(driver, NEW_SPENDING_CAP); + + await driver.waitForSelector({ + css: 'h2', + text: 'Remove permission', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await assertChangedSpendingCap(driver, NEW_SPENDING_CAP); + }, + ); + }); + }); +}); + +async function mocks(server: MockttpServer) { + return [await mocked4BytesApprove(server)]; +} + +async function createERC20ApproveTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#approveTokens'); +} 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/block-explorer.spec.js b/test/e2e/tests/dapp-interactions/block-explorer.spec.js index d9d21e92ee87..0b01aa65aab3 100644 --- a/test/e2e/tests/dapp-interactions/block-explorer.spec.js +++ b/test/e2e/tests/dapp-interactions/block-explorer.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { mockNetworkStateOld } = require('../../../stub/networks'); const { @@ -38,19 +37,17 @@ describe('Block Explorer', function () { await driver.clickElement({ text: 'View on explorer', tag: 'p' }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/address/0x5CfE73b6021E818B776b421B1c4Db2474086a7e1', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/address/0x5CfE73b6021E818B776b421B1c4Db2474086a7e1', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); @@ -82,10 +79,12 @@ describe('Block Explorer', function () { await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); - const [, tst] = await driver.findElements( - '[data-testid="multichain-token-list-button"]', - ); - await tst.click(); + + await driver.clickElement({ + text: 'TST', + tag: 'span', + }); + await driver.clickElement('[data-testid="asset-options__button"]'); await driver.clickElement({ text: 'View Asset in explorer', @@ -93,19 +92,17 @@ describe('Block Explorer', function () { }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/token/0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/token/0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); @@ -143,19 +140,17 @@ describe('Block Explorer', function () { }); // Switch to block explorer - await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); - const body = await driver.findElement( - '[data-testid="empty-page-body"]', - ); + await driver.switchToWindowWithTitle('E2E Test Page'); // Verify block explorer - assert.equal(await body.getText(), 'Empty page by MetaMask'); - assert.equal( - await driver.getCurrentUrl(), - 'https://etherscan.io/tx/0xe5e7b95690f584b8f66b33e31acc6184fea553fa6722d42486a59990d13d5fa2', - ); + await driver.waitForUrl({ + url: 'https://etherscan.io/tx/0xe5e7b95690f584b8f66b33e31acc6184fea553fa6722d42486a59990d13d5fa2', + }); + + await driver.waitForSelector({ + text: 'Empty page by MetaMask', + tag: 'body', + }); }, ); }); 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 2f595138d0cf..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,15 +185,17 @@ 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', + }); }, ); }); - it('should show balance correctly as Fiat', async function () { + it('should show balance correctly in native tokens', async function () { + // In component ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js, after removing useNativeCurrencyAsPrimaryCurrency; + // We will display native balance in the confirm-encryption-public-key.component.js await withFixtures( { dapp: true, @@ -203,7 +203,7 @@ describe('Encrypt Decrypt', function () { .withPermissionControllerConnectedToTestDapp() .withPreferencesController({ preferences: { - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -228,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(), '$42,500.00 USD'); + 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 b9b4b08ca73e..668f93e65dc5 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -14,11 +14,19 @@ const { MetaMetricsEventName, } = require('../../../../shared/constants/metametrics'); -async function mockedDappViewedEndpoint(mockServer) { +async function mockedDappViewedEndpointFirstVisit(mockServer) { return await mockServer .forPost('https://api.segment.io/v1/batch') .withJsonBodyIncluding({ - batch: [{ type: 'track', event: MetaMetricsEventName.DappViewed }], + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: true, + }, + }, + ], }) .thenCallback(() => { return { @@ -27,11 +35,19 @@ async function mockedDappViewedEndpoint(mockServer) { }); } -async function mockPermissionApprovedEndpoint(mockServer) { +async function mockedDappViewedEndpointReVisit(mockServer) { return await mockServer .forPost('https://api.segment.io/v1/batch') .withJsonBodyIncluding({ - batch: [{ type: 'track', event: 'Permissions Approved' }], + batch: [ + { + type: 'track', + event: MetaMetricsEventName.DappViewed, + properties: { + is_first_visit: false, + }, + }, + ], }) .thenCallback(() => { return { @@ -40,20 +56,17 @@ 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', - }); +async function mockPermissionApprovedEndpoint(mockServer) { + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Permissions Approved' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }); } const waitForDappConnected = async (driver) => { @@ -67,7 +80,7 @@ describe('Dapp viewed Event @no-mmi', function () { const validFakeMetricsId = 'fake-metrics-fd20'; it('is not sent when metametrics ID is not valid', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -93,7 +106,7 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to dapp with no account connected', async function () { async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(mockServer)]; + return [await mockedDappViewedEndpointFirstVisit(mockServer)]; } await withFixtures( @@ -125,8 +138,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when opening the dapp in a new tab with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -163,8 +176,8 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when refreshing dapp with one account connected', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -189,10 +202,9 @@ describe('Dapp viewed Event @no-mmi', function () { // refresh dapp await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.refresh(); - const events = await getEventPayloads(driver, mockedEndpoints); - // events are original dapp viewed, new dapp viewed when refresh, and permission approved + // events are original dapp viewed, navigate to dapp, new dapp viewed when refresh, new dapp viewed when navigate and permission approved const dappViewedEventProperties = events[1].properties; assert.equal(dappViewedEventProperties.is_first_visit, false); assert.equal(dappViewedEventProperties.number_of_accounts, 1); @@ -204,10 +216,10 @@ describe('Dapp viewed Event @no-mmi', function () { it('is sent when navigating to a connected dapp', async function () { async function mockSegment(mockServer) { return [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), await mockPermissionApprovedEndpoint(mockServer), ]; } @@ -245,62 +257,11 @@ describe('Dapp viewed Event @no-mmi', function () { ); }); - it('is sent when connecting dapp with two accounts', async function () { - async function mockSegment(mockServer) { - return [await mockedDappViewedEndpoint(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 [ - await mockedDappViewedEndpoint(mockServer), - await mockedDappViewedEndpoint(mockServer), + await mockedDappViewedEndpointFirstVisit(mockServer), + await mockedDappViewedEndpointReVisit(mockServer), ]; } @@ -344,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/delete-metametrics-data.spec.ts b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts new file mode 100644 index 000000000000..308ff8508d0a --- /dev/null +++ b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts @@ -0,0 +1,246 @@ +import { strict as assert } from 'assert'; +import { MockedEndpoint, Mockttp } from 'mockttp'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + withFixtures, + getEventPayloads, + unlockWallet, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { TestSuiteArguments } from '../confirmations/transactions/shared'; +import { WebElementWithWaitForElementState } from '../../webdriver/types'; + +const selectors = { + accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', + globalMenuSettingsButton: '[data-testid="global-menu-settings"]', + securityAndPrivacySettings: { text: 'Security & privacy', tag: 'div' }, + experimentalSettings: { text: 'Experimental', tag: 'div' }, + deletMetaMetricsSettings: '[data-testid="delete-metametrics-data-button"]', + deleteMetaMetricsDataButton: { + text: 'Delete MetaMetrics data', + tag: 'button', + }, + clearButton: { text: 'Clear', tag: 'button' }, + backButton: '[data-testid="settings-back-button"]', +}; + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param mockServer + * @returns + */ +const mockSegment = async (mockServer: Mockttp) => { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { type: 'track', event: 'Delete MetaMetrics Data Request Submitted' }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://metametrics.metamask.test/regulations/sources/test') + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .withBodyIncluding( + JSON.stringify({ + regulationType: 'DELETE_ONLY', + subjectType: 'USER_ID', + subjectIds: ['fake-metrics-id'], + }), + ) + .thenCallback(() => ({ + statusCode: 200, + json: { data: { regulateId: 'fake-delete-regulation-id' } }, + })), + await mockServer + .forGet( + 'https://metametrics.metamask.test/regulations/fake-delete-regulation-id', + ) + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .thenCallback(() => ({ + statusCode: 200, + json: { + data: { + regulation: { + overallStatus: 'FINISHED', + }, + }, + }, + })), + ]; +}; +/** + * Scenarios: + * 1. Deletion while Metrics is Opted in. + * 2. Deletion while Metrics is Opted out. + * 3. Deletion when user never opted for metrics. + */ +describe('Delete MetaMetrics Data @no-mmi', function (this: Suite) { + it('while user has opted in for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = + await driver.findClickableElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButtonRefreshed.isEnabled(), + true, + 'Delete MetaMetrics data button is enabled', + ); + }, + ); + }); + it('while user has opted out for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 2); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButtonRefreshed as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + }, + ); + }); + it('when the user has never opted in for metrics', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + await driver.findElement(selectors.deletMetaMetricsSettings); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButton.isEnabled(), + false, + 'Delete MetaMetrics data button is disabled', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index ee22bdd93815..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: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -278,7 +278,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -319,7 +319,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -365,7 +365,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryMigratorError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -426,7 +426,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryInvariantMigrationError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -475,7 +475,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -521,7 +521,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -585,7 +585,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -621,7 +621,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -656,7 +656,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -702,7 +702,7 @@ describe('Sentry errors', function () { title: this.test.fullTitle(), testSpecificMock: mockSentryTestError, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -766,7 +766,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -810,7 +810,7 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryTestError, ignoredConsoleErrors: ['TestError'], manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, ganacheServer, mockedEndpoint }) => { @@ -898,7 +898,7 @@ describe('Sentry errors', function () { ganacheOptions, title: this.test.fullTitle(), manifestFlags: { - 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 b5666a9078b8..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: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -60,7 +60,7 @@ describe('Sessions', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentrySession, manifestFlags: { - 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 c133de6128ca..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 @@ -38,6 +38,7 @@ "showAccountBanner": true, "trezorModel": null, "onboardingDate": null, + "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -62,17 +63,13 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionSupport": "boolean", - "srcNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - }, - "destNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - } - } + "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, + "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "CronjobController": { "jobs": "object" }, @@ -180,6 +177,7 @@ "permissionActivityLog": "object" }, "PreferencesController": { + "selectedAddress": "string", "useBlockie": false, "useNonceField": false, "usePhishDetect": true, @@ -211,12 +209,14 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "tokenSortConfig": "object", + "shouldShowAggregatedBalancePopover": "boolean" }, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", @@ -228,8 +228,7 @@ "useExternalNameSources": "boolean", "useTransactionSimulations": true, "enableMV3TimestampSave": true, - "useExternalServices": "boolean", - "selectedAddress": "string" + "useExternalServices": "boolean" }, "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, @@ -305,8 +304,10 @@ }, "TxController": { "methodData": "object", + "submitHistory": "object", "transactions": "object", - "lastFetchedBlockNumbers": "object" + "lastFetchedBlockNumbers": "object", + "submitHistory": "object" }, "UserOperationController": { "userOperations": "object" }, "UserStorageController": { 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 dfee54fbd6cb..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 @@ -17,7 +17,6 @@ "internalAccounts": { "accounts": "object", "selectedAccount": "string" }, "transactions": "object", "networkConfigurations": "object", - "networkConfigurationsByChainId": "object", "addressBook": "object", "confirmationExchangeRates": {}, "pendingTokens": "object", @@ -32,12 +31,15 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, + "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", "redesignedConfirmationsEnabled": true, "redesignedTransactionsEnabled": "boolean", - "showMultiRpcModal": "boolean" + "tokenSortConfig": "object", + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, @@ -70,6 +72,7 @@ "showAccountBanner": true, "trezorModel": null, "onboardingDate": null, + "lastViewedUserSurvey": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -95,7 +98,9 @@ "status": "available" } }, + "networkConfigurationsByChainId": "object", "keyrings": "object", + "selectedAddress": "string", "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, @@ -127,16 +132,15 @@ "useTransactionSimulations": true, "enableMV3TimestampSave": true, "useExternalServices": "boolean", - "selectedAddress": "string", "metaMetricsId": "fake-metrics-id", "marketingCampaignCookieId": null, - "metaMetricsDataDeletionId": null, - "metaMetricsDataDeletionTimestamp": 0, "eventsBeforeMetricsOptIn": "object", "traits": "object", "previousUserTraits": "object", "fragments": "object", "segmentApiCalls": "object", + "metaMetricsDataDeletionId": null, + "metaMetricsDataDeletionTimestamp": 0, "currentCurrency": "usd", "alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true }, "unconnectedAccountAlertShownOrigins": "object", @@ -177,6 +181,7 @@ "logs": "object", "methodData": "object", "lastFetchedBlockNumbers": "object", + "submitHistory": "object", "fiatCurrency": "usd", "rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } }, "cryptocurrencies": ["btc"], @@ -194,6 +199,7 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", + "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", @@ -248,17 +254,13 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionSupport": "boolean", - "srcNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - }, - "destNetworkAllowlist": { - "0": "string", - "1": "string", - "2": "string" - } - } + "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, + "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index b6354922add0..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, @@ -112,11 +112,13 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -152,7 +154,11 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "SubjectMetadataController": { "subjectMetadata": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 51910b2057a7..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, @@ -112,11 +112,13 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, + "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, - "showConfirmationAdvancedDetails": false, "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "showMultiRpcModal": "boolean" + "showConfirmationAdvancedDetails": false, + "tokenSortConfig": "object", + "showMultiRpcModal": "boolean", + "shouldShowAggregatedBalancePopover": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -161,7 +163,11 @@ "1": "string", "2": "string" } - } + }, + "destTokens": {}, + "destTopAssets": {}, + "srcTokens": {}, + "srcTopAssets": {} } }, "TransactionController": { "transactions": "object" }, diff --git a/test/e2e/tests/metrics/traces.spec.ts b/test/e2e/tests/metrics/traces.spec.ts index 62c4d7da9219..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: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -73,7 +73,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryCustomTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -95,7 +95,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { @@ -117,7 +117,7 @@ describe('Traces', function () { title: this.test?.fullTitle(), testSpecificMock: mockSentryAutomatedTrace, manifestFlags: { - doNotForceSentryForThisTest: true, + sentry: { forceEnable: false }, }, }, async ({ driver, mockedEndpoint }) => { diff --git a/test/e2e/tests/multichain/asset-picker-send.spec.ts b/test/e2e/tests/multichain/asset-picker-send.spec.ts index 5accb14c6074..a071bec9426d 100644 --- a/test/e2e/tests/multichain/asset-picker-send.spec.ts +++ b/test/e2e/tests/multichain/asset-picker-send.spec.ts @@ -71,7 +71,7 @@ describe('AssetPickerSendFlow @no-mmi', function () { ) ).getText(); - assert.equal(tokenListValue, '25 ETH'); + assert.equal(tokenListValue, '$250,000.00'); const tokenListSecondaryValue = await ( await driver.findElement( @@ -79,7 +79,7 @@ describe('AssetPickerSendFlow @no-mmi', function () { ) ).getText(); - assert.equal(tokenListSecondaryValue, '$250,000.00'); + assert.equal(tokenListSecondaryValue, '25 ETH'); // Search for CHZ const searchInputField = await driver.waitForSelector( 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/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 7b03d411d6ec..c9fa95f986e9 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -397,7 +397,13 @@ describe('MultiRpc:', function (this: Suite) { // go to advanced settigns await driver.clickElement({ - text: 'Advanced configuration', + text: 'Manage default settings', + }); + + await driver.delay(regularDelayMs); + + await driver.clickElement({ + text: 'General', }); // open edit modal @@ -419,6 +425,27 @@ describe('MultiRpc:', function (this: Suite) { tag: 'button', }); + await driver.delay(regularDelayMs); + await driver.waitForSelector('[data-testid="category-back-button"]'); + await driver.clickElement('[data-testid="category-back-button"]'); + + await driver.waitForSelector( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + await driver.clickElement({ text: 'Done', tag: 'button', @@ -433,7 +460,7 @@ describe('MultiRpc:', function (this: Suite) { true, '“Arbitrum One” was successfully edited!', ); - + // Ensures popover backround doesn't kill test await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="network-display"]'); 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/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 1aa716953703..1b15dba5ddd7 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -21,6 +21,7 @@ const { regularDelayMs, unlockWallet, tinyDelayMs, + largeDelayMs, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { @@ -204,7 +205,7 @@ describe('MetaMask onboarding @no-mmi', function () { // Verify site assert.equal( await driver.isElementPresent({ - text: 'Wallet creation successful', + text: 'Your wallet is ready', tag: 'h2', }), true, @@ -270,76 +271,122 @@ describe('MetaMask onboarding @no-mmi', function () { }, async ({ driver, secondaryGanacheServer }) => { - await driver.navigate(); - await importSRPOnboardingFlow( - driver, - TEST_SEED_PHRASE, - WALLET_PASSWORD, - ); + try { + await driver.navigate(); + await importSRPOnboardingFlow( + driver, + TEST_SEED_PHRASE, + WALLET_PASSWORD, + ); - // Add custom network localhost 8546 during onboarding - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); - await driver.clickElement({ text: 'Add a network' }); - await driver.waitForSelector( - '.multichain-network-list-menu-content-wrapper__dialog', - ); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); - await driver.fill( - '[data-testid="network-form-network-name"]', - networkName, - ); - await driver.fill( - '[data-testid="network-form-chain-id"]', - chainId.toString(), - ); - await driver.fill( - '[data-testid="network-form-ticker-input"]', - currencySymbol, - ); + await driver.clickElement({ + text: 'General', + }); + await driver.delay(largeDelayMs); + await driver.clickElement({ text: 'Add a network' }); - // Add rpc url - const rpcUrlInputDropDown = await driver.waitForSelector( - '[data-testid="test-add-rpc-drop-down"]', - ); - await rpcUrlInputDropDown.click(); - await driver.delay(tinyDelayMs); - await driver.clickElement({ - text: 'Add RPC URL', - tag: 'button', - }); - const rpcUrlInput = await driver.waitForSelector( - '[data-testid="rpc-url-input-test"]', - ); - await rpcUrlInput.clear(); - await rpcUrlInput.sendKeys(networkUrl); - await driver.clickElement({ - text: 'Add URL', - tag: 'button', - }); + await driver.waitForSelector( + '.multichain-network-list-menu-content-wrapper__dialog', + ); - await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.clickElement({ - text: 'Done', - tag: 'button', - }); + await driver.fill( + '[data-testid="network-form-network-name"]', + networkName, + ); + await driver.fill( + '[data-testid="network-form-chain-id"]', + chainId.toString(), + ); + await driver.fill( + '[data-testid="network-form-ticker-input"]', + currencySymbol, + ); - await driver.clickElement('.mm-picker-network'); - await driver.clickElement( - `[data-rbd-draggable-id="${toHex(chainId)}"]`, - ); + // Add rpc url + const rpcUrlInputDropDown = await driver.waitForSelector( + '[data-testid="test-add-rpc-drop-down"]', + ); + await driver.delay(tinyDelayMs); + await rpcUrlInputDropDown.click(); + await driver.delay(tinyDelayMs); + await driver.clickElement({ + text: 'Add RPC URL', + tag: 'button', + }); + const rpcUrlInput = await driver.waitForSelector( + '[data-testid="rpc-url-input-test"]', + ); + await rpcUrlInput.clear(); + await rpcUrlInput.sendKeys(networkUrl); + await driver.clickElement({ + text: 'Add URL', + tag: 'button', + }); + + await driver.clickElement({ text: 'Save', tag: 'button' }); + + await driver.delay(largeDelayMs); + await driver.waitForSelector('[data-testid="category-back-button"]'); + const generalBackButton = await driver.waitForSelector( + '[data-testid="category-back-button"]', + ); + await generalBackButton.click(); - // Check localhost 8546 is selected and its balance value is correct - await driver.findElement({ - css: '[data-testid="network-display"]', - text: networkName, - }); + await driver.delay(largeDelayMs); + + await driver.waitForSelector( + '[data-testid="privacy-settings-back-button"]', + ); + const defaultSettingsBackButton = await driver.findElement( + '[data-testid="privacy-settings-back-button"]', + ); + await defaultSettingsBackButton.click(); + + await driver.delay(largeDelayMs); + + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + await driver.delay(largeDelayMs); - await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); + await driver.clickElement({ + text: 'Done', + tag: 'button', + }); + + await driver.clickElement('.mm-picker-network'); + await driver.clickElement( + `[data-rbd-draggable-id="${toHex(chainId)}"]`, + ); + await driver.delay(largeDelayMs); + // Check localhost 8546 is selected and its balance value is correct + await driver.findElement({ + css: '[data-testid="network-display"]', + text: networkName, + }); + + await locateAccountBalanceDOM(driver, secondaryGanacheServer[0]); + } catch (error) { + console.error('Error in test:', error); + throw error; + } }, ); }); - it('User can turn off basic functionality in advanced configurations', async function () { + it('User can turn off basic functionality in default settings', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -354,13 +401,25 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + + await driver.clickElement('[data-testid="onboarding-complete-done"]'); + await driver.clickElement('[data-testid="pin-extension-next"]'); + await driver.clickElement('[data-testid="pin-extension-done"]'); + // Check that the 'basic functionality is off' banner is displayed on the home screen after onboarding completion await driver.waitForSelector({ text: 'Basic functionality is off', diff --git a/test/e2e/tests/phishing-controller/phishing-detection.spec.js b/test/e2e/tests/phishing-controller/phishing-detection.spec.js index 444c98900026..ac9a6d8461d2 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -208,10 +208,9 @@ describe('Phishing Detection', function () { await driver.findElement({ text: `Empty page by ${BlockProvider.MetaMask}`, }); - assert.equal( - await driver.getCurrentUrl(), - `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, - ); + await driver.waitForUrl({ + url: `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, + }); }, ); }); @@ -445,11 +444,12 @@ describe('Phishing Detection', function () { await driver.openNewURL(blockedUrl); // check that the redirect was ultimately _not_ followed and instead // went to our "MetaMask Phishing Detection" site - assert.equal( - await driver.getCurrentUrl(), - // http://localhost:9999 is the Phishing Warning page - `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, - ); + + await driver.waitForUrl({ + url: + // http://localhost:9999 is the Phishing Warning page + `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, + }); }); } }); diff --git a/test/e2e/tests/portfolio/portfolio-site.spec.js b/test/e2e/tests/portfolio/portfolio-site.spec.js index ff4c7c363a71..cba9c0452522 100644 --- a/test/e2e/tests/portfolio/portfolio-site.spec.js +++ b/test/e2e/tests/portfolio/portfolio-site.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, unlockWallet, @@ -42,10 +41,9 @@ describe('Portfolio site', function () { await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); // Verify site - const currentUrl = await driver.getCurrentUrl(); - const expectedUrl = - 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false'; - assert.equal(currentUrl, expectedUrl); + await driver.waitForUrl({ + url: 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false', + }); }, ); }); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index aef2f16728de..062a0345a39a 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -5,6 +5,8 @@ const { importSRPOnboardingFlow, WALLET_PASSWORD, tinyDelayMs, + regularDelayMs, + largeDelayMs, defaultGanacheOptions, } = require('../../helpers'); const { METAMASK_STALELIST_URL } = require('../phishing-controller/helpers'); @@ -41,7 +43,7 @@ async function mockApis(mockServer) { } describe('MetaMask onboarding @no-mmi', function () { - it('should prevent network requests to basic functionality endpoints when the basica functionality toggle is off', async function () { + it('should prevent network requests to basic functionality endpoints when the basic functionality toggle is off', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -57,15 +59,36 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); + + await driver.delay(regularDelayMs); + await driver.clickElement( '[data-testid="basic-functionality-toggle"] .toggle-button', ); + await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.delay(regularDelayMs); + await driver.clickElement('[data-testid="category-item-Assets"]'); + await driver.delay(regularDelayMs); await driver.clickElement( '[data-testid="currency-rate-check-toggle"] .toggle-button', ); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.delay(regularDelayMs); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.delay(regularDelayMs); + + await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElement('[data-testid="pin-extension-next"]'); await driver.clickElement({ text: 'Done', tag: 'button' }); await driver.clickElement('[data-testid="network-display"]'); @@ -90,7 +113,7 @@ describe('MetaMask onboarding @no-mmi', function () { ); }); - it('should not prevent network requests to basic functionality endpoints when the basica functionality toggle is on', async function () { + it('should not prevent network requests to basic functionality endpoints when the basic functionality toggle is on', async function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), @@ -106,19 +129,29 @@ describe('MetaMask onboarding @no-mmi', function () { WALLET_PASSWORD, ); - await driver.clickElement({ text: 'Advanced configuration', tag: 'a' }); - + await driver.clickElement({ + text: 'Manage default settings', + tag: 'button', + }); + await driver.clickElement('[data-testid="category-item-General"]'); + await driver.delay(largeDelayMs); + await driver.clickElement('[data-testid="category-back-button"]'); + await driver.delay(largeDelayMs); + await driver.clickElement( + '[data-testid="privacy-settings-back-button"]', + ); + await driver.delay(largeDelayMs); + await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElement('[data-testid="pin-extension-next"]'); await driver.clickElement({ text: 'Done', tag: 'button' }); await driver.clickElement('[data-testid="network-display"]'); await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'p' }); - await driver.delay(tinyDelayMs); // Wait until network is fully switched and refresh tokens before asserting to mitigate flakiness await driver.assertElementNotPresent('.loading-overlay'); await driver.clickElement('[data-testid="refresh-list-button"]'); - for (let i = 0; i < mockedEndpoints.length; i += 1) { const requests = await mockedEndpoints[i].getSeenRequests(); assert.equal( 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 ee7200d8a59b..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 @@ -1,14 +1,12 @@ const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, + DAPP_ONE_URL, + DAPP_URL, + defaultGanacheOptions, openDapp, unlockWallet, - DAPP_URL, - DAPP_ONE_URL, - regularDelayMs, WINDOW_TITLES, - defaultGanacheOptions, - switchToNotificationWindow, + withFixtures, } = require('../../helpers'); describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { @@ -51,20 +49,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - 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( @@ -80,9 +69,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { 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); @@ -91,20 +77,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - 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"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -113,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 @@ -121,23 +98,28 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ + text: 'Use your enabled networks', + tag: 'p', + }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); - await driver.findClickableElements({ - text: 'Switch network', - tag: 'button', - }); + 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); // Wait for switch confirmation to close then tx confirmation to show. - await driver.waitUntilXWindowHandles(3); - await driver.delay(regularDelayMs); + // There is an extra window appearing and disappearing + // so we leave this delay until the issue is fixed (#27360) + await driver.delay(5000); - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. await driver.findElement({ @@ -145,7 +127,10 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { text: 'Localhost 8546', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); // Switch back to the extension await driver.switchToWindowWithTitle( @@ -206,20 +191,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await switchToNotificationWindow(driver); - - 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.switchToWindowWithTitle( @@ -235,9 +211,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { 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); @@ -246,20 +219,11 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { await driver.findClickableElement({ text: 'Connect', tag: 'button' }); await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); - - 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"]', }); await driver.switchToWindowWithUrl(DAPP_URL); @@ -268,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 @@ -276,23 +240,26 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { `window.ethereum.request(${switchEthereumChainRequest})`, ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ + text: 'Use your enabled networks', + tag: 'p', + }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); await driver.clickElement('#sendButton'); - await switchToNotificationWindow(driver, 4); - await driver.findClickableElements({ - text: 'Cancel', - tag: 'button', - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Cancel', tag: 'button' }); + await driver.switchToWindowWithUrl(DAPP_ONE_URL); // Wait for switch confirmation to close then tx confirmation to show. - await driver.waitUntilXWindowHandles(3); - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver, 4); + // There is an extra window appearing and disappearing + // so we leave this delay until the issue is fixed (#27360) + await driver.delay(5000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Check correct network on the send confirmation. await driver.findElement({ @@ -300,7 +267,10 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { text: 'Localhost 8546', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); // Switch back to the extension await driver.switchToWindowWithTitle( 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/responsive-ui/metamask-responsive-ui.spec.js b/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js index 6afdb9062ac3..446d579630bf 100644 --- a/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/responsive-ui/metamask-responsive-ui.spec.js @@ -2,10 +2,10 @@ const { strict: assert } = require('assert'); const { TEST_SEED_PHRASE_TWO, defaultGanacheOptions, - withFixtures, locateAccountBalanceDOM, + logInWithBalanceValidation, openActionMenuAndStartSendFlow, - unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -123,10 +123,8 @@ describe('MetaMask Responsive UI', function () { ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), }, - async ({ driver }) => { - await unlockWallet(driver); - - await driver.delay(1000); + async ({ driver, ganacheServer }) => { + await logInWithBalanceValidation(driver, ganacheServer); // Send ETH from inside MetaMask // starts to send a transaction @@ -140,9 +138,13 @@ describe('MetaMask Responsive UI', function () { const inputValue = await inputAmount.getProperty('value'); assert.equal(inputValue, '1'); - - // confirming transcation await driver.clickElement({ text: 'Continue', tag: 'button' }); + + // wait for transaction value to be rendered and confirm + await driver.waitForSelector({ + css: '.currency-display-component__text', + text: '1.000042', + }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); // finds the transaction in the transactions list diff --git a/test/e2e/tests/settings/4byte-directory.spec.js b/test/e2e/tests/settings/4byte-directory.spec.js index 2874118c3a28..483ff1e0149a 100644 --- a/test/e2e/tests/settings/4byte-directory.spec.js +++ b/test/e2e/tests/settings/4byte-directory.spec.js @@ -1,12 +1,10 @@ -const { strict: assert } = require('assert'); const FixtureBuilder = require('../../fixture-builder'); const { - withFixtures, + logInWithBalanceValidation, openDapp, - unlockWallet, openMenuSafe, - largeDelayMs, - veryLargeDelayMs, + unlockWallet, + withFixtures, WINDOW_TITLES, } = require('../../helpers'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); @@ -27,27 +25,23 @@ describe('4byte setting', function () { 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.delay(largeDelayMs); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', + await driver.waitForSelector({ + tag: 'span', text: 'Deposit', }); - assert.equal(await actionElement.getText(), 'DEPOSIT'); + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Contract interaction', + }); }, ); }); @@ -83,28 +77,18 @@ describe('4byte setting', function () { await openDapp(driver, contractAddress); // wait for deployed contract, calls and confirms a contract method where ETH is sent - await driver.findClickableElement('#depositButton'); await driver.clickElement('#depositButton'); - await driver.waitForSelector({ - css: 'span', - text: 'Deposit initiated', - }); - - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const contractInteraction = 'Contract interaction'; - const actionElement = await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', - text: contractInteraction, + + await driver.assertElementNotPresent({ + tag: 'span', + text: 'Deposit', + }); + await driver.waitForSelector({ + tag: 'span', + text: 'Contract interaction', }); - // We add a delay here to wait for any potential UI changes - await driver.delay(veryLargeDelayMs); - // css text-transform: uppercase is applied to the text - assert.equal( - await actionElement.getText(), - contractInteraction.toUpperCase(), - ); }, ); }); diff --git a/test/e2e/tests/settings/account-token-list.spec.js b/test/e2e/tests/settings/account-token-list.spec.js index 0fae71ae1d85..9e4822d0dbbc 100644 --- a/test/e2e/tests/settings/account-token-list.spec.js +++ b/test/e2e/tests/settings/account-token-list.spec.js @@ -3,6 +3,7 @@ const { withFixtures, defaultGanacheOptions, logInWithBalanceValidation, + unlockWallet, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -41,26 +42,18 @@ describe('Settings', function () { it('Should match the value of token list item and account list item for fiat conversion', async function () { await withFixtures( { - fixtures: new FixtureBuilder().withConversionRateEnabled().build(), + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withShowFiatTestnetEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), }, - async ({ driver, ganacheServer }) => { - await logInWithBalanceValidation(driver, ganacheServer); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ - text: 'General', - tag: 'div', - }); - await driver.clickElement({ text: 'Fiat', tag: 'label' }); + async ({ driver }) => { + await unlockWallet(driver); - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); + await driver.clickElement('[data-testid="popover-close"]'); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); @@ -70,7 +63,6 @@ describe('Settings', function () { ); await driver.delay(1000); assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); - await driver.clickElement('[data-testid="account-menu-icon"]'); const accountTokenValue = await driver.waitForSelector( '.multichain-account-list-item .multichain-account-list-item__asset', diff --git a/test/e2e/tests/settings/address-book.spec.js b/test/e2e/tests/settings/address-book.spec.js index c784ce3daa3b..e81bd7c544aa 100644 --- a/test/e2e/tests/settings/address-book.spec.js +++ b/test/e2e/tests/settings/address-book.spec.js @@ -5,6 +5,7 @@ const { withFixtures, logInWithBalanceValidation, openActionMenuAndStartSendFlow, + openMenuSafe, unlockWallet, } = require('../../helpers'); const { shortenAddress } = require('../../../../ui/helpers/utils/util'); @@ -92,10 +93,8 @@ describe('Address Book', function () { }, async ({ driver }) => { await unlockWallet(driver); + await openMenuSafe(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Contacts', tag: 'div' }); await driver.clickElement({ @@ -159,9 +158,8 @@ describe('Address Book', function () { async ({ driver }) => { await unlockWallet(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Contacts', tag: 'div' }); diff --git a/test/e2e/tests/settings/auto-lock.spec.js b/test/e2e/tests/settings/auto-lock.spec.js index bded29fa5f39..7d7a159d4a1b 100644 --- a/test/e2e/tests/settings/auto-lock.spec.js +++ b/test/e2e/tests/settings/auto-lock.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -17,9 +18,7 @@ describe('Auto-Lock Timer', function () { async ({ driver }) => { await unlockWallet(driver); // Set Auto Lock Timer - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); const sixSecsInMins = '0.1'; diff --git a/test/e2e/tests/settings/backup-restore.spec.js b/test/e2e/tests/settings/backup-restore.spec.js index 02a36b638884..41186e70cd23 100644 --- a/test/e2e/tests/settings/backup-restore.spec.js +++ b/test/e2e/tests/settings/backup-restore.spec.js @@ -2,10 +2,11 @@ const { strict: assert } = require('assert'); const { promises: fs } = require('fs'); const { - defaultGanacheOptions, - withFixtures, createDownloadFolder, + defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -31,10 +32,10 @@ const getBackupJson = async () => { try { const backup = `${downloadsFolder}/${userDataFileName}`; - await fs.access(backup); const contents = await fs.readFile(backup); return JSON.parse(contents.toString()); } catch (e) { + console.log('Error reading the backup file', e); return null; } }; @@ -56,9 +57,8 @@ describe('Backup and Restore', function () { await unlockWallet(driver); // Download user settings - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement('[data-testid="export-data-button"]'); diff --git a/test/e2e/tests/settings/change-language.spec.ts b/test/e2e/tests/settings/change-language.spec.ts index aafb36059c9b..1bd9915a33da 100644 --- a/test/e2e/tests/settings/change-language.spec.ts +++ b/test/e2e/tests/settings/change-language.spec.ts @@ -16,8 +16,8 @@ const selectors = { ethOverviewSend: '[data-testid="eth-overview-send"]', ensInput: '[data-testid="ens-input"]', nftsTab: '[data-testid="account-overview__nfts-tab"]', - labelSpanish: { tag: 'span', text: 'Idioma actual' }, - currentLanguageLabel: { tag: 'span', text: 'Current language' }, + labelSpanish: { tag: 'p', text: 'Idioma actual' }, + currentLanguageLabel: { tag: 'p', text: 'Current language' }, advanceText: { text: 'Avanceret', tag: 'div' }, waterText: '[placeholder="Søg"]', headerTextDansk: { text: 'Indstillinger', tag: 'h3' }, diff --git a/test/e2e/tests/settings/clear-activity.spec.js b/test/e2e/tests/settings/clear-activity.spec.js index 34a23aecb173..781363bf42d3 100644 --- a/test/e2e/tests/settings/clear-activity.spec.js +++ b/test/e2e/tests/settings/clear-activity.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -38,9 +39,8 @@ describe('Clear account activity', function () { }); // Clear activity and nonce data - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement({ diff --git a/test/e2e/tests/settings/ipfs-ens-resolution.spec.js b/test/e2e/tests/settings/ipfs-ens-resolution.spec.js index e706a3f41e31..450ec649d809 100644 --- a/test/e2e/tests/settings/ipfs-ens-resolution.spec.js +++ b/test/e2e/tests/settings/ipfs-ens-resolution.spec.js @@ -1,4 +1,9 @@ -const { withFixtures, tinyDelayMs, unlockWallet } = require('../../helpers'); +const { + openMenuSafe, + tinyDelayMs, + unlockWallet, + withFixtures, +} = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); describe('Settings', function () { @@ -64,9 +69,8 @@ describe('Settings', function () { await unlockWallet(driver); // goes to the settings screen - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); diff --git a/test/e2e/tests/settings/ipfs-toggle.spec.js b/test/e2e/tests/settings/ipfs-toggle.spec.js index 045c51496853..cd078b587d54 100644 --- a/test/e2e/tests/settings/ipfs-toggle.spec.js +++ b/test/e2e/tests/settings/ipfs-toggle.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { - withFixtures, defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); @@ -20,10 +21,8 @@ describe('Settings', function () { }, async ({ driver }) => { await unlockWallet(driver); + await openMenuSafe(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); diff --git a/test/e2e/tests/settings/localization.spec.js b/test/e2e/tests/settings/localization.spec.js index 707cc120e578..57dbfd5f68cf 100644 --- a/test/e2e/tests/settings/localization.spec.js +++ b/test/e2e/tests/settings/localization.spec.js @@ -17,6 +17,7 @@ describe('Localization', function () { .withPreferencesController({ preferences: { showFiatInTestnets: true, + showNativeTokenAsMainBalance: false, }, }) .build(), @@ -26,15 +27,13 @@ describe('Localization', function () { async ({ driver }) => { await unlockWallet(driver); - const secondaryBalance = await driver.findElement( - '[data-testid="eth-overview__secondary-currency"]', + // After the removal of displaying secondary currency in coin-overview.tsx, we will test localization on main balance with showNativeTokenAsMainBalance = false + const primaryBalance = await driver.findElement( + '[data-testid="eth-overview__primary-currency"]', ); - const secondaryBalanceText = await secondaryBalance.getText(); - const [fiatAmount, fiatUnit] = secondaryBalanceText - .trim() - .split(/\s+/u); - assert.ok(fiatAmount.startsWith('₱')); - assert.equal(fiatUnit, 'PHP'); + const balanceText = await primaryBalance.getText(); + assert.ok(balanceText.startsWith('₱')); + assert.ok(balanceText.endsWith('PHP')); }, ); }); diff --git a/test/e2e/tests/settings/settings-general.spec.js b/test/e2e/tests/settings/settings-general.spec.js index dafe32ba9bea..5e75c857a7f8 100644 --- a/test/e2e/tests/settings/settings-general.spec.js +++ b/test/e2e/tests/settings/settings-general.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { defaultGanacheOptions, - withFixtures, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -18,9 +19,8 @@ describe('Settings', function () { await unlockWallet(driver); // goes to the settings screen - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); // finds the jazzicon toggle turned on diff --git a/test/e2e/tests/settings/settings-search.spec.js b/test/e2e/tests/settings/settings-search.spec.js index 7a9207dcbf9f..bf27c591bc29 100644 --- a/test/e2e/tests/settings/settings-search.spec.js +++ b/test/e2e/tests/settings/settings-search.spec.js @@ -9,7 +9,7 @@ const FixtureBuilder = require('../../fixture-builder'); describe('Settings Search', function () { const settingsSearch = { - general: 'Primary currency', + general: 'Show native token as main balance', advanced: 'State logs', contacts: 'Contacts', security: 'Reveal Secret', @@ -122,32 +122,6 @@ describe('Settings Search', function () { }, ); }); - it('should find element inside the Alerts tab', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - await openMenuSafe(driver); - - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.fill('#search-settings', settingsSearch.alerts); - - // Check if element redirects to the correct page - const page = 'Alerts'; - await driver.clickElement({ text: page, tag: 'span' }); - assert.equal( - await driver.isElementPresent({ text: page, tag: 'div' }), - true, - `${settingsSearch.alerts} item does not redirect to ${page} view`, - ); - }, - ); - }); it('should find element inside the Experimental tab', async function () { await withFixtures( { diff --git a/test/e2e/tests/settings/show-native-as-main-balance.spec.ts b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts new file mode 100644 index 000000000000..d81e590cc5db --- /dev/null +++ b/test/e2e/tests/settings/show-native-as-main-balance.spec.ts @@ -0,0 +1,240 @@ +import { strict as assert } from 'assert'; +import { expect } from '@playwright/test'; +import { + withFixtures, + defaultGanacheOptions, + logInWithBalanceValidation, + unlockWallet, + getEventPayloads, +} from '../../helpers'; +import { MockedEndpoint, Mockttp } from '../../mock-e2e'; +import { Driver } from '../../webdriver/driver'; + +import FixtureBuilder from '../../fixture-builder'; + +async function mockSegment(mockServer: Mockttp) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Show native token as main balance' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +describe('Settings: Show native token as main balance', function () { + it('Should show balance in crypto when toggle is on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().withConversionRateDisabled().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ + driver, + ganacheServer, + }: { + driver: Driver; + ganacheServer: unknown; + }) => { + await logInWithBalanceValidation(driver, ganacheServer); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + const tokenValue = '25 ETH'; + const tokenListAmount = await driver.findElement( + '[data-testid="multichain-token-list-item-value"]', + ); + await driver.waitForNonEmptyElement(tokenListAmount); + assert.equal(await tokenListAmount.getText(), tokenValue); + }, + ); + }); + + it('Should show balance in fiat when toggle is OFF', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover + await driver.clickElement('[data-testid="popover-close"]'); + + await driver.clickElement( + '[data-testid="account-overview__asset-tab"]', + ); + + const tokenListAmount = await driver.findElement( + '.eth-overview__primary-container', + ); + assert.equal(await tokenListAmount.getText(), '$42,500.00\nUSD'); + }, + ); + }); + + it('Should not show popover twice', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withConversionRateEnabled() + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + + await driver.clickElement({ + text: 'Advanced', + tag: 'div', + }); + await driver.clickElement('.show-fiat-on-testnets-toggle'); + + await driver.delay(1000); + + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // close popover for the first time + await driver.clickElement('[data-testid="popover-close"]'); + // go to setting and back to home page and make sure popover is not shown again + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + // close setting + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); + // assert popover does not exist + await driver.assertElementNotPresent('[data-testid="popover-close"]'); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned off', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: false, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); + + it('Should Successfully track the event when toggle is turned on', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-fd20', + participateInMetaMetrics: true, + }) + .withPreferencesControllerShowNativeTokenAsMainBalanceDisabled() + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint[]; + }) => { + await unlockWallet(driver); + + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ + text: 'General', + tag: 'div', + }); + await driver.clickElement('.show-native-token-as-main-balance'); + + const events = await getEventPayloads(driver, mockedEndpoints); + expect(events[0].properties).toMatchObject({ + show_native_token_as_main_balance: true, + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/settings/state-logs.spec.js b/test/e2e/tests/settings/state-logs.spec.js index 07e9b4744900..3b9e568ee6bc 100644 --- a/test/e2e/tests/settings/state-logs.spec.js +++ b/test/e2e/tests/settings/state-logs.spec.js @@ -1,10 +1,11 @@ const { strict: assert } = require('assert'); const { promises: fs } = require('fs'); const { - defaultGanacheOptions, - withFixtures, createDownloadFolder, + defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -38,9 +39,8 @@ describe('State logs', function () { await unlockWallet(driver); // Download state logs - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Advanced', tag: 'div' }); await driver.clickElement({ diff --git a/test/e2e/tests/settings/terms-of-use.spec.js b/test/e2e/tests/settings/terms-of-use.spec.js index 87c2c2d0018d..ee314ee95600 100644 --- a/test/e2e/tests/settings/terms-of-use.spec.js +++ b/test/e2e/tests/settings/terms-of-use.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -26,12 +25,7 @@ describe('Terms of use', function () { const acceptTerms = '[data-testid="terms-of-use-accept-button"]'; await driver.clickElement('[data-testid="popover-scroll-button"]'); await driver.clickElement('[data-testid="terms-of-use-checkbox"]'); - await driver.clickElement(acceptTerms); - - // check modal is no longer shown - await driver.assertElementNotPresent(acceptTerms); - const termsExists = await driver.isElementPresent(acceptTerms); - assert.equal(termsExists, false, 'terms of use should not be shown'); + await driver.clickElementAndWaitToDisappear(acceptTerms); }, ); }); diff --git a/test/e2e/tests/survey/survey.spec.js b/test/e2e/tests/survey/survey.spec.js new file mode 100644 index 000000000000..66a494297e43 --- /dev/null +++ b/test/e2e/tests/survey/survey.spec.js @@ -0,0 +1,62 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + unlockWallet, + defaultGanacheOptions, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +describe('Test Survey', function () { + it('should show 2 surveys, and then none', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPreferencesController() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id-power-user', + participateInMetaMetrics: true, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + async function checkForToast(surveyId) { + await driver.findElement('[data-testid="survey-toast"]'); + const surveyElement = await driver.findElement( + '[data-testid="survey-toast-banner-base"] p', + ); + const surveyText = await surveyElement.getText(); + assert.equal( + surveyText, + `Test survey ${surveyId}`, + `Survey text should be "Test survey ${surveyId}"`, + ); + await driver.clickElement( + '[data-testid="survey-toast-banner-base"] [aria-label="Close"]', + ); + } + + async function checkForNoToast() { + const surveyToastAfterRefresh = + await driver.isElementPresentAndVisible( + '[data-testid="survey-toast"]', + ); + assert.equal( + surveyToastAfterRefresh, + false, + 'Survey should not be visible after refresh', + ); + } + + await unlockWallet(driver); + await checkForToast(1); + await driver.refresh(); + await checkForToast(2); + await driver.refresh(); + await checkForNoToast(); + }, + ); + }); +}); 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/import-tokens.spec.js b/test/e2e/tests/tokens/import-tokens.spec.js index 890353236912..a1eb2782f9db 100644 --- a/test/e2e/tests/tokens/import-tokens.spec.js +++ b/test/e2e/tests/tokens/import-tokens.spec.js @@ -37,7 +37,7 @@ describe('Import flow', function () { it('allows importing multiple tokens from search', async function () { await withFixtures( { - fixtures: new FixtureBuilder().build(), + fixtures: new FixtureBuilder().withNetworkControllerOnMainnet().build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), testSpecificMock: mockPriceFetch, @@ -45,22 +45,8 @@ describe('Import flow', function () { async ({ driver }) => { await unlockWallet(driver); - // Token list is only on mainnet - await driver.clickElement('[data-testid="network-display"]'); - const networkSelectionModal = await driver.findVisibleElement( - '.mm-modal', - ); await driver.assertElementNotPresent('.loading-overlay'); - await driver.clickElement({ text: 'Ethereum Mainnet', tag: 'p' }); - - // Wait for network to change and token list to load from state - await networkSelectionModal.waitForElementState('hidden'); - await driver.findElement({ - css: '[data-testid="network-display"]', - text: 'Ethereum Mainnet', - }); - await driver.clickElement('[data-testid="import-token-button"]'); await driver.fill('input[placeholder="Search tokens"]', 'cha'); diff --git a/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js b/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js index 36b09723444b..ccd15ce0f71b 100644 --- a/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js +++ b/test/e2e/tests/tokens/nft/auto-detect-nft.spec.js @@ -1,8 +1,9 @@ const { strict: assert } = require('assert'); const { - withFixtures, defaultGanacheOptions, + openMenuSafe, unlockWallet, + withFixtures, } = require('../../../helpers'); const FixtureBuilder = require('../../../fixture-builder'); const { setupAutoDetectMocking } = require('./mocks'); @@ -25,9 +26,8 @@ describe('NFT detection', function () { await unlockWallet(driver); // go to settings - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); + await openMenuSafe(driver); + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); await driver.clickElement( 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/test/e2e/tests/transaction/send-edit.spec.js b/test/e2e/tests/transaction/send-edit.spec.js index e5a74798d8fd..953f2ebf3569 100644 --- a/test/e2e/tests/transaction/send-edit.spec.js +++ b/test/e2e/tests/transaction/send-edit.spec.js @@ -1,5 +1,4 @@ const { strict: assert } = require('assert'); - const { createInternalTransaction, } = require('../../page-objects/flows/transaction'); diff --git a/test/e2e/tests/transaction/send-eth.spec.js b/test/e2e/tests/transaction/send-eth.spec.js index 36872115dcbe..5cbcb8309a18 100644 --- a/test/e2e/tests/transaction/send-eth.spec.js +++ b/test/e2e/tests/transaction/send-eth.spec.js @@ -189,7 +189,9 @@ describe('Send ETH', function () { const balance = await driver.findElement( '[data-testid="eth-overview__primary-currency"]', ); + assert.ok(/^[\d.]+\sETH$/u.test(await balance.getText())); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 97c616d01f4b..a03a0d1cbd04 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -63,6 +63,8 @@ function wrapElementWithAPI(element, driver) { return await driver.wait(until.stalenessOf(element), timeout); case 'visible': return await driver.wait(until.elementIsVisible(element), timeout); + case 'disabled': + return await driver.wait(until.elementIsDisabled(element), timeout); default: throw new Error(`Provided state: '${state}' is not supported`); } diff --git a/test/e2e/webdriver/types.ts b/test/e2e/webdriver/types.ts new file mode 100644 index 000000000000..68cfa15dd600 --- /dev/null +++ b/test/e2e/webdriver/types.ts @@ -0,0 +1,5 @@ +import { WebElement, WebElementPromise } from 'selenium-webdriver'; + +export type WebElementWithWaitForElementState = WebElement & { + waitForElementState: (state: unknown, timeout?: unknown) => WebElementPromise; +}; diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 809ac988962f..e11f206d1996 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,16 +1,16 @@ -import { act, fireEvent, waitFor, screen } from '@testing-library/react'; -import nock from 'nock'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import nock from 'nock'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation } from '../../helpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -182,10 +182,12 @@ describe('Permit Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); - expect( - screen.getByText('This site wants permission to spend your tokens.'), - ).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Spending cap request')).toBeInTheDocument(); + expect( + screen.getByText('This site wants permission to spend your tokens.'), + ).toBeInTheDocument(); + }); }); it('displays the simulation section', async () => { diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 5a965a3d6928..690446caa533 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,15 +1,15 @@ -import { fireEvent, waitFor } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; jest.mock('../../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../../ui/store/background-connection'), @@ -156,14 +156,16 @@ describe('PersonalSign Confirmation', () => { account.address, ); - const { getByText } = await integrationTestRender({ - preloadedState: mockedMetaMaskState, - backgroundConnection: backgroundConnectionMocked, + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - expect(getByText('Signature request')).toBeInTheDocument(); + expect(screen.getByText('Signature request')).toBeInTheDocument(); expect( - getByText('Review request details before you confirm.'), + screen.getByText('Review request details before you confirm.'), ).toBeInTheDocument(); }); diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx new file mode 100644 index 000000000000..ecef04f30861 --- /dev/null +++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx @@ -0,0 +1,408 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; +import { + act, + fireEvent, + screen, + waitFor, + within, +} from '@testing-library/react'; +import nock from 'nock'; +import { + MetaMetricsEventCategory, + MetaMetricsEventLocation, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedContractDeploymentTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedContractDeployment = ({ + accountAddress, + showConfirmationAdvancedDetails = false, +}: { + accountAddress: string; + showConfirmationAdvancedDetails?: boolean; +}) => { + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails, + }, + nextNonce: '8', + currencyRates: { + SepoliaETH: { + conversionDate: 1721392020.645, + conversionRate: 3404.13, + usdConversionRate: 3404.13, + }, + ETH: { + conversionDate: 1721393858.083, + conversionRate: 3414.67, + usdConversionRate: 3414.67, + }, + }, + currentCurrency: 'usd', + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'local:http://localhost:8086/', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xd0e30db0': { + name: 'Deposit', + params: [ + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedContractDeploymentTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'Deposit', + params: [ + { + name: 'numberOfTokens', + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'Sourcify', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); +}; + +describe('Contract Deployment Confirmation', () => { + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks(); + const DEPOSIT_HEX_SIG = '0xd0e30db0'; + mock4byte(DEPOSIT_HEX_SIG); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('displays the header account modal with correct data', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const accountName = account.metadata.name; + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect(screen.getByTestId('header-account-name')).toHaveTextContent( + accountName, + ); + expect(screen.getByTestId('header-network-display-name')).toHaveTextContent( + 'Sepolia', + ); + + fireEvent.click(screen.getByTestId('header-info__account-details-button')); + + expect( + await screen.findByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).toHaveTextContent(accountName); + expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent( + '0x0DCD5...3E7bc', + ); + expect( + screen.getByTestId('confirmation-account-details-modal__account-balance'), + ).toHaveTextContent('1.582717SepoliaETH'); + + let confirmAccountDetailsModalMetricsEvent; + + await waitFor(() => { + confirmAccountDetailsModalMetricsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => + call[0] === 'trackMetaMetricsEvent' && + call[1]?.[0].category === MetaMetricsEventCategory.Confirmations, + ); + + expect(confirmAccountDetailsModalMetricsEvent?.[0]).toBe( + 'trackMetaMetricsEvent', + ); + }); + + expect(confirmAccountDetailsModalMetricsEvent?.[1]).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + category: MetaMetricsEventCategory.Confirmations, + event: MetaMetricsEventName.AccountDetailsOpened, + properties: { + action: 'Confirm Screen', + location: MetaMetricsEventLocation.Transaction, + transaction_type: TransactionType.deployContract, + }, + }), + ]), + ); + + fireEvent.click( + screen.getByTestId('confirmation-account-details-modal__close-button'), + ); + + await waitFor(() => { + expect( + screen.queryByTestId( + 'confirmation-account-details-modal__account-name', + ), + ).not.toBeInTheDocument(); + }); + }); + + it('displays the transaction details section', async () => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleDeployContract') as string), + ).toBeInTheDocument(); + + const simulationSection = screen.getByTestId('simulation-details-layout'); + expect(simulationSection).toBeInTheDocument(); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); + const simulationDetailsRow = await screen.findByTestId( + 'simulation-rows-incoming', + ); + expect(simulationSection).toContainElement(simulationDetailsRow); + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, + ); + expect(simulationDetailsRow).toContainElement( + screen.getByTestId('simulation-details-amount-pill'), + ); + + const transactionDetailsSection = screen.getByTestId( + 'transaction-details-section', + ); + expect(transactionDetailsSection).toBeInTheDocument(); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + expect(gasFeesSection).toBeInTheDocument(); + + const editGasFeesRow = + within(gasFeesSection).getByTestId('edit-gas-fees-row'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); + + const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); + expect(firstGasField).toHaveTextContent('0.0001 ETH'); + const editGasFeeNativeCurrency = + within(editGasFeesRow).getByTestId('native-currency'); + expect(editGasFeeNativeCurrency).toHaveTextContent('$0.47'); + expect(editGasFeesRow).toContainElement( + screen.getByTestId('edit-gas-fee-icon'), + ); + + const gasFeeSpeed = within(gasFeesSection).getByTestId( + 'gas-fee-details-speed', + ); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); + + const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); + expect(gasTimingTime).toHaveTextContent('~0 sec'); + }); + + it('sets the preference showConfirmationAdvancedDetails to true when advanced details button is clicked', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: false, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + fireEvent.click(screen.getByTestId('header-advanced-details-button')); + + await waitFor(() => { + expect( + mockedBackgroundConnection.callBackgroundMethod, + ).toHaveBeenCalledWith( + 'setPreference', + ['showConfirmationAdvancedDetails', true], + expect.anything(), + ); + }); + }); + + it('displays the advanced transaction details section', async () => { + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ setPreference: {} }), + ); + + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedContractDeployment({ + accountAddress: account.address, + showConfirmationAdvancedDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + await waitFor(() => { + expect( + mockedBackgroundConnection.submitRequestToBackground, + ).toHaveBeenCalledWith('getNextNonce', expect.anything()); + }); + + const gasFeesSection = screen.getByTestId('gas-fee-section'); + const maxFee = screen.getByTestId('gas-fee-details-max-fee'); + expect(gasFeesSection).toContainElement(maxFee); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); + expect(maxFee).toHaveTextContent('0.0023 ETH'); + expect(maxFee).toHaveTextContent('$7.72'); + + const nonceSection = screen.getByTestId('advanced-details-nonce-section'); + expect(nonceSection).toBeInTheDocument(); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); + expect(nonceSection).toContainElement( + screen.getByTestId('advanced-details-displayed-nonce'), + ); + expect( + screen.getByTestId('advanced-details-displayed-nonce'), + ).toHaveTextContent('9'); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Deposit'); + + const transactionDataParams = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(transactionDataParams); + expect(transactionDataParams).toHaveTextContent('Number Of Tokens'); + expect(transactionDataParams).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx index cd5953db50b8..1102cb21c67d 100644 --- a/test/integration/confirmations/transactions/contract-interaction.test.tsx +++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx @@ -1,25 +1,26 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; import { + act, fireEvent, + screen, waitFor, within, - screen, - act, } from '@testing-library/react'; -import { ApprovalType } from '@metamask/controller-utils'; import nock from 'nock'; -import { TransactionType } from '@metamask/transaction-controller'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; import { getMaliciousUnapprovedTransaction, - getUnapprovedTransaction, + getUnapprovedContractInteractionTransaction, } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -89,7 +90,7 @@ const getMetaMaskStateWithUnapprovedContractInteraction = ({ }, }, transactions: [ - getUnapprovedTransaction( + getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, @@ -261,18 +262,21 @@ describe('Contract Interaction Confirmation', () => { }); }); - expect(screen.getByText('Transaction request')).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleTransaction') as string), + ).toBeInTheDocument(); const simulationSection = screen.getByTestId('simulation-details-layout'); expect(simulationSection).toBeInTheDocument(); - expect(simulationSection).toHaveTextContent('Estimated changes'); + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsTitle') as string, + ); const simulationDetailsRow = await screen.findByTestId( 'simulation-rows-incoming', ); expect(simulationSection).toContainElement(simulationDetailsRow); - expect(simulationDetailsRow).toHaveTextContent('You receive'); - expect(simulationDetailsRow).toContainElement( - screen.getByTestId('simulation-details-asset-pill'), + expect(simulationDetailsRow).toHaveTextContent( + tEn('simulationDetailsIncomingHeading') as string, ); expect(simulationDetailsRow).toContainElement( screen.getByTestId('simulation-details-amount-pill'), @@ -282,15 +286,19 @@ describe('Contract Interaction Confirmation', () => { 'transaction-details-section', ); expect(transactionDetailsSection).toBeInTheDocument(); - expect(transactionDetailsSection).toHaveTextContent('Request from'); - expect(transactionDetailsSection).toHaveTextContent('Interacting with'); + expect(transactionDetailsSection).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(transactionDetailsSection).toHaveTextContent( + tEn('interactingWith') as string, + ); const gasFeesSection = screen.getByTestId('gas-fee-section'); expect(gasFeesSection).toBeInTheDocument(); const editGasFeesRow = within(gasFeesSection).getByTestId('edit-gas-fees-row'); - expect(editGasFeesRow).toHaveTextContent('Network fee'); + expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string); const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field'); expect(firstGasField).toHaveTextContent('0.0001 ETH'); @@ -304,7 +312,7 @@ describe('Contract Interaction Confirmation', () => { const gasFeeSpeed = within(gasFeesSection).getByTestId( 'gas-fee-details-speed', ); - expect(gasFeeSpeed).toHaveTextContent('Speed'); + expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string); const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time'); expect(gasTimingTime).toHaveTextContent('~0 sec'); @@ -393,13 +401,15 @@ describe('Contract Interaction Confirmation', () => { const gasFeesSection = screen.getByTestId('gas-fee-section'); const maxFee = screen.getByTestId('gas-fee-details-max-fee'); expect(gasFeesSection).toContainElement(maxFee); - expect(maxFee).toHaveTextContent('Max fee'); + expect(maxFee).toHaveTextContent(tEn('maxFee') as string); expect(maxFee).toHaveTextContent('0.0023 ETH'); expect(maxFee).toHaveTextContent('$7.72'); const nonceSection = screen.getByTestId('advanced-details-nonce-section'); expect(nonceSection).toBeInTheDocument(); - expect(nonceSection).toHaveTextContent('Nonce'); + expect(nonceSection).toHaveTextContent( + tEn('advancedDetailsNonceDesc') as string, + ); expect(nonceSection).toContainElement( screen.getByTestId('advanced-details-displayed-nonce'), ); @@ -414,7 +424,9 @@ describe('Contract Interaction Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('mintNFTs'); const transactionDataParams = screen.getByTestId( @@ -444,9 +456,8 @@ describe('Contract Interaction Confirmation', () => { }); }); - const headingText = 'This is a deceptive request'; - const bodyText = - 'If you approve this request, a third party known for scams will take all your assets.'; + const headingText = tEn('blockaidTitleDeceptive') as string; + const bodyText = tEn('blockaidDescriptionTransferFarming') as string; expect(screen.getByText(headingText)).toBeInTheDocument(); expect(screen.getByText(bodyText)).toBeInTheDocument(); }); diff --git a/test/integration/confirmations/transactions/erc20-approve.test.tsx b/test/integration/confirmations/transactions/erc20-approve.test.tsx index b6ab98774cb4..a2404ba75b09 100644 --- a/test/integration/confirmations/transactions/erc20-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc20-approve.test.tsx @@ -2,12 +2,13 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -161,9 +162,13 @@ describe('ERC20 Approve Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); expect( - screen.getByText('This site wants permission to withdraw your tokens'), + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText( + tEn('confirmTitleDescERC20ApproveTransaction') as string, + ), ).toBeInTheDocument(); }); @@ -184,9 +189,9 @@ describe('ERC20 Approve Confirmation', () => { expect(simulationSection).toBeInTheDocument(); expect(simulationSection).toHaveTextContent( - "You're giving someone else permission to spend this amount from your account.", + tEn('simulationDetailsERC20ApproveDesc') as string, ); - expect(simulationSection).toHaveTextContent('Spending cap'); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); const spendingCapValue = screen.getByTestId('simulation-token-value'); expect(simulationSection).toContainElement(spendingCapValue); expect(spendingCapValue).toHaveTextContent('1'); @@ -213,7 +218,7 @@ describe('ERC20 Approve Confirmation', () => { ); expect(approveDetails).toContainElement(approveDetailsSpender); - expect(approveDetailsSpender).toHaveTextContent('Spender'); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); const spenderTooltip = screen.getByTestId( 'confirmation__approve-spender-tooltip', @@ -222,7 +227,7 @@ describe('ERC20 Approve Confirmation', () => { await testUser.hover(spenderTooltip); const spenderTooltipContent = await screen.findByText( - 'This is the address that will be able to spend your tokens on your behalf.', + tEn('spenderTooltipERC20ApproveDesc') as string, ); expect(spenderTooltipContent).toBeInTheDocument(); @@ -243,7 +248,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRequestFromTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the site asking for your confirmation.', + tEn('requestFromTransactionDescription') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -266,13 +271,15 @@ describe('ERC20 Approve Confirmation', () => { ); expect(spendingCapSection).toBeInTheDocument(); - expect(spendingCapSection).toHaveTextContent('Account balance'); + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); expect(spendingCapSection).toHaveTextContent('0'); const spendingCapGroup = screen.getByTestId( 'confirmation__approve-spending-cap-group', ); expect(spendingCapSection).toContainElement(spendingCapGroup); - expect(spendingCapGroup).toHaveTextContent('Spending cap'); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); expect(spendingCapGroup).toHaveTextContent('1'); const spendingCapGroupTooltip = screen.getByTestId( @@ -281,7 +288,7 @@ describe('ERC20 Approve Confirmation', () => { expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); await testUser.hover(spendingCapGroupTooltip); const requestFromTooltipContent = await screen.findByText( - 'This is the amount of tokens the spender will be able to access on your behalf.', + tEn('spendingCapTooltipDesc') as string, ); expect(requestFromTooltipContent).toBeInTheDocument(); }); @@ -308,7 +315,9 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-recipient-row', ); expect(approveDetails).toContainElement(approveDetailsRecipient); - expect(approveDetailsRecipient).toHaveTextContent('Interacting with'); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); const approveDetailsRecipientTooltip = screen.getByTestId( @@ -319,7 +328,7 @@ describe('ERC20 Approve Confirmation', () => { ); await testUser.hover(approveDetailsRecipientTooltip); const recipientTooltipContent = await screen.findByText( - "This is the contract you're interacting with. Protect yourself from scammers by verifying the details.", + tEn('interactingWithTransactionDescription') as string, ); expect(recipientTooltipContent).toBeInTheDocument(); @@ -327,7 +336,7 @@ describe('ERC20 Approve Confirmation', () => { 'transaction-details-method-data-row', ); expect(approveDetails).toContainElement(approveMethodData); - expect(approveMethodData).toHaveTextContent('Method'); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); expect(approveMethodData).toHaveTextContent('Approve'); const approveMethodDataTooltip = screen.getByTestId( 'transaction-details-method-data-row-tooltip', @@ -335,7 +344,7 @@ describe('ERC20 Approve Confirmation', () => { expect(approveMethodData).toContainElement(approveMethodDataTooltip); await testUser.hover(approveMethodDataTooltip); const approveMethodDataTooltipContent = await screen.findByText( - 'Function executed based on decoded input data.', + tEn('methodDataTransactionDesc') as string, ); expect(approveMethodDataTooltipContent).toBeInTheDocument(); @@ -351,7 +360,9 @@ describe('ERC20 Approve Confirmation', () => { 'advanced-details-data-function', ); expect(dataSection).toContainElement(dataSectionFunction); - expect(dataSectionFunction).toHaveTextContent('Function'); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); expect(dataSectionFunction).toHaveTextContent('Approve'); const approveDataParams1 = screen.getByTestId( diff --git a/test/integration/confirmations/transactions/erc721-approve.test.tsx b/test/integration/confirmations/transactions/erc721-approve.test.tsx index 8a836dbd7568..c3948d150b1d 100644 --- a/test/integration/confirmations/transactions/erc721-approve.test.tsx +++ b/test/integration/confirmations/transactions/erc721-approve.test.tsx @@ -1,12 +1,14 @@ import { ApprovalType } from '@metamask/controller-utils'; -import { act, screen, waitFor } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation, mock4byte } from '../../helpers'; -import { TokenStandard } from '../../../../shared/constants/transaction'; -import { createTestProviderTools } from '../../../stub/provider'; import { getUnapprovedApproveTransaction } from './transactionDataHelpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -23,14 +25,21 @@ const backgroundConnectionMocked = { export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; export const pendingTransactionTime = new Date().getTime(); -const getMetaMaskStateWithUnapprovedApproveTransaction = ( - accountAddress: string, -) => { +const getMetaMaskStateWithUnapprovedApproveTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + return { ...mockMetaMaskState, preferences: { ...mockMetaMaskState.preferences, redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, }, pendingApprovals: { [pendingTransactionId]: { @@ -61,7 +70,7 @@ const getMetaMaskStateWithUnapprovedApproveTransaction = ( }, transactions: [ getUnapprovedApproveTransaction( - accountAddress, + account.address, pendingTransactionId, pendingTransactionTime, ), @@ -78,7 +87,7 @@ const advancedDetailsMockedRequests = { decodeTransactionData: { data: [ { - name: 'approve', + name: 'Approve', params: [ { type: 'address', @@ -129,7 +138,8 @@ describe('ERC721 Approve Confirmation', () => { }, }); const APPROVE_NFT_HEX_SIG = '0x095ea7b3'; - mock4byte(APPROVE_NFT_HEX_SIG); + const APPROVE_NFT_TEXT_SIG = 'approve(address,uint256)'; + mock4byte(APPROVE_NFT_HEX_SIG, APPROVE_NFT_TEXT_SIG); }); afterEach(() => { @@ -141,15 +151,28 @@ describe('ERC721 Approve Confirmation', () => { delete (global as any).ethereumProvider; }); - it('displays approve details with correct data', async () => { - const account = - mockMetaMaskState.internalAccounts.accounts[ - mockMetaMaskState.internalAccounts - .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts - ]; + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitleApproveTransaction') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays approve simulation section', async () => { const mockedMetaMaskState = - getMetaMaskStateWithUnapprovedApproveTransaction(account.address); + getMetaMaskStateWithUnapprovedApproveTransaction(); await act(async () => { await integrationTestRender({ @@ -158,12 +181,163 @@ describe('ERC721 Approve Confirmation', () => { }); }); - await waitFor(() => { - expect(screen.getByText('Allowance request')).toBeInTheDocument(); + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent( + tEn('simulationApproveHeading') as string, + ); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - await waitFor(() => { - expect(screen.getByText('Request from')).toBeInTheDocument(); + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedApproveTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('Approve'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('Approve'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); }); }); diff --git a/test/integration/confirmations/transactions/increase-allowance.test.tsx b/test/integration/confirmations/transactions/increase-allowance.test.tsx new file mode 100644 index 000000000000..c288a5cc4e6d --- /dev/null +++ b/test/integration/confirmations/transactions/increase-allowance.test.tsx @@ -0,0 +1,384 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedIncreaseAllowanceTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0x39509351': { + name: 'increaseAllowance', + params: [ + { + type: 'address', + }, + { + type: 'uint256', + }, + ], + }, + }, + transactions: [ + getUnapprovedIncreaseAllowanceTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'increaseAllowance', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'uint256', + value: 1, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC20 increaseAllowance Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC20, + }, + }); + const INCREASE_ALLOWANCE_ERC20_HEX_SIG = '0x39509351'; + const INCREASE_ALLOWANCE_ERC20_TEXT_SIG = + 'increaseAllowance(address,uint256)'; + mock4byte( + INCREASE_ALLOWANCE_ERC20_HEX_SIG, + INCREASE_ALLOWANCE_ERC20_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays spending cap request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('confirmTitlePermitTokens') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescPermitSignature') as string), + ).toBeInTheDocument(); + }); + + it('displays increase allowance simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsERC20ApproveDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent('1'); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipERC20ApproveDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent('Request from'); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays spending cap section with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const spendingCapSection = screen.getByTestId( + 'confirmation__approve-spending-cap-section', + ); + expect(spendingCapSection).toBeInTheDocument(); + + expect(spendingCapSection).toHaveTextContent( + tEn('accountBalance') as string, + ); + expect(spendingCapSection).toHaveTextContent('0'); + const spendingCapGroup = screen.getByTestId( + 'confirmation__approve-spending-cap-group', + ); + expect(spendingCapSection).toContainElement(spendingCapGroup); + expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string); + expect(spendingCapGroup).toHaveTextContent('1'); + + const spendingCapGroupTooltip = screen.getByTestId( + 'confirmation__approve-spending-cap-group-tooltip', + ); + expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip); + await testUser.hover(spendingCapGroupTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('spendingCapTooltipDesc') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedIncreaseAllowanceTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('increaseAllowance'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('increaseAllowance'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('1'); + }); +}); diff --git a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx new file mode 100644 index 000000000000..a65688030e90 --- /dev/null +++ b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx @@ -0,0 +1,348 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import nock from 'nock'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { tEn } from '../../../lib/i18n-helpers'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import { createTestProviderTools } from '../../../stub/provider'; +import mockMetaMaskState from '../../data/integration-init-state.json'; +import { createMockImplementation, mock4byte } from '../../helpers'; +import { getUnapprovedSetApprovalForAllTransaction } from './transactionDataHelpers'; + +jest.mock('../../../../ui/store/background-connection', () => ({ + ...jest.requireActual('../../../../ui/store/background-connection'), + submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), +})); + +const mockedBackgroundConnection = jest.mocked(backgroundConnection); + +const backgroundConnectionMocked = { + onNotification: jest.fn(), +}; +export const pendingTransactionId = '48a75190-45ca-11ef-9001-f3886ec2397c'; +export const pendingTransactionTime = new Date().getTime(); + +const getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction = (opts?: { + showAdvanceDetails: boolean; +}) => { + const account = + mockMetaMaskState.internalAccounts.accounts[ + mockMetaMaskState.internalAccounts + .selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts + ]; + + return { + ...mockMetaMaskState, + preferences: { + ...mockMetaMaskState.preferences, + redesignedConfirmationsEnabled: true, + showConfirmationAdvancedDetails: opts?.showAdvanceDetails ?? false, + }, + pendingApprovals: { + [pendingTransactionId]: { + id: pendingTransactionId, + origin: 'origin', + time: pendingTransactionTime, + type: ApprovalType.Transaction, + requestData: { + txId: pendingTransactionId, + }, + requestState: null, + expectsResult: false, + }, + }, + pendingApprovalCount: 1, + knownMethodData: { + '0xa22cb465': { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + }, + { + type: 'bool', + }, + ], + }, + }, + transactions: [ + getUnapprovedSetApprovalForAllTransaction( + account.address, + pendingTransactionId, + pendingTransactionTime, + ), + ], + }; +}; + +const advancedDetailsMockedRequests = { + getGasFeeTimeEstimate: { + lowerTimeBound: new Date().getTime(), + upperTimeBound: new Date().getTime(), + }, + getNextNonce: '9', + decodeTransactionData: { + data: [ + { + name: 'setApprovalForAll', + params: [ + { + type: 'address', + value: '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B', + }, + { + type: 'bool', + value: true, + }, + ], + }, + ], + source: 'FourByte', + }, +}; + +const setupSubmitRequestToBackgroundMocks = ( + mockRequests?: Record, +) => { + mockedBackgroundConnection.submitRequestToBackground.mockImplementation( + createMockImplementation({ + ...advancedDetailsMockedRequests, + ...(mockRequests ?? {}), + }), + ); + + mockedBackgroundConnection.callBackgroundMethod.mockImplementation( + createMockImplementation({ addKnownMethodData: {} }), + ); +}; + +describe('ERC721 setApprovalForAll Confirmation', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'sepolia', + chainId: '0xaa36a7', + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + beforeEach(() => { + jest.resetAllMocks(); + setupSubmitRequestToBackgroundMocks({ + getTokenStandardAndDetails: { + standard: TokenStandard.ERC721, + }, + }); + const INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG = '0xa22cb465'; + const INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG = + 'setApprovalForAll(address,bool)'; + mock4byte( + INCREASE_SET_APPROVAL_FOR_ALL_HEX_SIG, + INCREASE_SET_APPROVAL_FOR_ALL_TEXT_SIG, + ); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + afterAll(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (global as any).ethereumProvider; + }); + + it('displays set approval for all request title', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + expect( + screen.getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); + + it('displays set approval for all simulation section', async () => { + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const simulationSection = screen.getByTestId( + 'confirmation__simulation_section', + ); + expect(simulationSection).toBeInTheDocument(); + + expect(simulationSection).toHaveTextContent( + tEn('simulationDetailsSetApprovalForAllDesc') as string, + ); + expect(simulationSection).toHaveTextContent(tEn('withdrawing') as string); + const spendingCapValue = screen.getByTestId('simulation-token-value'); + expect(simulationSection).toContainElement(spendingCapValue); + expect(spendingCapValue).toHaveTextContent(tEn('all') as string); + expect(simulationSection).toHaveTextContent('0x07614...3ad68'); + }); + + it('displays approve details with correct data', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction(); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + const approveDetailsSpender = screen.getByTestId( + 'confirmation__approve-spender', + ); + + expect(approveDetails).toContainElement(approveDetailsSpender); + expect(approveDetailsSpender).toHaveTextContent( + tEn('permissionFor') as string, + ); + expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B'); + const spenderTooltip = screen.getByTestId( + 'confirmation__approve-spender-tooltip', + ); + expect(approveDetailsSpender).toContainElement(spenderTooltip); + await testUser.hover(spenderTooltip); + + const spenderTooltipContent = await screen.findByText( + tEn('spenderTooltipDesc') as string, + ); + expect(spenderTooltipContent).toBeInTheDocument(); + + const approveDetailsRequestFrom = screen.getByTestId( + 'transaction-details-origin-row', + ); + expect(approveDetails).toContainElement(approveDetailsRequestFrom); + expect(approveDetailsRequestFrom).toHaveTextContent( + tEn('requestFrom') as string, + ); + expect(approveDetailsRequestFrom).toHaveTextContent( + 'http://localhost:8086/', + ); + + const approveDetailsRequestFromTooltip = screen.getByTestId( + 'transaction-details-origin-row-tooltip', + ); + expect(approveDetailsRequestFrom).toContainElement( + approveDetailsRequestFromTooltip, + ); + await testUser.hover(approveDetailsRequestFromTooltip); + const requestFromTooltipContent = await screen.findByText( + tEn('requestFromTransactionDescription') as string, + ); + expect(requestFromTooltipContent).toBeInTheDocument(); + }); + + it('displays the advanced transaction details section', async () => { + const testUser = userEvent.setup(); + + const mockedMetaMaskState = + getMetaMaskStateWithUnapprovedSetApprovalForAllTransaction({ + showAdvanceDetails: true, + }); + + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); + }); + + const approveDetails = screen.getByTestId('confirmation__approve-details'); + expect(approveDetails).toBeInTheDocument(); + + const approveDetailsRecipient = screen.getByTestId( + 'transaction-details-recipient-row', + ); + expect(approveDetails).toContainElement(approveDetailsRecipient); + expect(approveDetailsRecipient).toHaveTextContent( + tEn('interactingWith') as string, + ); + expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68'); + + const approveDetailsRecipientTooltip = screen.getByTestId( + 'transaction-details-recipient-row-tooltip', + ); + expect(approveDetailsRecipient).toContainElement( + approveDetailsRecipientTooltip, + ); + await testUser.hover(approveDetailsRecipientTooltip); + const recipientTooltipContent = await screen.findByText( + tEn('interactingWithTransactionDescription') as string, + ); + expect(recipientTooltipContent).toBeInTheDocument(); + + const approveMethodData = await screen.findByTestId( + 'transaction-details-method-data-row', + ); + expect(approveDetails).toContainElement(approveMethodData); + expect(approveMethodData).toHaveTextContent(tEn('methodData') as string); + expect(approveMethodData).toHaveTextContent('setApprovalForAll'); + const approveMethodDataTooltip = screen.getByTestId( + 'transaction-details-method-data-row-tooltip', + ); + expect(approveMethodData).toContainElement(approveMethodDataTooltip); + await testUser.hover(approveMethodDataTooltip); + const approveMethodDataTooltipContent = await screen.findByText( + tEn('methodDataTransactionDesc') as string, + ); + expect(approveMethodDataTooltipContent).toBeInTheDocument(); + + const approveDetailsNonce = screen.getByTestId( + 'advanced-details-nonce-section', + ); + expect(approveDetailsNonce).toBeInTheDocument(); + + const dataSection = screen.getByTestId('advanced-details-data-section'); + expect(dataSection).toBeInTheDocument(); + + const dataSectionFunction = screen.getByTestId( + 'advanced-details-data-function', + ); + expect(dataSection).toContainElement(dataSectionFunction); + expect(dataSectionFunction).toHaveTextContent( + tEn('transactionDataFunction') as string, + ); + expect(dataSectionFunction).toHaveTextContent('setApprovalForAll'); + + const approveDataParams1 = screen.getByTestId( + 'advanced-details-data-param-0', + ); + expect(dataSection).toContainElement(approveDataParams1); + expect(approveDataParams1).toHaveTextContent('Param #1'); + expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B'); + + const approveDataParams2 = screen.getByTestId( + 'advanced-details-data-param-1', + ); + expect(dataSection).toContainElement(approveDataParams2); + expect(approveDataParams2).toHaveTextContent('Param #2'); + expect(approveDataParams2).toHaveTextContent('true'); + }); +}); diff --git a/test/integration/confirmations/transactions/transactionDataHelpers.tsx b/test/integration/confirmations/transactions/transactionDataHelpers.tsx index 12550ea5e563..e9bcd7b818f2 100644 --- a/test/integration/confirmations/transactions/transactionDataHelpers.tsx +++ b/test/integration/confirmations/transactions/transactionDataHelpers.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; -export const getUnapprovedTransaction = ( +export const getUnapprovedContractInteractionTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, @@ -70,37 +70,105 @@ export const getUnapprovedTransaction = ( }; }; +export const getUnapprovedContractDeploymentTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xd0e30db0', + }, + type: TransactionType.deployContract, + }; +}; + export const getUnapprovedApproveTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, ), txParams: { - from: accountAddress, + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', - gas: '0x16a92', - to: '0x076146c765189d51be3160a2140cf80bfc73ad68', - value: '0x0', - maxFeePerGas: '0x5b06b0c0d', - maxPriorityFeePerGas: '0x59682f00', }, type: TransactionType.tokenMethodApprove, }; }; +export const getUnapprovedIncreaseAllowanceTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0x395093510000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000007530', + }, + type: TransactionType.tokenMethodIncreaseAllowance, + }; +}; + +export const getUnapprovedSetApprovalForAllTransaction = ( + accountAddress: string, + pendingTransactionId: string, + pendingTransactionTime: number, +) => { + return { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ), + txParams: { + ...getUnapprovedContractInteractionTransaction( + accountAddress, + pendingTransactionId, + pendingTransactionTime, + ).txParams, + data: '0xa22cb4650000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000000001', + }, + type: TransactionType.tokenMethodSetApprovalForAll, + }; +}; + export const getMaliciousUnapprovedTransaction = ( accountAddress: string, pendingTransactionId: string, pendingTransactionTime: number, ) => { return { - ...getUnapprovedTransaction( + ...getUnapprovedContractInteractionTransaction( accountAddress, pendingTransactionId, pendingTransactionTime, diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index b031611a06ea..82c55c9bd7e0 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -782,7 +782,6 @@ "showFiatInTestnets": false, "showTestNetworks": true, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": false, "showConfirmationAdvancedDetails": false }, diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index 06d85e298409..e651e9c2ce29 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -224,7 +224,6 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": null, - "useNativeCurrencyAsPrimaryCurrency": true, "hideZeroBalanceTokens": false, "petnamesEnabled": true, "redesignedConfirmationsEnabled": true, diff --git a/test/integration/onboarding/wallet-created.test.tsx b/test/integration/onboarding/wallet-created.test.tsx index 36ff7c8d3ecf..55be476839fe 100644 --- a/test/integration/onboarding/wallet-created.test.tsx +++ b/test/integration/onboarding/wallet-created.test.tsx @@ -10,6 +10,7 @@ import { jest.mock('../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../ui/store/background-connection'), submitRequestToBackground: jest.fn(), + callBackgroundMethod: jest.fn(), })); jest.mock('../../../ui/ducks/bridge/actions', () => ({ @@ -21,6 +22,7 @@ const mockedBackgroundConnection = jest.mocked(backgroundConnection); const backgroundConnectionMocked = { onNotification: jest.fn(), + callBackgroundMethod: jest.fn(), }; describe('Wallet Created Events', () => { @@ -34,7 +36,7 @@ describe('Wallet Created Events', () => { backgroundConnection: backgroundConnectionMocked, }); - expect(getByText('Wallet creation successful')).toBeInTheDocument(); + expect(getByText('Congratulations!')).toBeInTheDocument(); fireEvent.click(getByTestId('onboarding-complete-done')); @@ -69,6 +71,18 @@ describe('Wallet Created Events', () => { fireEvent.click(getByTestId('pin-extension-next')); + let onboardingPinExtensionMetricsEvent; + + await waitFor(() => { + onboardingPinExtensionMetricsEvent = + mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find( + (call) => call[0] === 'trackMetaMetricsEvent', + ); + expect(onboardingPinExtensionMetricsEvent?.[0]).toBe( + 'trackMetaMetricsEvent', + ); + }); + await waitFor(() => { expect( getByText( diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 625b6dcf6c83..a18f2e0b6944 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -210,7 +210,7 @@ export const createSwapsMockStore = () => { }, ], useCurrencyRateCheck: true, - currentCurrency: 'ETH', + currentCurrency: 'usd', currencyRates: { ETH: { conversionRate: 1, @@ -469,6 +469,23 @@ export const createSwapsMockStore = () => { decimals: 18, }, fee: 1, + isGasIncludedTrade: false, + approvalTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, + tradeTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, }, TEST_AGG_2: { trade: { @@ -503,6 +520,36 @@ export const createSwapsMockStore = () => { decimals: 18, }, fee: 1, + isGasIncludedTrade: false, + approvalTxFees: { + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 2310003200, maxPriorityFeePerGas: 513154852 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, + tradeTxFees: { + feeEstimate: 42000000000000, + fees: [ + { + maxFeePerGas: 2310003200, + maxPriorityFeePerGas: 513154852, + tokenFees: [ + { + token: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + decimals: 18, + }, + balanceNeededToken: '0x426dc933c2e5a', + }, + ], + }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, }, }, fetchParams: { @@ -658,16 +705,23 @@ export const createSwapsMockStore = () => { export const createBridgeMockStore = ( featureFlagOverrides = {}, bridgeSliceOverrides = {}, + bridgeStateOverrides = {}, + metamaskStateOverrides = {}, ) => { const swapsStore = createSwapsMockStore(); return { ...swapsStore, bridge: { - toChain: null, + toChainId: null, ...bridgeSliceOverrides, }, metamask: { ...swapsStore.metamask, + ...mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.LINEA_MAINNET }, + ), + ...metamaskStateOverrides, bridgeState: { ...(swapsStore.metamask.bridgeState ?? {}), bridgeFeatureFlags: { @@ -676,11 +730,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + ...bridgeStateOverrides, }, - ...mockNetworkState( - { chainId: CHAIN_IDS.MAINNET }, - { chainId: CHAIN_IDS.LINEA_MAINNET }, - ), }, }; }; diff --git a/test/jest/setup.js b/test/jest/setup.js index 0ee19a4d61b8..77fbb92783bc 100644 --- a/test/jest/setup.js +++ b/test/jest/setup.js @@ -1,6 +1,14 @@ // This file is for Jest-specific setup only and runs before our Jest tests. import '../helpers/setup-after-helper'; +jest.mock('webextension-polyfill', () => { + return { + runtime: { + getManifest: () => ({ manifest_version: 2 }), + }, + }; +}); + jest.mock('../../ui/hooks/usePetnamesEnabled', () => ({ usePetnamesEnabled: () => false, })); diff --git a/types/single-call-balance-checker-abi.d.ts b/types/single-call-balance-checker-abi.d.ts new file mode 100644 index 000000000000..ae42a6e98775 --- /dev/null +++ b/types/single-call-balance-checker-abi.d.ts @@ -0,0 +1,6 @@ +declare module 'single-call-balance-checker-abi' { + import { ContractInterface } from '@ethersproject/contracts'; + + const SINGLE_CALL_BALANCES_ABI: ContractInterface; + export default SINGLE_CALL_BALANCES_ABI; +} diff --git a/ui/components/app/alert-system/alert-modal/alert-modal.test.tsx b/ui/components/app/alert-system/alert-modal/alert-modal.test.tsx index 23d70b213edd..1c54dddbae55 100644 --- a/ui/components/app/alert-system/alert-modal/alert-modal.test.tsx +++ b/ui/components/app/alert-system/alert-modal/alert-modal.test.tsx @@ -147,11 +147,14 @@ describe('AlertModal', () => { it('sets the alert as confirmed when checkbox is called', () => { const setAlertConfirmedMock = jest.fn(); + const dangerAlertMock = alertsMock.find( + (alert) => alert.key === DATA_ALERT_KEY_MOCK, + ); const useAlertsSpy = jest.spyOn(useAlertsModule, 'default'); const newMockStore = configureMockStore([])({ ...STATE_MOCK, confirmAlerts: { - alerts: { [OWNER_ID_MOCK]: [alertsMock[1]] }, + alerts: { [OWNER_ID_MOCK]: [dangerAlertMock] }, confirmed: { [OWNER_ID_MOCK]: { [DATA_ALERT_KEY_MOCK]: false, @@ -162,10 +165,10 @@ describe('AlertModal', () => { (useAlertsSpy as jest.Mock).mockReturnValue({ setAlertConfirmed: setAlertConfirmedMock, - alerts: [alertsMock[1]], + alerts: [dangerAlertMock], generalAlerts: [], - fieldAlerts: [alertsMock[1]], - getFieldAlerts: () => [], + fieldAlerts: [dangerAlertMock], + getFieldAlerts: () => [dangerAlertMock], isAlertConfirmed: () => false, }); const { getByTestId } = renderWithProvider( @@ -233,11 +236,11 @@ describe('AlertModal', () => { ); expect(queryByTestId('alert-modal-acknowledge-checkbox')).toBeNull(); - expect(queryByTestId('alert-modal-button')).toBeNull(); + expect(queryByTestId('alert-modal-button')).toBeInTheDocument(); expect(getByText(ACTION_LABEL_MOCK)).toBeInTheDocument(); }); - it('renders acknowledge button and checkbox for non-blocking alerts', () => { + it('renders checkbox for non-blocking alerts', () => { const { getByTestId } = renderWithProvider( {customDetails ?? ( @@ -210,12 +208,11 @@ export function AcknowledgeCheckboxBase({ return ( {t('gotIt')} @@ -313,7 +306,7 @@ export function AlertModal({ customDetails, customAcknowledgeCheckbox, customAcknowledgeButton, - enableProvider = true, + showCloseIcon = true, }: AlertModalProps) { const { isAlertConfirmed, setAlertConfirmed, alerts } = useAlerts(ownerId); const { trackAlertRender } = useAlertMetrics(); @@ -348,13 +341,14 @@ export function AlertModal({ @@ -373,19 +367,13 @@ export function AlertModal({ onCheckboxClick={handleCheckboxClick} /> )} - {enableProvider ? ( - - ) : null} {customAcknowledgeButton ?? ( diff --git a/ui/components/app/alert-system/alert-modal/index.scss b/ui/components/app/alert-system/alert-modal/index.scss index 722dbf763446..c9100ae95345 100644 --- a/ui/components/app/alert-system/alert-modal/index.scss +++ b/ui/components/app/alert-system/alert-modal/index.scss @@ -8,7 +8,5 @@ &__acknowledge-checkbox { @include design-system.H6; - - padding-top: 2px; } } diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.test.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.test.tsx index ad365d78a9d2..c5e5923ac845 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.test.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.test.tsx @@ -68,7 +68,7 @@ describe('ConfirmAlertModal', () => { mockStore, ); - expect(getByText('Your assets may be at risk')).toBeInTheDocument(); + expect(getByText('This request is suspicious')).toBeInTheDocument(); }); it('disables submit button when confirm modal is not acknowledged', () => { @@ -101,41 +101,37 @@ describe('ConfirmAlertModal', () => { expect(onSubmitMock).toHaveBeenCalledTimes(1); }); - // todo: following 2 tests have been temporarily commented out - // we can un-comment as we add more alert providers - - // it('calls open multiple alert modal when review alerts link is clicked', () => { - // const { getByTestId } = renderWithProvider( - // , - // mockStore, - // ); - - // fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); - // expect(getByTestId('alert-modal-button')).toBeInTheDocument(); - // }); - - // describe('when there are multiple alerts', () => { - // it('renders the next alert when the "Got it" button is clicked', () => { - // const mockStoreAcknowledgeAlerts = configureMockStore([])({ - // ...STATE_MOCK, - // confirmAlerts: { - // alerts: { [OWNER_ID_MOCK]: alertsMock }, - // confirmed: { - // [OWNER_ID_MOCK]: { - // [FROM_ALERT_KEY_MOCK]: true, - // [DATA_ALERT_KEY_MOCK]: false, - // }, - // }, - // }, - // }); - // const { getByTestId, getByText } = renderWithProvider( - // , - // mockStoreAcknowledgeAlerts, - // ); - // fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); - // fireEvent.click(getByTestId('alert-modal-button')); - - // expect(getByText(DATA_ALERT_MESSAGE_MOCK)).toBeInTheDocument(); - // }); - // }); + it('calls open multiple alert modal when review alerts link is clicked', () => { + const { getByTestId } = renderWithProvider( + , + mockStore, + ); + + fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts')); + expect(getByTestId('alert-modal-button')).toBeInTheDocument(); + }); + + describe('when there are multiple alerts', () => { + it('renders the next alert when the "Got it" button is clicked', () => { + const mockStoreAcknowledgeAlerts = configureMockStore([])({ + ...STATE_MOCK, + confirmAlerts: { + alerts: { [OWNER_ID_MOCK]: alertsMock }, + confirmed: { + [OWNER_ID_MOCK]: { + [FROM_ALERT_KEY_MOCK]: true, + [DATA_ALERT_KEY_MOCK]: false, + }, + }, + }, + }); + const { getByTestId, getByText } = renderWithProvider( + , + mockStoreAcknowledgeAlerts, + ); + fireEvent.click(getByTestId('alert-modal-button')); + + expect(getByText(DATA_ALERT_MESSAGE_MOCK)).toBeInTheDocument(); + }); + }); }); diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index d46595e6b6be..f84c8113ae1e 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { SecurityProvider } from '../../../../../shared/constants/security-provider'; import { Box, Button, @@ -15,6 +14,7 @@ import { } from '../../../component-library'; import { AlignItems, + Severity, TextAlign, TextVariant, } from '../../../../helpers/constants/design-system'; @@ -87,11 +87,10 @@ function ConfirmDetails({ <> - {t('confirmAlertModalDetails')} + {t('confirmationAlertModalDetails')} - + {t('alertModalReviewAllAlerts')} @@ -122,23 +117,32 @@ export function ConfirmAlertModal({ ownerId, }: ConfirmAlertModalProps) { const t = useI18nContext(); - const { alerts, unconfirmedDangerAlerts } = useAlerts(ownerId); + const { fieldAlerts, alerts, hasUnconfirmedFieldDangerAlerts } = + useAlerts(ownerId); const [confirmCheckbox, setConfirmCheckbox] = useState(false); - // if there are multiple alerts, show the multiple alert modal + const hasDangerBlockingAlerts = fieldAlerts.some( + (alert) => alert.severity === Severity.Danger && alert.isBlocking, + ); + + // if there are unconfirmed danger alerts, show the multiple alert modal const [multipleAlertModalVisible, setMultipleAlertModalVisible] = - useState(unconfirmedDangerAlerts.length > 1); + useState(hasUnconfirmedFieldDangerAlerts); const handleCloseMultipleAlertModal = useCallback( (request?: { recursive?: boolean }) => { setMultipleAlertModalVisible(false); - if (request?.recursive) { + if ( + request?.recursive || + hasUnconfirmedFieldDangerAlerts || + hasDangerBlockingAlerts + ) { onClose(); } }, - [onClose], + [onClose, hasUnconfirmedFieldDangerAlerts, hasDangerBlockingAlerts], ); const handleOpenMultipleAlertModal = useCallback(() => { @@ -155,6 +159,7 @@ export function ConfirmAlertModal({ ownerId={ownerId} onFinalAcknowledgeClick={handleCloseMultipleAlertModal} onClose={handleCloseMultipleAlertModal} + showCloseIcon={false} /> ); } @@ -171,13 +176,9 @@ export function ConfirmAlertModal({ onAcknowledgeClick={onClose} alertKey={selectedAlert.key} onClose={onClose} - customTitle={t('confirmAlertModalTitle')} + customTitle={t('confirmationAlertModalTitle')} customDetails={ - selectedAlert.provider === SecurityProvider.Blockaid ? ( - SecurityProvider.Blockaid - ) : ( - - ) + } customAcknowledgeCheckbox={ } - enableProvider={false} /> ); } diff --git a/ui/components/app/alert-system/general-alert/general-alert.tsx b/ui/components/app/alert-system/general-alert/general-alert.tsx index 3ba74445acef..5ac2b2a335fb 100644 --- a/ui/components/app/alert-system/general-alert/general-alert.tsx +++ b/ui/components/app/alert-system/general-alert/general-alert.tsx @@ -27,7 +27,7 @@ export type GeneralAlertProps = { provider?: SecurityProvider; reportUrl?: string; severity: AlertSeverity; - title: string; + title?: string; }; function ReportLink({ diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx index 0c3e810a5657..3d176e57ccd0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx @@ -4,6 +4,7 @@ import { fireEvent } from '@testing-library/react'; import { Severity } from '../../../../helpers/constants/design-system'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import mockState from '../../../../../test/data/mock-state.json'; +import * as useAlertsModule from '../../../../hooks/useAlerts'; import { MultipleAlertModal, MultipleAlertModalProps, @@ -70,6 +71,70 @@ describe('MultipleAlertModal', () => { onClose: onCloseMock, }; + const mockStoreAcknowledgeAlerts = configureMockStore([])({ + ...STATE_MOCK, + confirmAlerts: { + alerts: { [OWNER_ID_MOCK]: alertsMock }, + confirmed: { + [OWNER_ID_MOCK]: { + [FROM_ALERT_KEY_MOCK]: true, + [DATA_ALERT_KEY_MOCK]: true, + [CONTRACT_ALERT_KEY_MOCK]: false, + }, + }, + }, + }); + + it('defaults to the first alert if the selected alert is not found', async () => { + const setAlertConfirmedMock = jest.fn(); + const useAlertsSpy = jest.spyOn(useAlertsModule, 'default'); + const dangerAlertMock = alertsMock.find( + (alert) => alert.key === DATA_ALERT_KEY_MOCK, + ); + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: alertsMock, + generalAlerts: [], + fieldAlerts: alertsMock, + getFieldAlerts: () => alertsMock, + isAlertConfirmed: () => false, + }); + + const { getByText, queryByText, rerender } = renderWithProvider( + , + mockStore, + ); + + // shows the contract alert + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + + // Update the mock to return only the data alert + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: [dangerAlertMock], + generalAlerts: [], + fieldAlerts: [dangerAlertMock], + getFieldAlerts: () => [dangerAlertMock], + isAlertConfirmed: () => false, + }); + + // Rerender the component to apply the updated mock + rerender( + , + ); + + // verifies the data alert is shown + expect(queryByText(alertsMock[0].message)).not.toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + useAlertsSpy.mockRestore(); + }); + it('renders the multiple alert modal', () => { const { getByTestId } = renderWithProvider( , @@ -80,19 +145,6 @@ describe('MultipleAlertModal', () => { }); it('invokes the onFinalAcknowledgeClick when the button is clicked', () => { - const mockStoreAcknowledgeAlerts = configureMockStore([])({ - ...STATE_MOCK, - confirmAlerts: { - alerts: { [OWNER_ID_MOCK]: alertsMock }, - confirmed: { - [OWNER_ID_MOCK]: { - [FROM_ALERT_KEY_MOCK]: true, - [DATA_ALERT_KEY_MOCK]: true, - [CONTRACT_ALERT_KEY_MOCK]: true, - }, - }, - }, - }); const { getByTestId } = renderWithProvider( { expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); - it('render the next alert when the "Got it" button is clicked', () => { - const mockStoreAcknowledgeAlerts = configureMockStore([])({ - ...STATE_MOCK, - confirmAlerts: { - alerts: { [OWNER_ID_MOCK]: alertsMock }, - confirmed: { - [OWNER_ID_MOCK]: { - [FROM_ALERT_KEY_MOCK]: true, - [DATA_ALERT_KEY_MOCK]: true, - [CONTRACT_ALERT_KEY_MOCK]: false, - }, - }, - }, - }); + it('renders the next alert when the "Got it" button is clicked', () => { const { getByTestId, getByText } = renderWithProvider( , mockStoreAcknowledgeAlerts, @@ -127,7 +166,37 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-button')); - expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + }); + + it('closes modal when the "Got it" button is clicked', () => { + onAcknowledgeClickMock.mockReset(); + const { getByTestId } = renderWithProvider( + , + mockStoreAcknowledgeAlerts, + ); + + fireEvent.click(getByTestId('alert-modal-button')); + + expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); + }); + + it('resets to the first alert if there are unconfirmed alerts and the final alert is acknowledged', () => { + const { getByTestId, getByText } = renderWithProvider( + , + mockStore, + ); + + fireEvent.click(getByTestId('alert-modal-button')); + + expect(getByText(alertsMock[0].message)).toBeInTheDocument(); }); describe('Navigation', () => { @@ -139,11 +208,15 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-next-button')); - expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); }); it('calls previous alert when the previous button is clicked', () => { - const selectSecondAlertMock = { ...defaultProps, alertKey: 'data' }; + const selectSecondAlertMock = { + ...defaultProps, + alertKey: CONTRACT_ALERT_KEY_MOCK, + }; const { getByTestId, getByText } = renderWithProvider( , mockStore, @@ -151,7 +224,7 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-back-button')); - expect(getByText(alertsMock[0].message)).toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); }); }); }); diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx index ae3e285efa00..62875bffcfe0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx @@ -30,6 +30,10 @@ export type MultipleAlertModalProps = { onClose: (request?: { recursive?: boolean }) => void; /** The unique identifier of the entity that owns the alert. */ ownerId: string; + /** Whether to show the close icon in the modal header. */ + showCloseIcon?: boolean; + /** Whether to skip the unconfirmed alerts validation and close the modal directly. */ + skipAlertNavigation?: boolean; }; function PreviousButton({ @@ -145,8 +149,10 @@ export function MultipleAlertModal({ onClose, onFinalAcknowledgeClick, ownerId, + showCloseIcon = true, + skipAlertNavigation = false, }: MultipleAlertModalProps) { - const { isAlertConfirmed, alerts } = useAlerts(ownerId); + const { isAlertConfirmed, fieldAlerts: alerts } = useAlerts(ownerId); const initialAlertIndex = alerts.findIndex( (alert: Alert) => alert.key === alertKey, @@ -156,7 +162,9 @@ export function MultipleAlertModal({ initialAlertIndex === -1 ? 0 : initialAlertIndex, ); - const selectedAlert = alerts[selectedIndex]; + // If the selected alert is not found, default to the first alert + const selectedAlert = alerts[selectedIndex] ?? alerts[0]; + const hasUnconfirmedAlerts = alerts.some( (alert: Alert) => !isAlertConfirmed(alert.key) && alert.severity === Severity.Danger, @@ -173,6 +181,11 @@ export function MultipleAlertModal({ }, []); const handleAcknowledgeClick = useCallback(() => { + if (skipAlertNavigation) { + onFinalAcknowledgeClick(); + return; + } + if (selectedIndex + 1 === alerts.length) { if (!hasUnconfirmedAlerts) { onFinalAcknowledgeClick(); @@ -189,13 +202,14 @@ export function MultipleAlertModal({ selectedIndex, alerts.length, hasUnconfirmedAlerts, + skipAlertNavigation, ]); return ( } + showCloseIcon={showCloseIcon} /> ); } 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 ebc78c3ab378..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,18 +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 { - showPrimaryCurrency, - showSecondaryCurrency, -} from '../../../../../shared/modules/currency-display.utils'; -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 { @@ -52,65 +37,44 @@ 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 { useNativeCurrencyAsPrimaryCurrency } = 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, numberOfDecimals: primaryNumberOfDecimals, - } = useUserPreferencedCurrency(PRIMARY, { ethNumberOfDecimals: 4 }); - const { - currency: secondaryCurrency, - numberOfDecimals: secondaryNumberOfDecimals, - } = useUserPreferencedCurrency(SECONDARY, { ethNumberOfDecimals: 4 }); - - const [primaryCurrencyDisplay, primaryCurrencyProperties] = - useCurrencyDisplay(balance, { - numberOfDecimals: primaryNumberOfDecimals, - currency: primaryCurrency, - }); + } = useUserPreferencedCurrency(PRIMARY, { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); - 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, @@ -124,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, ); @@ -148,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); @@ -155,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 && @@ -174,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 ? ( @@ -190,54 +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={ - showSecondaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) - ? secondaryCurrencyDisplay - : undefined - } - tokenSymbol={ - useNativeCurrencyAsPrimaryCurrency - ? primaryCurrencyProperties.suffix - : secondaryCurrencyProperties.suffix - } - secondary={ - showFiat && - showPrimaryCurrency( - isOriginalNativeSymbol, - useNativeCurrencyAsPrimaryCurrency, - ) - ? 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/nfts/nft-details/nft-full-image.tsx b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx index 64e2a3191c0e..42b4ddacbf95 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-full-image.tsx @@ -30,6 +30,7 @@ export default function NftFullImage() { const nfts = useSelector(getNfts); const nft = nfts.find( ({ address, tokenId }: { address: string; tokenId: string }) => + // @ts-expect-error TODO: Fix this type error by handling undefined parameters isEqualCaseInsensitive(address, asset) && id === tokenId.toString(), ); diff --git a/ui/components/app/assets/nfts/nfts-items/nfts-items.js b/ui/components/app/assets/nfts/nfts-items/nfts-items.js index ff5d7ea877bf..6e9e45e0b54a 100644 --- a/ui/components/app/assets/nfts/nfts-items/nfts-items.js +++ b/ui/components/app/assets/nfts/nfts-items/nfts-items.js @@ -157,7 +157,7 @@ export default function NftsItems({ const updateNftDropDownStateKey = (key, isExpanded) => { const newCurrentAccountState = { - ...nftsDropdownState[selectedAddress][chainId], + ...nftsDropdownState?.[selectedAddress]?.[chainId], [key]: !isExpanded, }; diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap index 4eeeb5603d46..dfed6aeffa98 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap @@ -52,7 +52,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-box--display-flex" >

@@ -67,7 +67,7 @@ exports[`Token Cell should match snapshot 1`] = ` 5.00

5 diff --git a/ui/components/app/assets/token-cell/token-cell.test.tsx b/ui/components/app/assets/token-cell/token-cell.test.tsx index 70714e9975f8..882c80964d5b 100644 --- a/ui/components/app/assets/token-cell/token-cell.test.tsx +++ b/ui/components/app/assets/token-cell/token-cell.test.tsx @@ -6,9 +6,13 @@ import { useSelector } from 'react-redux'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { getTokenList } from '../../../../selectors'; -import { getMultichainCurrentChainId } from '../../../../selectors/multichain'; +import { + getMultichainCurrentChainId, + getMultichainIsEvm, +} from '../../../../selectors/multichain'; import { useIsOriginalTokenSymbol } from '../../../../hooks/useIsOriginalTokenSymbol'; +import { getIntlLocale } from '../../../../ducks/locale/locale'; import TokenCell from '.'; jest.mock('react-redux', () => { @@ -100,6 +104,12 @@ describe('Token Cell', () => { if (selector === getMultichainCurrentChainId) { return '0x89'; } + if (selector === getMultichainIsEvm) { + return true; + } + if (selector === getIntlLocale) { + return 'en-US'; + } return undefined; }); (useTokenFiatAmount as jest.Mock).mockReturnValue('5.00'); 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/clear-metametrics-data/clear-metametrics-data.test.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx new file mode 100644 index 000000000000..5ce4ac7573dc --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import * as Actions from '../../../store/actions'; +import { DELETE_METAMETRICS_DATA_MODAL_CLOSE } from '../../../store/actionConstants'; +import ClearMetaMetricsData from './clear-metametrics-data'; + +const mockCloseDeleteMetaMetricsDataModal = jest.fn().mockImplementation(() => { + return { + type: DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +}); + +jest.mock('../../../store/actions', () => ({ + createMetaMetricsDataDeletionTask: jest.fn(), +})); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDeleteMetaMetricsDataModal: () => { + return mockCloseDeleteMetaMetricsDataModal(); + }, + }; +}); + +describe('ClearMetaMetricsData', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + + expect(getByText('Delete MetaMetrics data?')).toBeInTheDocument(); + expect( + getByText( + 'We are about to remove all your MetaMetrics data. Are you sure?', + ), + ).toBeInTheDocument(); + }); + + it('should call createMetaMetricsDataDeletionTask when Clear button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Clear')).toBeEnabled(); + fireEvent.click(getByText('Clear')); + expect(Actions.createMetaMetricsDataDeletionTask).toHaveBeenCalledTimes(1); + }); + + it('should call hideDeleteMetaMetricsDataModal when Cancel button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Cancel')).toBeEnabled(); + fireEvent.click(getByText('Cancel')); + expect(mockCloseDeleteMetaMetricsDataModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx new file mode 100644 index 000000000000..019c115eceac --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx @@ -0,0 +1,130 @@ +import React, { useContext } from 'react'; +import { useDispatch } from 'react-redux'; +import { + hideDeleteMetaMetricsDataModal, + openDataDeletionErrorModal, +} from '../../../ducks/app/app'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '../../component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { createMetaMetricsDataDeletionTask } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +export default function ClearMetaMetricsData() { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + + const closeModal = () => { + dispatch(hideDeleteMetaMetricsDataModal()); + }; + + const deleteMetaMetricsData = async () => { + try { + await createMetaMetricsDataDeletionTask(); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.MetricsDataDeletionRequest, + }, + { + excludeMetaMetricsId: true, + }, + ); + } catch (error: unknown) { + dispatch(openDataDeletionErrorModal()); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.ErrorOccured, + }, + { + excludeMetaMetricsId: true, + }, + ); + } finally { + dispatch(hideDeleteMetaMetricsDataModal()); + } + }; + + return ( + + + + + + + {t('deleteMetaMetricsDataModalTitle')} + + + + + + {t('deleteMetaMetricsDataModalDesc')} + + + + + + + + + + + ); +} diff --git a/ui/components/app/clear-metametrics-data/index.ts b/ui/components/app/clear-metametrics-data/index.ts new file mode 100644 index 000000000000..b29aee18d564 --- /dev/null +++ b/ui/components/app/clear-metametrics-data/index.ts @@ -0,0 +1 @@ +export { default } from './clear-metametrics-data'; diff --git a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap index 292be0318ce6..ec2eacb0d44b 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap @@ -315,7 +315,9 @@ exports[`ConfirmInfoRowAddress renders appropriately with PetNames enabled 1`] =
-
+
diff --git a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap index b11a8d89bd87..c3958d886710 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ConfirmInfoExpandableRow should match snapshot 1`] = `
+ ) : ( <>
)} diff --git a/ui/components/app/confirm/info/row/currency.stories.tsx b/ui/components/app/confirm/info/row/currency.stories.tsx index 2a520ca5bd35..ca9926e5cc6b 100644 --- a/ui/components/app/confirm/info/row/currency.stories.tsx +++ b/ui/components/app/confirm/info/row/currency.stories.tsx @@ -12,7 +12,7 @@ const store = configureStore({ ...mockState.metamask, preferences: { ...mockState.metamask.preferences, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, }, }); @@ -29,7 +29,7 @@ const ConfirmInfoRowCurrencyStory = { control: 'text', }, }, - decorators: [(story: any) => {story()}] + decorators: [(story: any) => {story()}], }; export const DefaultStory = ({ variant, value }) => ( diff --git a/ui/components/app/confirm/info/row/currency.tsx b/ui/components/app/confirm/info/row/currency.tsx index 82ce82c3a113..51ce1fceba28 100644 --- a/ui/components/app/confirm/info/row/currency.tsx +++ b/ui/components/app/confirm/info/row/currency.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { PRIMARY } from '../../../../../helpers/constants/common'; import { AlignItems, Display, @@ -38,7 +37,7 @@ export const ConfirmInfoRowCurrency = ({ {currency ? ( ) : ( - + )} ); diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx index e2b16b00e37d..7616ccae5f21 100644 --- a/ui/components/app/confirm/info/row/row.tsx +++ b/ui/components/app/confirm/info/row/row.tsx @@ -90,6 +90,7 @@ export const ConfirmInfoRow: React.FC = ({ flexDirection={FlexDirection.Row} justifyContent={JustifyContent.spaceBetween} flexWrap={FlexWrap.Wrap} + alignItems={AlignItems.center} backgroundColor={BACKGROUND_COLORS[variant]} borderRadius={BorderRadius.LG} marginTop={2} @@ -117,7 +118,7 @@ export const ConfirmInfoRow: React.FC = ({ {label} {labelChildren} - {tooltip && tooltip.length > 0 && ( + {!labelChildren && tooltip?.length && ( { + return { + type: DATA_DELETION_ERROR_MODAL_CLOSE, + }; + }); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDataDeletionErrorModal: () => { + return mockCloseDeleteMetaMetricsErrorModal(); + }, + }; +}); + +describe('DataDeletionErrorModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + + expect( + getByText('We are unable to delete this data right now'), + ).toBeInTheDocument(); + expect( + getByText( + "This request can't be completed right now due to an analytics system server issue, please try again later", + ), + ).toBeInTheDocument(); + }); + + it('should call hideDeleteMetaMetricsDataModal when Ok button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(, store); + expect(getByText('Ok')).toBeEnabled(); + fireEvent.click(getByText('Ok')); + expect(mockCloseDeleteMetaMetricsErrorModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx new file mode 100644 index 000000000000..0b6be4fa782b --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + Display, + FlexDirection, + AlignItems, + JustifyContent, + TextVariant, + BlockSize, + IconColor, + TextAlign, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + ModalOverlay, + ModalContent, + ModalHeader, + Modal, + Box, + Text, + ModalFooter, + Button, + IconName, + ButtonVariant, + Icon, + IconSize, + ButtonSize, +} from '../../component-library'; +import { hideDataDeletionErrorModal } from '../../../ducks/app/app'; + +export default function DataDeletionErrorModal() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + function closeModal() { + dispatch(hideDataDeletionErrorModal()); + } + + return ( + + + + + + + + {t('deleteMetaMetricsDataErrorTitle')} + + + + + + + {t('deleteMetaMetricsDataErrorDesc')} + + + + + + + + + + + ); +} diff --git a/ui/components/app/data-deletion-error-modal/index.ts b/ui/components/app/data-deletion-error-modal/index.ts new file mode 100644 index 000000000000..383efd7029b5 --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/index.ts @@ -0,0 +1 @@ +export { default } from './data-deletion-error-modal'; diff --git a/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap b/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap index 27ee8bbf6b69..8c44078d57cd 100644 --- a/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap +++ b/ui/components/app/incoming-trasaction-toggle/__snapshots__/incoming-transaction-toggle.test.js.snap @@ -6,7 +6,7 @@ exports[`IncomingTransactionToggle should render existing incoming transaction p class="mm-box mm-incoming-transaction-toggle" >

Show incoming transactions

diff --git a/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx b/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx index d1274075d056..0295fc1a044d 100644 --- a/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx +++ b/ui/components/app/incoming-trasaction-toggle/incoming-transaction-toggle.tsx @@ -60,7 +60,9 @@ const IncomingTransactionToggle = ({ return ( - {t('showIncomingTransactions')} + + {t('showIncomingTransactions')} + {t('showIncomingTransactionsExplainer')} diff --git a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js index 05bce9e841a0..8966fa01b749 100644 --- a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js +++ b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.test.js @@ -11,9 +11,7 @@ describe('CancelTransactionGasFee Component', () => { metamask: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), currencyRates: {}, - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, completedOnboarding: true, internalAccounts: mockState.metamask.internalAccounts, }, diff --git a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap index cab80e399a43..020adaa0c952 100644 --- a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap +++ b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap @@ -75,7 +75,7 @@ exports[`Customize Nonce should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
-
+
@@ -24,7 +26,9 @@ exports[`Name renders address with image 1`] = ` exports[`Name renders address with no saved name 1`] = `
-
+
@@ -44,7 +48,9 @@ exports[`Name renders address with no saved name 1`] = ` exports[`Name renders address with saved name 1`] = `
-
+
@@ -104,7 +110,9 @@ exports[`Name renders address with saved name 1`] = ` exports[`Name renders when no address value is passed 1`] = `
-
+
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap index 6ee9430c0fde..a6d0df79843d 100644 --- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -66,7 +66,9 @@ exports[`NameDetails renders proposed names 1`] = `
-
+
@@ -326,7 +328,9 @@ exports[`NameDetails renders when no address value is passed 1`] = `
-
+
@@ -509,7 +513,9 @@ exports[`NameDetails renders with no saved name 1`] = `
-
+
@@ -694,7 +700,9 @@ exports[`NameDetails renders with recognized name 1`] = `
-
+
@@ -884,7 +892,9 @@ exports[`NameDetails renders with saved name 1`] = `
-
+
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index d2684c188838..5af2851c8885 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -8,14 +8,14 @@ import React, { import { NameType } from '@metamask/name-controller'; import classnames from 'classnames'; import { toChecksumAddress } from 'ethereumjs-util'; -import { Icon, IconName, IconSize, Text } from '../../component-library'; +import { Box, Icon, IconName, IconSize, Text } from '../../component-library'; import { shortenAddress } from '../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { TextVariant } from '../../../helpers/constants/design-system'; +import { Display, TextVariant } from '../../../helpers/constants/design-system'; import { useDisplayName } from '../../../hooks/useDisplayName'; import Identicon from '../../ui/identicon'; import NameDetails from './name-details/name-details'; @@ -98,7 +98,7 @@ const Name = memo( const hasDisplayName = Boolean(name); return ( -
+ {!disableEdit && modalOpen && ( )} @@ -130,7 +130,7 @@ const Name = memo( )}
-
+ ); }, ); diff --git a/ui/components/app/permission-cell/permission-cell-status.js b/ui/components/app/permission-cell/permission-cell-status.js index 7f03a93a3584..7dcf32a3b2ee 100644 --- a/ui/components/app/permission-cell/permission-cell-status.js +++ b/ui/components/app/permission-cell/permission-cell-status.js @@ -25,6 +25,7 @@ import { AvatarGroup } from '../../multichain'; import { AvatarType } from '../../multichain/avatar-group/avatar-group.types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { formatDate } from '../../../helpers/utils/util'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; /** * Renders status of the given permission. Used by PermissionCell component. @@ -48,7 +49,7 @@ export const PermissionCellStatus = ({ const renderAccountsGroup = () => ( <> - {process.env.CHAIN_PERMISSIONS ? ( + {networks.length > 0 ? ( {networks?.map((network, index) => ( - {network.avatarName} + {network.name} ))} diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index 9f6637d66cf7..e5e8503e6c73 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -71,30 +71,18 @@ export default class PermissionPageContainerContent extends PureComponent { paddingBottom={4} > - {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)} + , + ])} selectedAccount.address, + ); + + const permittedChainsPermission = + _request.permissions?.[PermissionNames.permittedChains]; + const approvedChainIds = permittedChainsPermission?.caveats.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value; + const request = { ..._request, permissions: { ..._request.permissions }, - ...(_request.permissions.eth_accounts && { - approvedAccounts: selectedAccounts.map( - (selectedAccount) => selectedAccount.address, - ), - }), - ...(_request.permissions.permittedChains && { - approvedChainIds: _request.permissions?.permittedChains?.caveats.find( - (caveat) => caveat.type === 'restrictNetworkSwitching', - )?.value, + ...(_request.permissions?.eth_accounts && { approvedAccounts }), + ...(_request.permissions?.[PermissionNames.permittedChains] && { + approvedChainIds, }), }; diff --git a/ui/components/app/snaps/copyable/index.scss b/ui/components/app/snaps/copyable/index.scss index 7cfed455aade..4fa96c5d7604 100644 --- a/ui/components/app/snaps/copyable/index.scss +++ b/ui/components/app/snaps/copyable/index.scss @@ -3,7 +3,7 @@ transition: background-color background 0.2s; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } &:hover { @@ -11,7 +11,7 @@ color: var(--color-primary-default) !important; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } p, @@ -31,14 +31,14 @@ opacity: 0.75; & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } &:hover { background-color: var(--color-primary-muted); & .show-more__button { - background: linear-gradient(90deg, transparent 0%, var(--color-background-primary-muted) 33%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary-muted) 33%); } } } diff --git a/ui/components/app/snaps/show-more/show-more.js b/ui/components/app/snaps/show-more/show-more.js index d6939b9f546f..03bf0c0994fd 100644 --- a/ui/components/app/snaps/show-more/show-more.js +++ b/ui/components/app/snaps/show-more/show-more.js @@ -41,7 +41,7 @@ export const ShowMore = ({ children, className = '', ...props }) => { bottom: 0, right: 0, // Avoids see-through with muted colors - background: `linear-gradient(90deg, transparent 0%, var(--color-${BackgroundColor.backgroundDefault}) 33%)`, + background: `linear-gradient(90deg, transparent 0%, var(--color-${BackgroundColor.backgroundAlternative}) 33%)`, }} > +
+
+

+ Select network +

+
+
+ +
+ +
+
+
+ +
+
+
+
+ +`; + +exports[`AssetPickerModalNetwork should not show selected network when network prop is not passed in 1`] = ` + +
+
+
+ @@ -427,7 +427,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -446,7 +446,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-theme="light" >
@@ -515,7 +515,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -528,7 +528,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 @@ -835,7 +835,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >

@@ -854,7 +854,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -873,7 +873,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -892,7 +892,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -910,7 +910,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -929,7 +929,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-theme="light" >
@@ -998,7 +998,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` class="mm-box mm-box--display-flex" >

@@ -1011,7 +1011,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` data-testid="multichain-token-list-item-secondary-value" />

0 diff --git a/ui/pages/asset/components/asset-page.test.tsx b/ui/pages/asset/components/asset-page.test.tsx index bf616d0aaac9..35721a30a1c2 100644 --- a/ui/pages/asset/components/asset-page.test.tsx +++ b/ui/pages/asset/components/asset-page.test.tsx @@ -49,9 +49,7 @@ describe('AssetPage', () => { }, }, useCurrencyRateCheck: true, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: { accounts: { 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index a07cdaca2d48..bb3f129bade8 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -48,7 +48,12 @@ import { JustifyContent, } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; -import { Box, Icon, IconName } from '../../../components/component-library'; +import { + Box, + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF @@ -115,7 +120,11 @@ const TokenButtons = ({ + } label={t('buyAndSell')} data-testid="token-overview-buy" @@ -144,7 +153,11 @@ const TokenButtons = ({ + } label={t('stake')} data-testid="token-overview-mmi-stake" @@ -163,6 +176,7 @@ const TokenButtons = ({ } label={t('portfolio')} @@ -215,6 +229,7 @@ const TokenButtons = ({ } label={t('send')} @@ -229,6 +244,7 @@ const TokenButtons = ({ } onClick={() => { @@ -281,7 +297,11 @@ const TokenButtons = ({ className="token-overview__button" data-testid="token-overview-bridge" Icon={ - + } label={t('bridge')} onClick={() => { diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts index da2637b1f5f4..07c35ae57749 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/ui/pages/bridge/bridge.util.test.ts @@ -1,6 +1,6 @@ import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import { fetchBridgeFeatureFlags } from './bridge.util'; +import { fetchBridgeFeatureFlags, fetchBridgeTokens } from './bridge.util'; jest.mock('../../../shared/lib/fetch-with-cache'); @@ -79,4 +79,66 @@ describe('Bridge utils', () => { await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); }); }); + + describe('fetchBridgeTokens', () => { + it('should fetch bridge tokens successfully', async () => { + const mockResponse = [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + symbol: 'DEF', + }, + { + address: '0x124', + symbol: 'JKL', + decimals: 16, + }, + ]; + + (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeTokens('0xa'); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: 'fetchBridgeTokens', + }); + + expect(result).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: './images/eth_logo.svg', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 16, + symbol: 'ABC', + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (fetchWithCache as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); + }); + }); }); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index 0f72b75a0787..915a933e7c02 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -1,4 +1,4 @@ -import { add0x } from '@metamask/utils'; +import { Hex, add0x } from '@metamask/utils'; import { BridgeFeatureFlagsKey, BridgeFeatureFlags, @@ -12,7 +12,19 @@ import { import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { validateData } from '../../../shared/lib/swaps-utils'; -import { decimalToHex } from '../../../shared/modules/conversion.utils'; +import { + decimalToHex, + hexToDecimal, +} from '../../../shared/modules/conversion.utils'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, +} from '../../../shared/constants/swaps'; +import { TOKEN_VALIDATORS } from '../swaps/swaps.util'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../shared/modules/swaps.utils'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; @@ -31,17 +43,17 @@ export type FeatureFlagResponse = { }; // End of copied types -type Validator = { - property: keyof T; +type Validator = { + property: keyof ExpectedResponse | string; type: string; - validator: (value: unknown) => boolean; + validator: (value: DataToValidate) => boolean; }; -const validateResponse = ( - validators: Validator[], +const validateResponse = ( + validators: Validator[], data: unknown, urlUsed: string, -): data is T => { +): data is ExpectedResponse => { return validateData(validators, data, urlUsed); }; @@ -55,7 +67,7 @@ export async function fetchBridgeFeatureFlags(): Promise { }); if ( - validateResponse( + validateResponse( [ { property: BridgeFlag.EXTENSION_SUPPORT, @@ -104,3 +116,46 @@ export async function fetchBridgeFeatureFlags(): Promise { [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], }; } + +// Returns a list of enabled (unblocked) tokens +export async function fetchBridgeTokens( + chainId: Hex, +): Promise> { + // TODO make token api v2 call + const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToDecimal( + chainId, + )}`; + const tokens = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeTokens', + }); + + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + const transformedTokens: Record = {}; + if (nativeToken) { + transformedTokens[nativeToken.address] = nativeToken; + } + + tokens.forEach((token: SwapsTokenObject) => { + if ( + validateResponse( + TOKEN_VALIDATORS, + token, + url, + ) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ) { + transformedTokens[token.address] = token; + } + }); + return transformedTokens; +} diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 4352ff359742..a73cfa370681 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -22,6 +22,8 @@ setBackgroundConnection({ getNetworkConfigurationByNetworkClientId: jest .fn() .mockResolvedValue({ chainId: '0x1' }), + setBridgeFeatureFlags: jest.fn(), + selectSrcNetwork: jest.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index f216a52ec71d..e4b5c0b930d4 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -28,10 +28,14 @@ import { BlockSize, } from '../../helpers/constants/design-system'; import { getIsBridgeEnabled } from '../../selectors'; +import useBridging from '../../hooks/bridge/useBridging'; import { PrepareBridgePage } from './prepare/prepare-bridge-page'; const CrossChainSwap = () => { const t = useContext(I18nContext); + + useBridging(); + const history = useHistory(); const dispatch = useDispatch(); @@ -40,7 +44,6 @@ const CrossChainSwap = () => { const redirectToDefaultRoute = async () => { history.push({ pathname: DEFAULT_ROUTE, - // @ts-expect-error - property 'state' does not exist on type PartialPath. state: { stayOnHomePage: true }, }); dispatch(clearSwapsState()); diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js index fd6585381a27..88d7c8deb40b 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.container.js @@ -10,9 +10,7 @@ import { decryptMsgInline, } from '../../store/actions'; import { - conversionRateSelector, getCurrentCurrency, - getPreferences, getTargetAccountWithSendEtherInfo, unconfirmedTransactionsListSelector, } from '../../selectors'; @@ -21,13 +19,12 @@ import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getNativeCurrency } from '../../ducks/metamask/metamask'; import ConfirmDecryptMessage from './confirm-decrypt-message.component'; +// ConfirmDecryptMessage component is not used in codebase, removing usage of useNativeCurrencyAsPrimaryCurrency function mapStateToProps(state) { const { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = cloneDeep(unconfirmedTransactions[0]); @@ -43,9 +40,7 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), + conversionRate: null, mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), currentCurrency: getCurrentCurrency(state), diff --git a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap index 05cee4e7016f..b6ae6ed60c6f 100644 --- a/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap +++ b/ui/pages/confirm-encryption-public-key/__snapshots__/confirm-encryption-public-key.component.test.js.snap @@ -208,252 +208,6 @@ exports[`ConfirmDecryptMessage Component should match snapshot when preference i -

- - test - -
- would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you. - - -
- -
-
- -
-
-`; - -exports[`ConfirmDecryptMessage Component should match snapshot when preference is Fiat currency 1`] = ` -
-
-
-
-
- Request encryption public key -
-
-
-
-
-
- -
-
- - T - -
- - -
diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js index 1595576a4fac..dd84bea68360 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -10,8 +10,6 @@ import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics' import SiteOrigin from '../../components/ui/site-origin'; import { Numeric } from '../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../shared/constants/common'; -import { formatCurrency } from '../../helpers/utils/confirm-tx.util'; -import { getValueFromWeiHex } from '../../../shared/modules/conversion.utils'; export default class ConfirmEncryptionPublicKey extends Component { static contextTypes = { @@ -34,8 +32,6 @@ export default class ConfirmEncryptionPublicKey extends Component { subjectMetadata: PropTypes.object, mostRecentOverviewPage: PropTypes.string.isRequired, nativeCurrency: PropTypes.string.isRequired, - currentCurrency: PropTypes.string.isRequired, - conversionRate: PropTypes.number, }; renderHeader = () => { @@ -73,30 +69,20 @@ export default class ConfirmEncryptionPublicKey extends Component { renderBalance = () => { const { - conversionRate, nativeCurrency, - currentCurrency, fromAccount: { balance }, } = this.props; const { t } = this.context; - const nativeCurrencyBalance = conversionRate - ? formatCurrency( - getValueFromWeiHex({ - value: balance, - fromCurrency: nativeCurrency, - toCurrency: currentCurrency, - conversionRate, - numberOfDecimals: 6, - toDenomination: EtherDenomination.ETH, - }), - currentCurrency, - ) - : new Numeric(balance, 16, EtherDenomination.WEI) - .toDenomination(EtherDenomination.ETH) - .round(6) - .toBase(10) - .toString(); + const nativeCurrencyBalance = new Numeric( + balance, + 16, + EtherDenomination.WEI, + ) + .toDenomination(EtherDenomination.ETH) + .round(6) + .toBase(10) + .toString(); return (
@@ -104,9 +90,7 @@ export default class ConfirmEncryptionPublicKey extends Component { {`${t('balance')}:`}
- {`${nativeCurrencyBalance} ${ - conversionRate ? currentCurrency?.toUpperCase() : nativeCurrency - }`} + {`${nativeCurrencyBalance} ${nativeCurrency}`}
); diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js index 771a5a5f5ef7..3edc10e9d313 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.test.js @@ -61,19 +61,6 @@ describe('ConfirmDecryptMessage Component', () => { ).toMatchInlineSnapshot(`"966.987986 ABC"`); }); - it('should match snapshot when preference is Fiat currency', () => { - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - expect( - container.querySelector('.request-encryption-public-key__balance-value') - .textContent, - ).toMatchInlineSnapshot(`"1520956.064158 DEF"`); - }); - it('should match snapshot when there is no txData', () => { const newProps = { ...baseProps, diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js index 489d7d088033..554ba41fdaa4 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js @@ -9,11 +9,8 @@ import { } from '../../store/actions'; import { - conversionRateSelector, unconfirmedTransactionsListSelector, getTargetAccountWithSendEtherInfo, - getPreferences, - getCurrentCurrency, } from '../../selectors'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; @@ -26,8 +23,6 @@ function mapStateToProps(state) { metamask: { subjectMetadata = {} }, } = state; - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); - const unconfirmedTransactions = unconfirmedTransactionsListSelector(state); const txData = unconfirmedTransactions[0]; @@ -43,12 +38,8 @@ function mapStateToProps(state) { fromAccount, requester: null, requesterAddress: null, - conversionRate: useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateSelector(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), nativeCurrency: getNativeCurrency(state), - currentCurrency: getCurrentCurrency(state), }; } diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js index c80ef735222b..c1d4ae838301 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-gas-display.test.js @@ -39,9 +39,7 @@ const render = async ({ transactionProp = {}, contextProps = {} } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js index 34440be28693..70d5ed1070f4 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.js @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { getIsMainnet, - getPreferences, getUnapprovedTransactions, getUseCurrencyRateCheck, transactionFeeSelector, @@ -34,7 +33,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { // state selectors const isMainnet = useSelector(getIsMainnet); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const transactionData = useDraftTransactionWithTxParams(); const txData = useSelector((state) => txDataSelector(state)); @@ -108,7 +106,7 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => {
) @@ -119,7 +117,6 @@ const ConfirmLegacyGasDisplay = ({ 'data-testid': dataTestId } = {}) => { { key="editGasSubTextFeeAmount" type={PRIMARY} value={estimatedHexMaxFeeTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js index df5b9ea0e50f..4952fb87edca 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js +++ b/ui/pages/confirmations/components/confirm-gas-display/confirm-legacy-gas-display/confirm-legacy-gas-display.test.js @@ -21,9 +21,6 @@ const mmState = { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, }, confirmTransaction: { txData: { diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js index 5b1e505ddc14..5d3065e8a3d3 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.test.js @@ -11,9 +11,7 @@ describe('Confirm Detail Row Component', () => { metamask: { currencyRates: {}, ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, internalAccounts: defaultMockState.metamask.internalAccounts, }, }; diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js index e1219d299288..da99ade8210c 100644 --- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js +++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.js @@ -29,7 +29,6 @@ const ConfirmSubTitle = ({ if (subtitleComponent) { return subtitleComponent; } - return ( { expect(getByText('Review alerts')).toBeDisabled(); }); - it('sets the alert modal visible when the review alerts button is clicked', () => { - const { getByTestId } = render(stateWithAlertsMock); - fireEvent.click(getByTestId('confirm-footer-button')); - expect(getByTestId('confirm-alert-modal-submit-button')).toBeDefined(); + it('renders the "review alert" button when there are unconfirmed alerts', () => { + const { getByText } = render(stateWithAlertsMock); + expect(getByText('Review alert')).toBeInTheDocument(); + }); + + it('renders the "confirm" button when there are confirmed danger alerts', () => { + const stateWithConfirmedDangerAlertMock = createStateWithAlerts( + alertsMock, + { + [KEY_ALERT_KEY_MOCK]: true, + }, + ); + const { getByText } = render(stateWithConfirmedDangerAlertMock); + expect(getByText('Confirm')).toBeInTheDocument(); }); it('renders the "confirm" button when there are no alerts', () => { const { getByText } = render(); expect(getByText('Confirm')).toBeInTheDocument(); }); + + it('sets the alert modal visible when the review alerts button is clicked', () => { + const { getByTestId } = render(stateWithAlertsMock); + fireEvent.click(getByTestId('confirm-footer-button')); + expect(getByTestId('alert-modal-button')).toBeDefined(); + }); }); }); diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index d40e72144612..cc9b39609030 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -39,6 +39,7 @@ import { import { useConfirmContext } from '../../../context/confirm'; import { getConfirmationSender } from '../utils'; import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics'; +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; import { Severity } from '../../../../../helpers/constants/design-system'; export type OnCancelHandler = ({ @@ -47,6 +48,21 @@ export type OnCancelHandler = ({ location: MetaMetricsEventLocation; }) => void; +function reviewAlertButtonText( + unconfirmedDangerAlerts: Alert[], + t: ReturnType, +) { + if (unconfirmedDangerAlerts.length === 1) { + return t('reviewAlert'); + } + + if (unconfirmedDangerAlerts.length > 1) { + return t('reviewAlerts'); + } + + return t('confirm'); +} + function getButtonDisabledState( hasUnconfirmedDangerAlerts: boolean, hasBlockingAlerts: boolean, @@ -79,10 +95,15 @@ const ConfirmButton = ({ const [confirmModalVisible, setConfirmModalVisible] = useState(false); - const { dangerAlerts, hasDangerAlerts, hasUnconfirmedDangerAlerts } = - useAlerts(alertOwnerId); + const { + hasDangerAlerts, + hasUnconfirmedDangerAlerts, + fieldAlerts, + hasUnconfirmedFieldDangerAlerts, + unconfirmedFieldDangerAlerts, + } = useAlerts(alertOwnerId); - const hasDangerBlockingAlerts = dangerAlerts.some( + const hasDangerBlockingAlerts = fieldAlerts.some( (alert) => alert.severity === Severity.Danger && alert.isBlocking, ); @@ -116,9 +137,13 @@ const ConfirmButton = ({ )} onClick={handleOpenConfirmModal} size={ButtonSize.Lg} - startIconName={IconName.Danger} + startIconName={ + hasUnconfirmedFieldDangerAlerts + ? IconName.SecuritySearch + : IconName.Danger + } > - {dangerAlerts?.length > 0 ? t('reviewAlerts') : t('confirm')} + {reviewAlertButtonText(unconfirmedFieldDangerAlerts, t)} ) : ( +
+
+`; diff --git a/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap b/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap index 46bf53c2a7bc..4346963ead15 100644 --- a/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/header/__snapshots__/header.test.tsx.snap @@ -113,6 +113,170 @@ exports[`Header should match snapshot with signature confirmation 1`] = `
`; +exports[`Header should match snapshot with token transfer confirmation initiated in a dApp 1`] = ` +
+
+
+
+
+
+
+ + + + + +
+
+
+
+ G +
+
+
+

+

+ Goerli +

+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`Header should match snapshot with token transfer confirmation initiated in the wallet 1`] = ` +
+
+ +

+ Review +

+
+ +
+
+
+`; + exports[`Header should match snapshot with transaction confirmation 1`] = `
+

+ Review +

+
+ +
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx b/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx new file mode 100644 index 000000000000..ab0837a2fb95 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/advanced-details-button.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { getMockTokenTransferConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; +import configureStore from '../../../../../store/store'; +import { AdvancedDetailsButton } from './advanced-details-button'; + +const mockStore = getMockTokenTransferConfirmState({}); + +const render = () => { + const store = configureStore(mockStore); + return renderWithConfirmContextProvider(, store); +}; + +describe('', () => { + it('should match snapshot', async () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx b/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx new file mode 100644 index 000000000000..685f1417f064 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/advanced-details-button.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Box, + ButtonIcon, + ButtonIconSize, + IconName, +} from '../../../../../components/component-library'; +import { + BackgroundColor, + BorderRadius, + IconColor, +} from '../../../../../helpers/constants/design-system'; +import { setConfirmationAdvancedDetailsOpen } from '../../../../../store/actions'; +import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; + +export const AdvancedDetailsButton = () => { + const dispatch = useDispatch(); + + const showAdvancedDetails = useSelector( + selectConfirmationAdvancedDetailsOpen, + ); + + const setShowAdvancedDetails = (value: boolean): void => { + dispatch(setConfirmationAdvancedDetailsOpen(value)); + }; + + return ( + + { + setShowAdvancedDetails(!showAdvancedDetails); + }} + /> + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/header/header-info.tsx b/ui/pages/confirmations/components/confirm/header/header-info.tsx index 5001be21ff07..9cc50b0fe676 100644 --- a/ui/pages/confirmations/components/confirm/header/header-info.tsx +++ b/ui/pages/confirmations/components/confirm/header/header-info.tsx @@ -1,6 +1,6 @@ import { TransactionType } from '@metamask/transaction-controller'; import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { MetaMetricsEventCategory, MetaMetricsEventLocation, @@ -28,8 +28,6 @@ import Tooltip from '../../../../../components/ui/tooltip/tooltip'; import { MetaMetricsContext } from '../../../../../contexts/metametrics'; import { AlignItems, - BackgroundColor, - BorderRadius, Display, FlexDirection, FontWeight, @@ -40,32 +38,22 @@ import { } from '../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { getUseBlockie } from '../../../../../selectors'; -import { setConfirmationAdvancedDetailsOpen } from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; import { useBalance } from '../../../hooks/useBalance'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; -import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; import { SignatureRequestType } from '../../../types/confirm'; import { isSignatureTransactionType, REDESIGN_DEV_TRANSACTION_TYPES, } from '../../../utils/confirm'; -import { useConfirmContext } from '../../../context/confirm'; +import { AdvancedDetailsButton } from './advanced-details-button'; const HeaderInfo = () => { - const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const useBlockie = useSelector(getUseBlockie); const [showAccountInfo, setShowAccountInfo] = React.useState(false); - const showAdvancedDetails = useSelector( - selectConfirmationAdvancedDetailsOpen, - ); - - const setShowAdvancedDetails = (value: boolean): void => { - dispatch(setConfirmationAdvancedDetailsOpen(value)); - }; - const { currentConfirmation } = useConfirmContext(); const { senderAddress: fromAddress, senderName: fromName } = @@ -127,28 +115,7 @@ const HeaderInfo = () => { data-testid="header-info__account-details-button" /> - {isShowAdvancedDetailsToggle && ( - - { - setShowAdvancedDetails(!showAdvancedDetails); - }} - /> - - )} + {isShowAdvancedDetailsToggle && } { expect(container).toMatchSnapshot(); }); + it('should match snapshot with token transfer confirmation initiated in a dApp', () => { + const { container } = render( + getMockTokenTransferConfirmState({ + isWalletInitiatedConfirmation: false, + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with token transfer confirmation initiated in the wallet', () => { + const { container } = render( + getMockTokenTransferConfirmState({ + isWalletInitiatedConfirmation: true, + }), + ); + + expect(container).toMatchSnapshot(); + }); + it('contains network name and account name', () => { const { getByText } = render(); expect(getByText('Test Account')).toBeInTheDocument(); diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index 255384c58b82..9c113effe6a5 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -1,3 +1,7 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import React from 'react'; import { AvatarNetwork, @@ -14,15 +18,29 @@ import { TextVariant, } from '../../../../../helpers/constants/design-system'; import { getAvatarNetworkColor } from '../../../../../helpers/utils/accounts'; +import { useConfirmContext } from '../../../context/confirm'; import useConfirmationNetworkInfo from '../../../hooks/useConfirmationNetworkInfo'; import useConfirmationRecipientInfo from '../../../hooks/useConfirmationRecipientInfo'; +import { Confirmation } from '../../../types/confirm'; import HeaderInfo from './header-info'; +import { WalletInitiatedHeader } from './wallet-initiated-header'; const Header = () => { const { networkImageUrl, networkDisplayName } = useConfirmationNetworkInfo(); const { senderAddress: fromAddress, senderName: fromName } = useConfirmationRecipientInfo(); + const { currentConfirmation } = useConfirmContext(); + + if (currentConfirmation?.type === TransactionType.tokenMethodTransfer) { + const isWalletInitiated = + (currentConfirmation as TransactionMeta).origin === 'metamask'; + + if (isWalletInitiated) { + return ; + } + } + return ( { + const store = configureStore(state); + return renderWithConfirmContextProvider(, store); +}; + +describe('', () => { + it('should match snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx new file mode 100644 index 000000000000..ffc8e7549faf --- /dev/null +++ b/ui/pages/confirmations/components/confirm/header/wallet-initiated-header.tsx @@ -0,0 +1,72 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { AssetType } from '../../../../../../shared/constants/transaction'; +import { + Box, + ButtonIcon, + ButtonIconSize, + IconName, + Text, +} from '../../../../../components/component-library'; +import { clearConfirmTransaction } from '../../../../../ducks/confirm-transaction/confirm-transaction.duck'; +import { editExistingTransaction } from '../../../../../ducks/send'; +import { + AlignItems, + BackgroundColor, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../helpers/constants/design-system'; +import { SEND_ROUTE } from '../../../../../helpers/constants/routes'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { showSendTokenPage } from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; +import { AdvancedDetailsButton } from './advanced-details-button'; + +export const WalletInitiatedHeader = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const history = useHistory(); + + const { currentConfirmation } = useConfirmContext(); + + const handleBackButtonClick = useCallback(async () => { + const { id } = currentConfirmation; + + await dispatch(editExistingTransaction(AssetType.token, id.toString())); + dispatch(clearConfirmTransaction()); + dispatch(showSendTokenPage()); + + history.push(SEND_ROUTE); + }, [currentConfirmation, dispatch, history]); + + return ( + + + + {t('review')} + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap index 60bb488888b3..669dc0d5302d 100644 --- a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap @@ -2,144 +2,12 @@ exports[`Info renders info section for approve request 1`] = `
-
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -188,7 +56,7 @@ exports[`Info renders info section for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -252,12 +120,12 @@ exports[`Info renders info section for approve request 1`] = `
@@ -428,7 +296,7 @@ exports[`Info renders info section for contract interaction request 1`] = `
@@ -525,7 +393,7 @@ exports[`Info renders info section for contract interaction request 1`] = ` data-testid="gas-fee-section" >
@@ -589,12 +457,12 @@ exports[`Info renders info section for contract interaction request 1`] = `
-
+
@@ -824,73 +694,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -939,7 +743,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="gas-fee-section" >
@@ -1003,12 +807,12 @@ exports[`Info renders info section for setApprovalForAll request 1`] = `
renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl" data-testid="simulation-token-value" > - 0 + 1000

-
+
@@ -107,7 +109,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
@@ -202,7 +204,7 @@ exports[` renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -246,7 +248,7 @@ exports[` renders component for approve request 1`] = `
@@ -343,7 +345,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-spending-cap-section" >
renders component for approve request 1`] = ` class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - 0 + 0

@@ -374,7 +376,7 @@ exports[` renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -414,7 +416,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-text mm-text--body-md mm-box--color-inherit" style="white-space: pre-wrap;" > - 0 + 1000

renders component for approve request 1`] = `
@@ -595,7 +597,7 @@ exports[` renders component for approve request 1`] = ` data-testid="advanced-details-nonce-section" >
renders component for approve request 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve request 1`] = ` />
@@ -699,7 +701,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -791,7 +793,7 @@ exports[` renders component for approve request 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap index a66499aab561..17d04e237fb2 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap @@ -11,7 +11,7 @@ exports[` renders component for approve details 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve details for setApprova data-testid="advanced-details-data-section" >
renders component 1`] = ` data-testid="confirmation__simulation_section" >
renders component 1`] = `
renders component 1`] = ` 1000

-
+
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 f6524249dd21..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 @@ -65,7 +65,7 @@ export const ApproveStaticSimulation = () => { ); - const simulationElements = ( + const SpendingCapRow = ( @@ -87,12 +87,15 @@ export const ApproveStaticSimulation = () => { ); + const simulationElements = SpendingCapRow; + return ( ({ + useApproveTokenSimulation: jest.fn(() => ({ + spendingCap: '1000', + formattedSpendingCap: '1000', + value: '1000', + })), +})); + jest.mock('../../../../hooks/useAssetDetails', () => ({ useAssetDetails: jest.fn(() => ({ decimals: 18, diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index 39baf7761157..eabf8639ccfb 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -1,14 +1,22 @@ -import { TransactionMeta } from '@metamask/transaction-controller'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import { useConfirmContext } from '../../../../context/confirm'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { ApproveDetails } from './approve-details/approve-details'; import { ApproveStaticSimulation } from './approve-static-simulation/approve-static-simulation'; import { EditSpendingCapModal } from './edit-spending-cap-modal/edit-spending-cap-modal'; +import { useApproveTokenSimulation } from './hooks/use-approve-token-simulation'; import { useIsNFT } from './hooks/use-is-nft'; +import { RevokeDetails } from './revoke-details/revoke-details'; +import { RevokeStaticSimulation } from './revoke-static-simulation/revoke-static-simulation'; import { SpendingCap } from './spending-cap/spending-cap'; const ApproveInfo = () => { @@ -25,15 +33,38 @@ const ApproveInfo = () => { const [isOpenEditSpendingCapModal, setIsOpenEditSpendingCapModal] = useState(false); + const { decimals } = useAssetDetails( + transactionMeta.txParams.to, + transactionMeta.txParams.from, + transactionMeta.txParams.data, + ); + + const { spendingCap, pending } = useApproveTokenSimulation( + transactionMeta, + decimals || '0', + ); + + const showRevokeVariant = + spendingCap === '0' && + transactionMeta.type === TransactionType.tokenMethodApprove; + if (!transactionMeta?.txParams) { return null; } + if (pending) { + return ; + } + return ( <> - - - {!isNFT && ( + {showRevokeVariant ? ( + + ) : ( + + )} + {showRevokeVariant ? : } + {!isNFT && !showRevokeVariant && ( diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap index 7d64def9ff73..e56a87ed34d7 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap @@ -57,7 +57,7 @@ exports[` renders component 1`] = ` focused="true" placeholder="1000 TST" type="number" - value="" + value="1000" />

({ + ...jest.requireActual('react-dom'), + createPortal: (node: ReactNode) => node, +})); + +jest.mock('../../../../../../../store/actions', () => ({ + ...jest.requireActual('../../../../../../../store/actions'), + getGasFeeTimeEstimate: jest.fn().mockResolvedValue({ + lowerTimeBound: 0, + upperTimeBound: 60000, + }), +})); + +jest.mock('../hooks/use-approve-token-simulation', () => ({ + useApproveTokenSimulation: jest.fn(() => ({ + spendingCap: '1000', + formattedSpendingCap: '1000', + value: '1000', + })), +})); jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), @@ -57,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 8b5a237d1b36..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 @@ -1,5 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { calcTokenAmount } from '../../../../../../../../shared/lib/transactions-controller-utils'; import { hexToDecimal } from '../../../../../../../../shared/modules/conversion.utils'; @@ -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, @@ -64,18 +68,28 @@ export const EditSpendingCapModal = ({ ); const [customSpendingCapInputValue, setCustomSpendingCapInputValue] = - useState(''); + useState(formattedSpendingCap.toString()); + + useEffect(() => { + if (formattedSpendingCap) { + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + } + }, [formattedSpendingCap]); const handleCancel = useCallback(() => { setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [setIsOpenEditSpendingCapModal, setCustomSpendingCapInputValue]); + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + }, [ + setIsOpenEditSpendingCapModal, + setCustomSpendingCapInputValue, + formattedSpendingCap, + ]); const [isModalSaving, setIsModalSaving] = useState(false); const handleSubmit = useCallback(async () => { setIsModalSaving(true); - const parsedValue = parseInt(customSpendingCapInputValue, 10); + const parsedValue = parseInt(String(customSpendingCapInputValue), 10); const customTxParamsData = getCustomTxParamsData( transactionMeta?.txParams?.data, @@ -103,13 +117,17 @@ export const EditSpendingCapModal = ({ setIsModalSaving(false); setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [customSpendingCapInputValue]); + 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" @@ -144,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/hooks/use-approve-token-simulation.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts index fd99ffb1a2f7..19f26c9c9300 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-approve-token-simulation.ts @@ -1,5 +1,7 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { isHexString } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; +import { isBoolean } from 'lodash'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; @@ -23,11 +25,24 @@ export const useApproveTokenSimulation = ( const { value, pending } = decodedResponse; const decodedSpendingCap = useMemo(() => { - return value - ? new BigNumber(value.data[0].params[1].value) - .dividedBy(new BigNumber(10).pow(Number(decimals))) - .toNumber() - : 0; + if (!value) { + return 0; + } + + const paramIndex = value.data[0].params.findIndex( + (param) => + param.value !== undefined && + !isHexString(param.value) && + param.value.length === undefined && + !isBoolean(param.value), + ); + if (paramIndex === -1) { + return 0; + } + + return new BigNumber(value.data[0].params[paramIndex].value.toString()) + .dividedBy(new BigNumber(10).pow(Number(decimals))) + .toNumber(); }, [value, decimals]); const formattedSpendingCap = useMemo(() => { diff --git a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts index 874e817cc20a..a6e92167e558 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts +++ b/ui/pages/confirmations/components/confirm/info/approve/hooks/use-received-token.test.ts @@ -1,10 +1,8 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { - CONTRACT_INTERACTION_SENDER_ADDRESS, - genUnapprovedApproveConfirmation, -} from '../../../../../../../../test/data/confirmations/contract-interaction'; +import { CONTRACT_INTERACTION_SENDER_ADDRESS } from '../../../../../../../../test/data/confirmations/contract-interaction'; import { getMockConfirmStateForTransaction } from '../../../../../../../../test/data/confirmations/helper'; +import { genUnapprovedApproveConfirmation } from '../../../../../../../../test/data/confirmations/token-approve'; import { renderHookWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; import { useAccountTotalFiatBalance } from '../../../../../../../hooks/useAccountTotalFiatBalance'; import { useReceivedToken } from './use-received-token'; diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx new file mode 100644 index 000000000000..49bf5e7724e1 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../../components/app/confirm/info/row/section'; +import { OriginRow } from '../../shared/transaction-details/transaction-details'; + +export const RevokeDetails = () => { + return ( + + + + ); +}; 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 new file mode 100644 index 000000000000..38ff93ba9b36 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx @@ -0,0 +1,62 @@ +import { NameType } from '@metamask/name-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoRow } from '../../../../../../../components/app/confirm/info/row'; +import Name from '../../../../../../../components/app/name'; +import { Box } from '../../../../../../../components/component-library'; +import { Display } from '../../../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; +import { useConfirmContext } from '../../../../../context/confirm'; +import StaticSimulation from '../../shared/static-simulation/static-simulation'; + +export const RevokeStaticSimulation = () => { + const t = useI18nContext(); + + const { currentConfirmation: transactionMeta } = useConfirmContext() as { + currentConfirmation: TransactionMeta; + }; + + const TokenContractRow = ( + + + + + + + + ); + + const SpenderRow = ( + + + + + + + + ); + + const simulationElements = ( + <> + {TokenContractRow} + {SpenderRow} + + ); + + return ( + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap index 6e415415a452..3579fe496673 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/__snapshots__/spending-cap.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component 1`] = ` data-testid="confirmation__approve-spending-cap-section" >

renders component 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.test.tsx b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.test.tsx index c6001475c330..194747c77b4d 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.test.tsx @@ -13,6 +13,14 @@ jest.mock('../hooks/use-approve-token-simulation', () => ({ })), })); +jest.mock('../hooks/use-approve-token-simulation', () => ({ + useApproveTokenSimulation: jest.fn(() => ({ + spendingCap: '1000', + formattedSpendingCap: '1000', + value: '1000', + })), +})); + describe('', () => { const middleware = [thunk]; diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap index 3cd8e825da01..5f0370343f00 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap @@ -89,7 +89,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -133,7 +133,7 @@ exports[` renders component for contract interaction requ
@@ -230,7 +230,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -294,12 +294,12 @@ exports[` renders component for contract interaction requ
renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -508,7 +508,7 @@ exports[` renders component for contract interaction requ
@@ -605,7 +605,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -669,12 +669,12 @@ exports[` renders component for contract interaction requ
renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -880,7 +880,7 @@ exports[` renders component for contract interaction requ
@@ -977,7 +977,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -1041,12 +1041,12 @@ exports[` renders component for contract interaction requ
{ + it('returns iconUrl from selected token if it exists', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + iconUrl: 'iconUrl', + image: 'image', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: 'iconUrl' }); + }); + + it('returns selected token image if no iconUrl is included', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + image: 'image', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: 'image' }); + }); + + it('returns token list icon url if no image is included in the token', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + { + ...mockState, + metamask: { + ...mockState.metamask, + tokenList: { + '0x076146c765189d51be3160a2140cf80bfc73ad68': { + iconUrl: 'tokenListIconUrl', + }, + }, + }, + }, + ); + + expect(result.current).toEqual({ tokenImage: 'tokenListIconUrl' }); + }); + + it('returns undefined if no image is found', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + }; + + const { result } = renderHookWithProvider( + () => useTokenImage(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + expect(result.current).toEqual({ tokenImage: undefined }); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts new file mode 100644 index 000000000000..5817d08028ab --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-image.ts @@ -0,0 +1,20 @@ +import { TokenListMap } from '@metamask/assets-controllers'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { useSelector } from 'react-redux'; +import { getTokenList } from '../../../../../../selectors'; +import { SelectedToken } from '../shared/selected-token'; + +export const useTokenImage = ( + transactionMeta: TransactionMeta, + selectedToken: SelectedToken, +) => { + const tokenList = useSelector(getTokenList) as TokenListMap; + + // TODO: Add support for NFT images in one of the following tasks + const tokenImage = + selectedToken?.iconUrl || + selectedToken?.image || + tokenList[transactionMeta?.txParams?.to as string]?.iconUrl; + + return { tokenImage }; +}; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts new file mode 100644 index 000000000000..7ac4aa5b5c92 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.test.ts @@ -0,0 +1,120 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; +import mockState from '../../../../../../../test/data/mock-state.json'; +import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; +// import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; +import { useTokenValues } from './use-token-values'; + +jest.mock( + '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate', + () => jest.fn(), +); + +jest.mock('../../../../../../hooks/useTokenTracker', () => ({ + ...jest.requireActual('../../../../../../hooks/useTokenTracker'), + useTokenTracker: jest.fn(), +})); + +describe('useTokenValues', () => { + const useTokenExchangeRateMock = jest.mocked(useTokenExchangeRate); + const useTokenTrackerMock = jest.mocked(useTokenTracker); + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + symbol: 'symbol', + iconUrl: 'iconUrl', + image: 'image', + }; + + it('returns native and fiat balances', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [ + { + address: '0x076146c765189d51be3160a2140cf80bfc73ad68', + balance: '1000000000000000000', + decimals: 18, + }, + ], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( + new Numeric(1, 10), + ); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: '$1.00', + tokenBalance: '1', + }); + }); + + it('returns undefined native and fiat balances if no token with balances is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue( + new Numeric(1, 10), + ); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: undefined, + tokenBalance: undefined, + }); + }); + + it('returns undefined fiat balance if no token rate is returned', async () => { + (useTokenTrackerMock as jest.Mock).mockResolvedValue({ + tokensWithBalances: [ + { + address: '0x076146c765189d51be3160a2140cf80bfc73ad68', + balance: '1000000000000000000', + decimals: 18, + }, + ], + }); + + (useTokenExchangeRateMock as jest.Mock).mockResolvedValue(null); + + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const { result, waitForNextUpdate } = renderHookWithProvider( + () => useTokenValues(transactionMeta, TEST_SELECTED_TOKEN), + mockState, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + fiatDisplayValue: null, + tokenBalance: '1', + }); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts new file mode 100644 index 000000000000..9515a45515bf --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -0,0 +1,77 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { useMemo, useState } from 'react'; +import { calcTokenAmount } from '../../../../../../../shared/lib/transactions-controller-utils'; +import { toChecksumHexAddress } from '../../../../../../../shared/modules/hexstring-utils'; +import { Numeric } from '../../../../../../../shared/modules/Numeric'; +import useTokenExchangeRate from '../../../../../../components/app/currency-input/hooks/useTokenExchangeRate'; +import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; +import { useTokenTracker } from '../../../../../../hooks/useTokenTracker'; +import { SelectedToken } from '../shared/selected-token'; + +export const useTokenValues = ( + transactionMeta: TransactionMeta, + selectedToken: SelectedToken, +) => { + const [tokensWithBalances, setTokensWithBalances] = useState< + { balance: string; address: string; decimals: number; string: string }[] + >([]); + + const fetchTokenBalances = async () => { + const result: { + tokensWithBalances: { + balance: string; + address: string; + decimals: number; + string: string; + }[]; + } = await useTokenTracker({ + tokens: [selectedToken], + address: undefined, + }); + + setTokensWithBalances(result.tokensWithBalances); + }; + + fetchTokenBalances(); + + const [exchangeRate, setExchangeRate] = useState(); + const fetchExchangeRate = async () => { + const result = await useTokenExchangeRate(transactionMeta?.txParams?.to); + + setExchangeRate(result); + }; + + fetchExchangeRate(); + + const tokenBalance = useMemo(() => { + const tokenWithBalance = tokensWithBalances.find( + (token: { + balance: string; + address: string; + decimals: number; + string: string; + }) => + toChecksumHexAddress(token.address) === + toChecksumHexAddress(transactionMeta?.txParams?.to as string), + ); + + if (!tokenWithBalance) { + return undefined; + } + + return calcTokenAmount(tokenWithBalance.balance, tokenWithBalance.decimals); + }, [tokensWithBalances]); + + const fiatValue = + exchangeRate && tokenBalance && exchangeRate.times(tokenBalance).toNumber(); + + const fiatFormatter = useFiatFormatter(); + + const fiatDisplayValue = + fiatValue && fiatFormatter(fiatValue, { shorten: true }); + + return { + fiatDisplayValue, + tokenBalance: tokenBalance && String(tokenBalance.toNumber()), + }; +}; diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts index b96f149f0bdd..32a711abf754 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useDecodedTransactionData.test.ts @@ -60,15 +60,18 @@ describe('useDecodedTransactionData', () => { ); it('returns undefined if no transaction to', async () => { - const result = await runHook({ - currentConfirmation: { + const result = await runHook( + getMockConfirmStateForTransaction({ + id: '123', chainId: CHAIN_ID_MOCK, + type: TransactionType.contractInteraction, + status: TransactionStatus.unapproved, txParams: { data: TRANSACTION_DATA_UNISWAP, to: undefined, } as TransactionParams, - }, - }); + }), + ); expect(result).toStrictEqual({ pending: false, value: undefined }); }); diff --git a/ui/pages/confirmations/components/confirm/info/info.test.tsx b/ui/pages/confirmations/components/confirm/info/info.test.tsx index 4931c2fbaa01..75d98d91a1bd 100644 --- a/ui/pages/confirmations/components/confirm/info/info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.test.tsx @@ -1,6 +1,6 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; - import { getMockApproveConfirmState, getMockContractInteractionConfirmState, @@ -50,17 +50,27 @@ describe('Info', () => { expect(container).toMatchSnapshot(); }); - it('renders info section for approve request', () => { + it('renders info section for approve request', async () => { const state = getMockApproveConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); - it('renders info section for setApprovalForAll request', () => { + it('renders info section for setApprovalForAll request', async () => { const state = getMockSetApprovalForAllConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index 5a9c4757158e..3e87f4f7908c 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -6,6 +6,7 @@ import ApproveInfo from './approve/approve'; import BaseTransactionInfo from './base-transaction-info/base-transaction-info'; import PersonalSignInfo from './personal-sign/personal-sign'; import SetApprovalForAllInfo from './set-approval-for-all-info/set-approval-for-all-info'; +import TokenTransferInfo from './token-transfer/token-transfer'; import TypedSignV1Info from './typed-sign-v1/typed-sign-v1'; import TypedSignInfo from './typed-sign/typed-sign'; @@ -29,6 +30,7 @@ const Info = () => { [TransactionType.tokenMethodIncreaseAllowance]: () => ApproveInfo, [TransactionType.tokenMethodSetApprovalForAll]: () => SetApprovalForAllInfo, + [TransactionType.tokenMethodTransfer]: () => TokenTransferInfo, }), [currentConfirmation], ); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap index 79290bb3b49b..2f46c283b830 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap @@ -6,7 +6,7 @@ exports[`PersonalSignInfo handle reverse string properly 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
- metamask.github.io + https://metamask.github.io

- metamask.github.io + https://metamask.github.io

{ const { address, chainId, - domain, issuedAt, nonce, requestId, statement, resources, + uri, version, } = siweMessage; const hexChainId = toHex(chainId); @@ -44,7 +44,7 @@ const SIWESignInfo: React.FC = () => { - + diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap index 9f009645f67b..81782a20ec5b 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap @@ -9,7 +9,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` All

-
+
@@ -109,73 +111,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -224,7 +160,7 @@ exports[` renders component for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -288,12 +224,12 @@ exports[` renders component for approve request 1`] = `
renders component for setAp data-testid="confirmation__simulation_section" >
renders component for setAp
renders component for setAp
-
+
@@ -92,7 +94,7 @@ exports[` renders component for setAp
renders component for setAp
-
+
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-info.test.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx index 3460b1d8e76e..77a840dcece5 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx @@ -1,3 +1,4 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -39,6 +40,10 @@ describe('', () => { mockStore, ); + await waitFor(() => { + expect(screen.getByText('Data')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx index 6a2c98f224e2..92df913783a1 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx @@ -6,6 +6,7 @@ import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/pre import { ApproveDetails } from '../approve/approve-details/approve-details'; import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { getIsRevokeSetApprovalForAll } from '../utils'; import { RevokeSetApprovalForAllStaticSimulation } from './revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation'; @@ -21,7 +22,7 @@ const SetApprovalForAllInfo = () => { const decodedResponse = useDecodedTransactionData(); - const { value } = decodedResponse; + const { value, pending } = decodedResponse; const isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll(value); @@ -31,6 +32,10 @@ const SetApprovalForAllInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {isRevokeSetApprovalForAll ? ( diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap index 9ab4107ff173..b2f289875f6b 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component for approve req data-testid="confirmation__simulation_section" >
renders component for approve req
renders component for approve req All

-
+
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/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap index a521ff23795a..f66db615defe 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap @@ -7,7 +7,7 @@ exports[` does not render component for advanced transaction data-testid="advanced-details-nonce-section" >
does not render component for advanced transaction data-testid="advanced-details-data-section" >
renders component for advanced transaction details data-testid="advanced-details-nonce-section" >
renders component for advanced transaction details data-testid="advanced-details-data-section" >
renders component 1`] = ` +
+
+ + + + + + + + + +
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx new file mode 100644 index 000000000000..155f6a27850b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getMockSetApprovalForAllConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { ConfirmLoader } from './confirm-loader'; + +describe('', () => { + const middleware = [thunk]; + + it('renders component', async () => { + const state = getMockSetApprovalForAllConfirmState(); + + const mockStore = configureMockStore(middleware)(state); + + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx new file mode 100644 index 000000000000..2aa57f489dfe --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box } from '../../../../../../../components/component-library'; +import Preloader from '../../../../../../../components/ui/icon/preloader'; +import { + AlignItems, + Display, + JustifyContent, +} from '../../../../../../../helpers/constants/design-system'; + +export const ConfirmLoader = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap index 87a78c4928bc..3ad4343e8ee5 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx index 049270fd5af2..7d4223f01204 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/edit-gas-fees-row.tsx @@ -1,5 +1,4 @@ import React, { Dispatch, SetStateAction } from 'react'; -import { useSelector } from 'react-redux'; import { TransactionMeta } from '@metamask/transaction-controller'; import { Box, Text } from '../../../../../../../components/component-library'; import { @@ -11,7 +10,6 @@ import { TextColor, } from '../../../../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; -import { getPreferences } from '../../../../../../../selectors'; import { useConfirmContext } from '../../../../../context/confirm'; import { EditGasIconButton } from '../edit-gas-icon/edit-gas-icon-button'; import { ConfirmInfoAlertRow } from '../../../../../../../components/app/confirm/info/row/alert-row/alert-row'; @@ -30,9 +28,6 @@ export const EditGasFeesRow = ({ }) => { const t = useI18nContext(); - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -56,14 +51,14 @@ export const EditGasFeesRow = ({ color={TextColor.textDefault} data-testid="first-gas-field" > - {isNativeCurrencyUsed ? nativeFee : fiatFee} + {nativeFee} - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {fiatFee} renders component for gas fees section 1`] = `
@@ -67,12 +67,12 @@ exports[` renders component for gas fees section 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap index 6643621734f6..5de2d2361b38 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
{ - const { useNativeCurrencyAsPrimaryCurrency: isNativeCurrencyUsed } = - useSelector(getPreferences); - return ( - {isNativeCurrencyUsed ? nativeFee : fiatFee} - - - {isNativeCurrencyUsed ? fiatFee : nativeFee} + {nativeFee} + {fiatFee} ); diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap index 65932520e283..5901dd2e8f2d 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-section/__snapshots__/gas-fees-section.test.tsx.snap @@ -9,7 +9,7 @@ exports[` renders component for gas fees section 1`] = ` data-testid="gas-fee-section" >
@@ -73,12 +73,12 @@ exports[` renders component for gas fees section 1`] = `
renders component 1`] = ` +
+
+
+ ? +
+

+ Unknown +

+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx new file mode 100644 index 000000000000..f4bfb484c107 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.stories.tsx @@ -0,0 +1,29 @@ +import { Meta } from '@storybook/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import configureStore from '../../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../../context/confirm'; +import SendHeading from './send-heading'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/SendHeading', + component: SendHeading, + decorators: [ + (story: () => Meta) => ( + {story()} + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ( + + + +); + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx new file mode 100644 index 000000000000..613930f9901d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getMockTokenTransferConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import SendHeading from './send-heading'; + +describe('', () => { + const middleware = [thunk]; + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore(middleware)(state); + + it('renders component', () => { + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx new file mode 100644 index 000000000000..d571c61ee93e --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -0,0 +1,84 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { + AvatarToken, + AvatarTokenSize, + Box, + Text, +} from '../../../../../../../components/component-library'; +import { + AlignItems, + BackgroundColor, + Display, + FlexDirection, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; +import { getWatchedToken } from '../../../../../../../selectors'; +import { MultichainState } from '../../../../../../../selectors/multichain'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { useTokenImage } from '../../hooks/use-token-image'; +import { useTokenValues } from '../../hooks/use-token-values'; + +const SendHeading = () => { + const t = useI18nContext(); + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + const selectedToken = useSelector((state: MultichainState) => + getWatchedToken(transactionMeta)(state), + ); + const { tokenImage } = useTokenImage(transactionMeta, selectedToken); + const { tokenBalance, fiatDisplayValue } = useTokenValues( + transactionMeta, + selectedToken, + ); + + const TokenImage = ( + + ); + + const TokenValue = ( + <> + {`${tokenBalance || ''} ${selectedToken?.symbol || t('unknown')}`} + {fiatDisplayValue && ( + + {fiatDisplayValue} + + )} + + ); + + return ( + + {TokenImage} + {TokenValue} + + ); +}; + +export default SendHeading; diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap index b830449ca55c..c53805c877e1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap @@ -7,7 +7,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = data-testid="advanced-details-data-section" >
@@ -140,7 +140,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -191,7 +191,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -299,7 +299,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -344,7 +344,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -389,7 +389,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -511,7 +511,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -562,7 +562,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -670,7 +670,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -726,7 +726,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -777,7 +777,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -885,7 +885,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -941,7 +941,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -999,7 +999,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` data-testid="advanced-details-data-section" >
@@ -1056,7 +1056,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1101,7 +1101,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1157,7 +1157,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1213,7 +1213,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` data-testid="advanced-details-data-section" >
@@ -1286,7 +1286,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1342,7 +1342,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1380,7 +1380,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1418,7 +1418,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1456,7 +1456,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1512,7 +1512,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1557,7 +1557,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1602,7 +1602,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1648,7 +1648,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1686,7 +1686,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1742,7 +1742,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1787,7 +1787,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1832,7 +1832,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1878,7 +1878,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1916,7 +1916,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1972,7 +1972,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2017,7 +2017,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2062,7 +2062,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2109,7 +2109,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2165,7 +2165,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2211,7 +2211,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2269,7 +2269,7 @@ exports[`TransactionData renders raw hexadecimal if no decoded data 1`] = ` data-testid="advanced-details-data-section" >
renders component for transaction details 1`] = data-testid="transaction-details-section" >
@@ -60,7 +60,7 @@ exports[` renders component for transaction details 1`] =
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx index e8735c3ec2c4..1263acf08397 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.test.tsx @@ -7,9 +7,9 @@ import { getMockConfirmStateForTransaction, getMockContractInteractionConfirmState, } from '../../../../../../../../test/data/confirmations/helper'; -import { genUnapprovedContractInteractionConfirmation } from '../../../../../../../../test/data/confirmations/contract-interaction'; -import { CHAIN_IDS } from '../../../../../../../../shared/constants/network'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { CHAIN_IDS } from '../../../../../../../../shared/constants/network'; +import { genUnapprovedContractInteractionConfirmation } from '../../../../../../../../test/data/confirmations/contract-interaction'; import { TransactionDetails } from './transaction-details'; jest.mock( diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap new file mode 100644 index 000000000000..63b44d50173d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/token-transfer.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenTransferInfo renders correctly 1`] = ` +
+
+
+ ? +
+

+ Unknown +

+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx new file mode 100644 index 000000000000..384a8f161e9b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import configureStore from '../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../context/confirm'; +import TokenTransferInfo from './token-transfer'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/TokenTransferInfo', + component: TokenTransferInfo, + decorators: [ + (story: () => any) => ( + + {story()} + + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx new file mode 100644 index 000000000000..186505ee7740 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import TokenTransferInfo from './token-transfer'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +describe('TokenTransferInfo', () => { + it('renders correctly', () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx new file mode 100644 index 000000000000..6fe5ecf166b2 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import SendHeading from '../shared/send-heading/send-heading'; + +const TokenTransferInfo = () => { + return ; +}; + +export default TokenTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap index 57aa7e62fcb2..59a6065e6b9d 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap @@ -6,7 +6,7 @@ exports[`TypedSignInfo correctly renders typed sign data request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -127,7 +129,7 @@ exports[`TypedSignInfo correctly renders permit sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -762,7 +766,7 @@ exports[`TypedSignInfo correctly renders permit sign type with no deadline 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx index 66125d9def17..1be34109a637 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.test.tsx @@ -5,7 +5,6 @@ import { act } from 'react-dom/test-utils'; import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; import { permitSignatureMsg } from '../../../../../../../../test/data/confirmations/typed_sign'; - import PermitSimulation from './permit-simulation'; jest.mock('../../../../../../../store/actions', () => { diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 022ff8b6dbc2..26def806c6fa 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -32,7 +32,9 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = `
-
+
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/confirmations/components/confirm/info/typed-sign/typed-sign.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx index 86ea63470c37..7a608fc68b8a 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx @@ -12,12 +12,12 @@ import { ConfirmInfoRowUrl, } from '../../../../../../components/app/confirm/info/row'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { getTokenStandardAndDetails } from '../../../../../../store/actions'; import { SignatureRequestType } from '../../../../types/confirm'; import { isOrderSignatureRequest, isPermitSignatureRequest, } from '../../../../utils'; +import { fetchErc20Decimals } from '../../../../utils/token'; import { useConfirmContext } from '../../../../context/confirm'; import { selectUseTransactionSimulations } from '../../../../selectors/preferences'; import { ConfirmInfoRowTypedSignData } from '../../row/typed-sign-data/typedSignData'; @@ -49,10 +49,8 @@ const TypedSignInfo: React.FC = () => { if (!isPermit && !isOrder) { return; } - const tokenDetails = await getTokenStandardAndDetails(verifyingContract); - const tokenDecimals = tokenDetails?.decimals; - - setDecimals(parseInt(tokenDecimals ?? '0', 10)); + const tokenDecimals = await fetchErc20Decimals(verifyingContract); + setDecimals(tokenDecimals); })(); }, [verifyingContract]); diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx index 2fd394f18ae2..6546b882b784 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -32,9 +32,9 @@ import { import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { pendingConfirmationsSortedSelector } from '../../../../../selectors'; import { rejectPendingApproval } from '../../../../../store/actions'; +import { useConfirmContext } from '../../../context/confirm'; import { useQueuedConfirmationsEvent } from '../../../hooks/useQueuedConfirmationEvents'; import { isSignatureApprovalRequest } from '../../../utils'; -import { useConfirmContext } from '../../../context/confirm'; const Nav = () => { const history = useHistory(); diff --git a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap index cc8f3451676a..68d8aab887be 100644 --- a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap @@ -6,7 +6,7 @@ exports[`DataTree correctly renders reverse strings 1`] = ` class="mm-box mm-box--width-full" >
= { */ const NONE_DATE_VALUE = -1; +/** + * If a token contract is found within the dataTree, fetch the token decimal of this contract + * to be utilized for displaying token amounts of the dataTree. + * + * @param dataTreeData + */ const getTokenDecimalsOfDataTree = async ( dataTreeData: Record | TreeData[], ): Promise => { @@ -91,7 +97,7 @@ const getTokenDecimalsOfDataTree = async ( export const DataTree = ({ data, primaryType, - tokenDecimals = 0, + tokenDecimals: tokenDecimalsProp, }: { data: Record | TreeData[]; primaryType?: PrimaryType; @@ -102,8 +108,8 @@ export const DataTree = ({ [data], ); - const tokenContractDecimals = - typeof decimalsResponse === 'number' ? decimalsResponse : undefined; + const tokenDecimals = + typeof decimalsResponse === 'number' ? decimalsResponse : tokenDecimalsProp; return ( @@ -122,7 +128,7 @@ export const DataTree = ({ primaryType={primaryType} value={value} type={type} - tokenDecimals={tokenContractDecimals ?? tokenDecimals} + tokenDecimals={tokenDecimals} /> } @@ -153,7 +159,7 @@ const DataField = memo( primaryType?: PrimaryType; type: string; value: ValueType; - tokenDecimals: number; + tokenDecimals?: number; }) => { const t = useI18nContext(); diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/__snapshots__/typedSignDataV1.test.tsx.snap b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/__snapshots__/typedSignDataV1.test.tsx.snap index 133a578e3131..a35e406b2e7b 100644 --- a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/__snapshots__/typedSignDataV1.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/__snapshots__/typedSignDataV1.test.tsx.snap @@ -13,7 +13,7 @@ exports[`ConfirmInfoRowTypedSignData should match snapshot 1`] = ` class="mm-box mm-box--width-full" >
{ + it('returns the correct spending cap', () => { + const transactionMeta = genUnapprovedApproveConfirmation({ + address: CONTRACT_INTERACTION_SENDER_ADDRESS, + }) as TransactionMeta; + + const { result } = renderHookWithConfirmContextProvider( + () => useCurrentSpendingCap(transactionMeta), + mockState, + ); + + expect(result.current.customSpendingCap).toMatchInlineSnapshot(`"#0"`); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts new file mode 100644 index 000000000000..5f588a971561 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts @@ -0,0 +1,49 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { Confirmation } from '../../../../types/confirm'; +import { useAssetDetails } from '../../../../hooks/useAssetDetails'; +import { useApproveTokenSimulation } from '../../info/approve/hooks/use-approve-token-simulation'; + +const isTransactionMeta = ( + confirmation: Confirmation | undefined, +): confirmation is TransactionMeta => { + return ( + confirmation !== undefined && + (confirmation as TransactionMeta).txParams !== undefined + ); +}; + +export function useCurrentSpendingCap(currentConfirmation: Confirmation) { + const isTxWithSpendingCap = + isTransactionMeta(currentConfirmation) && + [ + TransactionType.tokenMethodApprove, + TransactionType.tokenMethodIncreaseAllowance, + ].includes(currentConfirmation.type as TransactionType); + + const txParamsTo = isTxWithSpendingCap + ? currentConfirmation.txParams.to + : null; + const txParamsFrom = isTxWithSpendingCap + ? currentConfirmation.txParams.from + : null; + const txParamsData = isTxWithSpendingCap + ? currentConfirmation.txParams.data + : null; + + const { decimals } = useAssetDetails(txParamsTo, txParamsFrom, txParamsData); + + const { spendingCap, pending } = useApproveTokenSimulation( + currentConfirmation as TransactionMeta, + decimals || '0', + ); + + let customSpendingCap = ''; + if (isTxWithSpendingCap) { + customSpendingCap = spendingCap; + } + + return { customSpendingCap, pending }; +} diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index 938ed46d5537..3c03343c2afb 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -1,6 +1,6 @@ +import { waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; - import { getMockApproveConfirmState, getMockContractInteractionConfirmState, @@ -22,6 +22,22 @@ import { Severity } from '../../../../../helpers/constants/design-system'; import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import ConfirmTitle from './title'; +jest.mock('../info/approve/hooks/use-approve-token-simulation', () => ({ + useApproveTokenSimulation: jest.fn(() => ({ + spendingCap: '1000', + formattedSpendingCap: '1000', + value: '1000', + })), +})); + +jest.mock('../../../hooks/useAssetDetails', () => ({ + useAssetDetails: jest.fn(() => ({ + decimals: 18, + userBalance: '1000000', + tokenSymbol: 'TST', + })), +})); + jest.mock('../info/approve/hooks/use-is-nft', () => ({ useIsNFT: jest.fn(() => ({ isNFT: true })), })); @@ -119,7 +135,7 @@ describe('ConfirmTitle', () => { ).toBeInTheDocument(); }); - it('should render the title and description for a setApprovalForAll transaction', () => { + it('should render the title and description for a setApprovalForAll transaction', async () => { const mockStore = configureMockStore([])( getMockSetApprovalForAllConfirmState(), ); @@ -128,12 +144,15 @@ describe('ConfirmTitle', () => { mockStore, ); - expect( - getByText(tEn('setApprovalForAllRedesignedTitle') as string), - ).toBeInTheDocument(); - expect( - getByText(tEn('confirmTitleDescApproveTransaction') as string), - ).toBeInTheDocument(); + await waitFor(() => { + expect( + getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + + expect( + getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); }); describe('Alert banner', () => { @@ -143,16 +162,23 @@ describe('ConfirmTitle', () => { reason: 'mock reason', key: 'mock key', }; + + const alertMock2 = { + ...alertMock, + key: 'mock key 2', + reason: 'mock reason 2', + }; const mockAlertState = (state: Partial = {}) => getMockPersonalSignConfirmStateForRequest(unapprovedPersonalSignMsg, { metamask: {}, confirmAlerts: { alerts: { - [unapprovedPersonalSignMsg.id]: [alertMock, alertMock, alertMock], + [unapprovedPersonalSignMsg.id]: [alertMock, alertMock2], }, confirmed: { [unapprovedPersonalSignMsg.id]: { [alertMock.key]: false, + [alertMock2.key]: false, }, }, ...state, @@ -175,7 +201,7 @@ describe('ConfirmTitle', () => { expect(queryByText(alertMock.message)).toBeInTheDocument(); }); - it('renders alert banner when there are multiple alerts', () => { + it('renders multiple alert banner when there are multiple alerts', () => { const mockStore = configureMockStore([])(mockAlertState()); const { getByText } = renderWithConfirmContextProvider( @@ -183,7 +209,8 @@ describe('ConfirmTitle', () => { mockStore, ); - expect(getByText('Multiple alerts!')).toBeInTheDocument(); + expect(getByText(alertMock.reason)).toBeInTheDocument(); + expect(getByText(alertMock2.reason)).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 4fa3119c4802..2645feed8a41 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -3,9 +3,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; - import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; -import { getHighestSeverity } from '../../../../../components/app/alert-system/utils'; import { Box, Text } from '../../../../../components/component-library'; import { TextAlign, @@ -23,39 +21,30 @@ import { import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData'; import { getIsRevokeSetApprovalForAll } from '../info/utils'; +import { useCurrentSpendingCap } from './hooks/useCurrentSpendingCap'; function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { - const t = useI18nContext(); const { generalAlerts } = useAlerts(ownerId); if (generalAlerts.length === 0) { return null; } - const hasMultipleAlerts = generalAlerts.length > 1; - const singleAlert = generalAlerts[0]; - const highestSeverity = hasMultipleAlerts - ? getHighestSeverity(generalAlerts) - : singleAlert.severity; return ( - - + + {generalAlerts.map((alert) => ( + + + + ))} ); } @@ -66,8 +55,14 @@ const getTitle = ( t: IntlFunction, confirmation?: Confirmation, isNFT?: boolean, + customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return t('confirmTitleTransaction'); @@ -86,6 +81,9 @@ const getTitle = ( if (isNFT) { return t('confirmTitleApproveTransaction'); } + if (customSpendingCap === '0') { + return t('confirmTitleRevokeApproveTransaction'); + } return t('confirmTitlePermitTokens'); case TransactionType.tokenMethodIncreaseAllowance: return t('confirmTitlePermitTokens'); @@ -103,8 +101,14 @@ const getDescription = ( t: IntlFunction, confirmation?: Confirmation, isNFT?: boolean, + customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return ''; @@ -123,6 +127,9 @@ const getDescription = ( if (isNFT) { return t('confirmTitleDescApproveTransaction'); } + if (customSpendingCap === '0') { + return ''; + } return t('confirmTitleDescERC20ApproveTransaction'); case TransactionType.tokenMethodIncreaseAllowance: return t('confirmTitleDescPermitSignature'); @@ -143,7 +150,11 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); + const { customSpendingCap, pending: spendingCapPending } = + useCurrentSpendingCap(currentConfirmation); + let isRevokeSetApprovalForAll = false; + let revokePending = false; if ( currentConfirmation?.type === TransactionType.tokenMethodSetApprovalForAll ) { @@ -152,6 +163,7 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll( decodedResponse.value, ); + revokePending = decodedResponse.pending; } const title = useMemo( @@ -160,9 +172,18 @@ const ConfirmTitle: React.FC = memo(() => { t as IntlFunction, currentConfirmation, isNFT, + customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - [currentConfirmation, isNFT, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); const description = useMemo( @@ -171,9 +192,18 @@ const ConfirmTitle: React.FC = memo(() => { t as IntlFunction, currentConfirmation, isNFT, + customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - [currentConfirmation, isNFT, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); if (!currentConfirmation) { @@ -183,21 +213,25 @@ const ConfirmTitle: React.FC = memo(() => { return ( <> - - {title} - - - {description} - + {title !== '' && ( + + {title} + + )} + {description !== '' && ( + + {description} + + )} ); }); diff --git a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js index 19bcc25e445f..84ea244ec8cd 100644 --- a/ui/pages/confirmations/components/fee-details-component/fee-details-component.js +++ b/ui/pages/confirmations/components/fee-details-component/fee-details-component.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { AlignItems, Display, @@ -19,7 +19,7 @@ import { Text, } from '../../../../components/component-library'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; -import { getPreferences, getShouldShowFiat } from '../../../../selectors'; +import { getShouldShowFiat } from '../../../../selectors'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import LoadingHeartBeat from '../../../../components/ui/loading-heartbeat'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component'; @@ -36,8 +36,6 @@ export default function FeeDetailsComponent({ const [expandFeeDetails, setExpandFeeDetails] = useState(false); const shouldShowFiat = useSelector(getShouldShowFiat); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const t = useI18nContext(); const { minimumCostInHexWei: hexMinimumTransactionFee } = useGasFeeContext(); @@ -64,13 +62,13 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySmBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> )}
); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const renderTotalDetailValue = useCallback( @@ -91,13 +89,12 @@ export default function FeeDetailsComponent({ color: TextColor.textAlternative, variant: TextVariant.bodySm, }} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> )} ); }, - [txData, useNativeCurrencyAsPrimaryCurrency], + [txData], ); const hasLayer1GasFee = layer1GasFee !== null; diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js index 90ccbb09ce23..c861a9dce9d9 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.js @@ -17,7 +17,6 @@ import { import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; import { PriorityLevels } from '../../../../../shared/constants/gas'; import { - getPreferences, getShouldShowFiat, getTxData, transactionFeeSelector, @@ -68,7 +67,6 @@ const GasDetailsItem = ({ supportsEIP1559, } = useGasFeeContext(); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const getTransactionFeeTotal = useMemo(() => { if (layer1GasFee) { return sumHexes(hexMinimumTransactionFee, layer1GasFee); @@ -148,7 +146,7 @@ const GasDetailsItem = ({ }} type={SECONDARY} value={getTransactionFeeTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel // Label not required here as it will always display fiat value. /> )}
@@ -168,7 +166,7 @@ const GasDetailsItem = ({ }} type={PRIMARY} value={getTransactionFeeTotal || draftHexMinimumTransactionFee} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} + // Label required here as it will always display crypto value />
} @@ -216,7 +214,6 @@ const GasDetailsItem = ({ value={ getMaxTransactionFeeTotal || draftHexMaximumTransactionFee } - hideLabel={!useNativeCurrencyAsPrimaryCurrency} />
diff --git a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js index ca85dd9abae9..3e45b4c87722 100644 --- a/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js +++ b/ui/pages/confirmations/components/gas-details-item/gas-details-item.test.js @@ -35,9 +35,7 @@ const render = async ({ contextProps } = {}) => { balance: '0x1F4', }, }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, gasFeeEstimates: mockEstimates[GasEstimateTypes.feeMarket].gasFeeEstimates, gasFeeEstimatesByChainId: { diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js index 30f839b3f78c..9c91ca476ebb 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js @@ -9,10 +9,8 @@ import { } from '../../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, - conversionRateSelector, getCurrentChainId, getCurrentCurrency, - getPreferences, } from '../../../../selectors'; import { formatCurrency } from '../../../../helpers/utils/confirm-tx.util'; import { @@ -38,11 +36,8 @@ const SignatureRequestHeader = ({ txData }) => { const providerConfig = useSelector(getProviderConfig); const networkName = getNetworkNameFromProviderType(providerConfig.type); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const conversionRateFromSelector = useSelector(conversionRateSelector); - const conversionRate = useNativeCurrencyAsPrimaryCurrency - ? null - : conversionRateFromSelector; + + const conversionRate = null; // setting conversion rate to null by default to display balance in native const currentNetwork = networkName === '' diff --git a/ui/pages/confirmations/components/signature-request/signature-request.test.js b/ui/pages/confirmations/components/signature-request/signature-request.test.js index 0d50f906e5ca..9851cdbef454 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.test.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.test.js @@ -45,9 +45,7 @@ const mockStore = { rpcUrl: 'http://localhost:8545', ticker: 'ETH', }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, accounts: { '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx index 15bfab8a2428..7c4fdc6e0d22 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.stories.tsx @@ -29,7 +29,7 @@ const storeMock = configureStore({ ...mockState.metamask, preferences: { ...mockState.metamask.preferences, - useNativeCurrencyAsPrimaryCurrency: false, + showNativeTokenAsMainBalance: false, }, ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), useTokenDetection: true, diff --git a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts index 178561d0f5d6..b64a83394898 100644 --- a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts +++ b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts @@ -67,13 +67,14 @@ export function useSimulationMetrics({ setLoadingComplete(); } - const displayNameRequests: UseDisplayNameRequest[] = balanceChanges.map( - ({ asset }) => ({ - value: asset.address ?? '', + const displayNameRequests: UseDisplayNameRequest[] = balanceChanges + // Filter out changes with no address (e.g. ETH) + .filter(({ asset }) => Boolean(asset.address)) + .map(({ asset }) => ({ + value: asset.address as string, type: NameType.ETHEREUM_ADDRESS, preferContractSymbol: true, - }), - ); + })); const displayNames = useDisplayNames(displayNameRequests); @@ -145,7 +146,9 @@ export function useSimulationMetrics({ function useIncompleteAssetEvent( balanceChanges: BalanceChange[], - displayNamesByAddress: { [address: string]: UseDisplayNameResponse }, + displayNamesByAddress: { + [address: string]: UseDisplayNameResponse | undefined; + }, ) { const trackEvent = useContext(MetaMetricsContext); const [processedAssets, setProcessedAssets] = useState([]); @@ -170,7 +173,7 @@ function useIncompleteAssetEvent( properties: { asset_address: change.asset.address, asset_petname: getPetnameType(change, displayName), - asset_symbol: displayName.contractDisplayName, + asset_symbol: displayName?.contractDisplayName, asset_type: getAssetType(change.asset.standard), fiat_conversion_available: change.fiatAmount ? FiatType.Available @@ -244,7 +247,7 @@ function getAssetType(standard: TokenStandard) { function getPetnameType( balanceChange: BalanceChange, - displayName: UseDisplayNameResponse, + displayName: UseDisplayNameResponse = { name: '', hasPetname: false }, ) { if (balanceChange.asset.standard === TokenStandard.none) { return PetnameType.Default; diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index cbe80f86fe8b..ebd57c35a141 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -86,7 +86,6 @@ export default class ConfirmApproveContent extends Component { setUserAcknowledgedGasMissing: PropTypes.func, renderSimulationFailureWarning: PropTypes.bool, useCurrencyRateCheck: PropTypes.bool, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, }; state = { @@ -159,7 +158,6 @@ export default class ConfirmApproveContent extends Component { userAcknowledgedGasMissing, renderSimulationFailureWarning, useCurrencyRateCheck, - useNativeCurrencyAsPrimaryCurrency, } = this.props; if ( !hasLayer1GasFee && @@ -183,7 +181,6 @@ export default class ConfirmApproveContent extends Component { } noBold diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 2abec9ef4c13..ba1c7c5a568c 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -11,9 +11,7 @@ const renderComponent = (props) => { const store = configureMockStore([])({ metamask: { ...mockNetworkState({ chainId: '0x0' }), - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, currencyRates: {}, }, }); diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 0828c236a38f..a5dcaeb6202d 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -27,7 +27,6 @@ import { getRpcPrefsForCurrentProvider, checkNetworkAndAccountSupports1559, getUseCurrencyRateCheck, - getPreferences, } from '../../../selectors'; import { useApproveTransaction } from '../hooks/useApproveTransaction'; import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning'; @@ -84,7 +83,6 @@ export default function ConfirmApprove({ isAddressLedgerByFromAddress(userAddress), ); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const [customPermissionAmount, setCustomPermissionAmount] = useState(''); const [submitWarning, setSubmitWarning] = useState(''); const [isContract, setIsContract] = useState(false); @@ -298,9 +296,6 @@ export default function ConfirmApprove({ hasLayer1GasFee={layer1GasFee !== undefined} supportsEIP1559={supportsEIP1559} useCurrencyRateCheck={useCurrencyRateCheck} - useNativeCurrencyAsPrimaryCurrency={ - useNativeCurrencyAsPrimaryCurrency - } /> {showCustomizeGasPopover && !supportsEIP1559 && (
0.000021 + + ETH +
@@ -431,13 +436,18 @@ exports[`Confirm Transaction Base should match snapshot 1`] = `
0.000021 + + ETH +
diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index 31149997a39c..96fd5315e317 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -146,7 +146,6 @@ export default class ConfirmTransactionBase extends Component { secondaryTotalTextOverride: PropTypes.string, gasIsLoading: PropTypes.bool, primaryTotalTextOverrideMaxAmount: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, maxFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string, baseFeePerGas: PropTypes.string, @@ -399,7 +398,6 @@ export default class ConfirmTransactionBase extends Component { nextNonce, getNextNonce, txData, - useNativeCurrencyAsPrimaryCurrency, primaryTotalTextOverrideMaxAmount, showLedgerSteps, nativeCurrency, @@ -459,7 +457,6 @@ export default class ConfirmTransactionBase extends Component { type={PRIMARY} key="total-max-amount" value={getTotalAmount(useMaxFee)} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); } @@ -468,9 +465,8 @@ export default class ConfirmTransactionBase extends Component { const primaryTotal = useMaxFee ? primaryTotalTextOverrideMaxAmount : primaryTotalTextOverride; - const totalMaxAmount = useNativeCurrencyAsPrimaryCurrency - ? primaryTotal - : secondaryTotalTextOverride; + + const totalMaxAmount = primaryTotal; return isBoldTextAndNotOverridden ? ( {totalMaxAmount} @@ -500,14 +496,12 @@ export default class ConfirmTransactionBase extends Component { color: TextColor.textDefault, variant: TextVariant.bodyMdBold, }} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel />
); } - return useNativeCurrencyAsPrimaryCurrency - ? secondaryTotalTextOverride - : primaryTotalTextOverride; + return secondaryTotalTextOverride; }; const nextNonceValue = diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index ed012d07e5ce..5d92a1af9c56 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -45,7 +45,6 @@ import { getIsEthGasPriceFetched, getShouldShowFiat, checkNetworkAndAccountSupports1559, - getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, getEnsResolutionByAddress, @@ -266,7 +265,6 @@ const mapStateToProps = (state, ownProps) => { customNonceValue = getCustomNonceValue(state); const isEthGasPriceFetched = getIsEthGasPriceFetched(state); const noGasPrice = !supportsEIP1559 && getNoGasPriceFetched(state); - const { useNativeCurrencyAsPrimaryCurrency } = getPreferences(state); const gasFeeIsCustom = fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE || txParamsAreDappSuggested(fullTxData); @@ -347,7 +345,6 @@ const mapStateToProps = (state, ownProps) => { noGasPrice, supportsEIP1559, gasIsLoading: isGasEstimatesLoading || gasLoadingAnimationIsShowing, - useNativeCurrencyAsPrimaryCurrency, maxFeePerGas: gasEstimationObject.maxFeePerGas, maxPriorityFeePerGas: gasEstimationObject.maxPriorityFeePerGas, baseFeePerGas: gasEstimationObject.baseFeePerGas, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index f8a7b40430fb..bea6aef1d84d 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -108,9 +108,7 @@ const baseStore = { chainId: CHAIN_IDS.GOERLI, }), tokens: [], - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, + preferences: {}, currentCurrency: 'USD', currencyRates: {}, featureFlags: { diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js index 7caade1d14fb..156bca192523 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js @@ -4,6 +4,7 @@ import { Route, Switch, useHistory, useParams } from 'react-router-dom'; import { ENVIRONMENT_TYPE_NOTIFICATION, ORIGIN_METAMASK, + TRACE_ENABLED_SIGN_METHODS, } from '../../../../shared/constants/app'; import Loading from '../../../components/ui/loading-screen'; import { @@ -105,11 +106,15 @@ const ConfirmTransaction = () => { return undefined; } + const traceId = TRACE_ENABLED_SIGN_METHODS.includes(type) + ? transaction.msgParams?.requestId?.toString() + : id; + return await endBackgroundTrace({ name: TraceName.NotificationDisplay, - id, + id: traceId, }); - }, [id, isNotification]); + }, [id, isNotification, type, transaction.msgParams]); const transactionId = id; const isValidTokenMethod = isTokenMethodAction(type); diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index 30a1b6ad118c..1d27b332f7a5 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -24,15 +24,48 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = `
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -105,7 +138,7 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -340,7 +373,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB data-testid="confirmation__simulation_section" >
-
+
@@ -486,7 +521,9 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB
-
+
@@ -514,7 +551,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -1446,7 +1483,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS data-testid="confirmation__simulation_section" >
-
+
@@ -1570,7 +1609,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -2254,7 +2326,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -2987,7 +3311,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = data-testid="confirmation__simulation_section" >
-
+
@@ -3107,7 +3433,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Signature request

@@ -3772,7 +4098,7 @@ exports[`Confirm should match snapshot signature - typed sign - order 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
{ const mockStatePersonalSign = getMockPersonalSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStatePersonalSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot signature - typed sign - order', async () => { @@ -106,8 +107,8 @@ describe('Confirm', () => { }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); - let container; + let container; await act(async () => { const { container: renderContainer } = renderWithConfirmContextProvider( , @@ -123,13 +124,15 @@ describe('Confirm', () => { const mockStateTypedSign = getMockTypedSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStateTypedSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot for signature - typed sign - V4 - PermitSingle', async () => { @@ -147,12 +150,11 @@ describe('Confirm', () => { }); await act(async () => { - const { container, findByText } = await renderWithConfirmContextProvider( - , - mockStore, - ); + const { container, findAllByText } = + await renderWithConfirmContextProvider(, mockStore); - expect(await findByText('1,461,501,637,3...')).toBeInTheDocument(); + const valueElement = await findAllByText('14,615,016,373,...'); + expect(valueElement[0]).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); }); @@ -172,12 +174,11 @@ describe('Confirm', () => { }); await act(async () => { - const { container, findByText } = await renderWithConfirmContextProvider( - , - mockStore, - ); + const { container, findAllByText } = + await renderWithConfirmContextProvider(, mockStore); - expect(await findByText('1,461,501,637,3...')).toBeInTheDocument(); + const valueElement = await findAllByText('14,615,016,373,...'); + expect(valueElement[0]).toBeInTheDocument(); expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/confirm/confirm.tsx b/ui/pages/confirmations/confirm/confirm.tsx index 94c20b37fb07..67df9ccfd222 100644 --- a/ui/pages/confirmations/confirm/confirm.tsx +++ b/ui/pages/confirmations/confirm/confirm.tsx @@ -1,5 +1,5 @@ -import React, { ReactNode } from 'react'; import { ReactNodeLike } from 'prop-types'; +import React, { ReactNode } from 'react'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { MMISignatureMismatchBanner } from '../../../components/institutional/signature-mismatch-banner'; @@ -16,11 +16,11 @@ import { Header } from '../components/confirm/header'; import { Info } from '../components/confirm/info'; import { LedgerInfo } from '../components/confirm/ledger-info'; import { Nav } from '../components/confirm/nav'; +import { NetworkChangeToast } from '../components/confirm/network-change-toast'; import { PluggableSection } from '../components/confirm/pluggable-section'; import ScrollToBottom from '../components/confirm/scroll-to-bottom'; import { Title } from '../components/confirm/title'; import EditGasFeePopover from '../components/edit-gas-fee-popover'; -import { NetworkChangeToast } from '../components/confirm/network-change-toast'; import { ConfirmContextProvider, useConfirmContext } from '../context/confirm'; const EIP1559TransactionGasModal = () => { diff --git a/ui/pages/confirmations/confirm/stories/utils.tsx b/ui/pages/confirmations/confirm/stories/utils.tsx index dd194d574109..9c68a392cbd7 100644 --- a/ui/pages/confirmations/confirm/stories/utils.tsx +++ b/ui/pages/confirmations/confirm/stories/utils.tsx @@ -46,7 +46,7 @@ export function ConfirmStoryTemplate( }`, ]} > - } /> + } /> ); diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap index d85bbe7bb4ed..114355592125 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/create-snap-account.test.js.snap @@ -31,6 +31,7 @@ exports[`create-snap-account confirmation should match snapshot 1`] = ` >

Test Snap

diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap index 3acaa31478e7..db600570c273 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/remove-snap-account.test.js.snap @@ -31,6 +31,7 @@ exports[`remove-snap-account confirmation should match snapshot 1`] = ` >

Test Snap

diff --git a/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap b/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap index 11ac26234265..d7731522c967 100644 --- a/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap +++ b/ui/pages/confirmations/confirmation/templates/__snapshots__/snap-account-redirect.test.js.snap @@ -22,6 +22,7 @@ exports[`snap-account-redirect confirmation should match snapshot 1`] = ` >
{ isBlocking: true, key: 'insufficientBalance', message: - 'You do not have enough ETH in your account to pay for transaction fees.', + 'You do not have enough ETH in your account to pay for network fees.', reason: 'Insufficient funds', severity: Severity.Danger, }, diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts index ac2732d21688..55b0b0d8d94a 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useInsufficientBalanceAlerts.ts @@ -55,7 +55,7 @@ export function useInsufficientBalanceAlerts(): Alert[] { field: RowAlertKey.EstimatedFee, isBlocking: true, key: 'insufficientBalance', - message: t('alertMessageInsufficientBalance'), + message: t('alertMessageInsufficientBalance2'), reason: t('alertReasonInsufficientBalance'), severity: Severity.Danger, }, diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index 8af6bcf3ba40..908f600564f8 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -10,10 +10,10 @@ import { import { getCurrentCurrency, getShouldShowFiat, - getPreferences, txDataSelector, getCurrentKeyring, getTokenExchangeRates, + getPreferences, } from '../../../selectors'; import { @@ -118,7 +118,7 @@ export const generateUseSelectorRouter = } if (selector === getPreferences) { return { - useNativeCurrencyAsPrimaryCurrency: true, + showNativeTokenAsMainBalance: true, }; } if ( diff --git a/ui/pages/confirmations/hooks/useAssetDetails.js b/ui/pages/confirmations/hooks/useAssetDetails.js index 2d447c7f5995..4a9afaf05468 100644 --- a/ui/pages/confirmations/hooks/useAssetDetails.js +++ b/ui/pages/confirmations/hooks/useAssetDetails.js @@ -33,6 +33,10 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) { const prevTokenBalance = usePrevious(tokensWithBalances); useEffect(() => { + if (!tokenAddress && !userAddress && !transactionData) { + return; + } + async function getAndSetAssetDetails() { dispatch(showLoadingIndication()); const assetDetails = await getAssetDetails( @@ -65,6 +69,10 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) { prevTokenBalance, ]); + if (!tokenAddress && !userAddress && !transactionData) { + return {}; + } + if (currentAsset) { const { standard, diff --git a/ui/pages/confirmations/hooks/useLedgerConnection.test.ts b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts new file mode 100644 index 000000000000..7041b11b1aa4 --- /dev/null +++ b/ui/pages/confirmations/hooks/useLedgerConnection.test.ts @@ -0,0 +1,319 @@ +import type { KeyringObject } from '@metamask/keyring-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import { + HardwareTransportStates, + LEDGER_USB_VENDOR_ID, + LedgerTransportTypes, + WebHIDConnectedStatuses, +} from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; +import { getMockConfirmStateForTransaction } from '../../../../test/data/confirmations/helper'; +import { genUnapprovedApproveConfirmation } from '../../../../test/data/confirmations/token-approve'; +import { renderHookWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers'; +import { flushPromises } from '../../../../test/lib/timer-helpers'; +import * as appActions from '../../../ducks/app/app'; +import { attemptLedgerTransportCreation } from '../../../store/actions'; +import useLedgerConnection from './useLedgerConnection'; + +jest.mock('../../../store/actions', () => ({ + ...jest.requireActual('../../../store/actions'), + attemptLedgerTransportCreation: jest.fn(), +})); + +type RootState = { + metamask: Record; + appState: Record; +} & Record; + +const MOCK_LEDGER_ACCOUNT = '0x1234567890abcdef1234567890abcdef12345678'; + +const updateLedgerHardwareAccounts = (keyrings: KeyringObject[]) => { + const ledgerHardwareIndex = keyrings.findIndex( + (keyring) => keyring.type === KeyringType.ledger, + ); + + if (ledgerHardwareIndex === -1) { + // If 'Ledger Hardware' does not exist, create a new entry + keyrings.push({ + type: KeyringType.ledger, + accounts: [MOCK_LEDGER_ACCOUNT], + }); + } else { + // If 'Ledger Hardware' exists, update its accounts + keyrings[ledgerHardwareIndex].accounts = [MOCK_LEDGER_ACCOUNT]; + } + + return keyrings; +}; + +const generateUnapprovedConfirmationOnLedgerState = (address: Hex) => { + const transactionMeta = genUnapprovedApproveConfirmation({ + address, + chainId: '0x5', + }) as TransactionMeta; + + const clonedState = cloneDeep( + getMockConfirmStateForTransaction(transactionMeta), + ) as RootState; + + clonedState.metamask.keyrings = updateLedgerHardwareAccounts( + clonedState.metamask.keyrings as KeyringObject[], + ); + + clonedState.metamask.ledgerTransportType = LedgerTransportTypes.webhid; + + return clonedState; +}; + +describe('useLedgerConnection', () => { + const mockAttemptLedgerTransportCreation = jest.mocked( + attemptLedgerTransportCreation, + ); + + let state: RootState; + let originalNavigatorHid: HID; + + beforeEach(() => { + originalNavigatorHid = window.navigator.hid; + jest.resetAllMocks(); + Object.defineProperty(window.navigator, 'hid', { + value: { + getDevices: jest + .fn() + .mockImplementation(() => + Promise.resolve([{ vendorId: Number(LEDGER_USB_VENDOR_ID) }]), + ), + }, + configurable: true, + }); + + state = generateUnapprovedConfirmationOnLedgerState(MOCK_LEDGER_ACCOUNT); + }); + + afterAll(() => { + Object.defineProperty(window.navigator, 'hid', { + value: originalNavigatorHid, + configurable: true, + }); + }); + + describe('checks hid devices initially', () => { + it('set LedgerWebHidConnectedStatus to connected if it finds Ledger hid', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.notConnected; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).toHaveBeenCalledWith( + WebHIDConnectedStatuses.connected, + ); + }); + + it('set LedgerWebHidConnectedStatus to notConnected if it does not find Ledger hid', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.unknown; + + (window.navigator.hid.getDevices as jest.Mock).mockImplementationOnce( + () => Promise.resolve([]), + ); + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).toHaveBeenCalledWith( + WebHIDConnectedStatuses.notConnected, + ); + }); + }); + + describe('determines transport status', () => { + it('set LedgerTransportStatus to verified if transport creation is successful', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockResolvedValue(true); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.verified, + ); + }); + + it('set LedgerTransportStatus to unknownFailure if transport creation fails', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockResolvedValue(false); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.unknownFailure, + ); + }); + + it('set LedgerTransportStatus to deviceOpenFailure if device open fails', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('Failed to open the device'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.deviceOpenFailure, + ); + }); + + it('set LedgerTransportStatus to verified if device is already open', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('the device is already open'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.verified, + ); + }); + + it('set LedgerTransportStatus to unknownFailure if an unknown error occurs', async () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + mockAttemptLedgerTransportCreation.mockRejectedValue( + new Error('Unknown error'), + ); + + state.appState.ledgerWebHidConnectedStatus = + WebHIDConnectedStatuses.connected; + state.appState.ledgerTransportStatus = HardwareTransportStates.none; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.unknownFailure, + ); + }); + }); + + it('reset LedgerTransportStatus to none on unmount', () => { + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + const { unmount } = renderHookWithConfirmContextProvider( + useLedgerConnection, + state, + ); + + unmount(); + + expect(spyOnSetLedgerTransportStatus).toHaveBeenCalledWith( + HardwareTransportStates.none, + ); + }); + + describe('does nothing', () => { + it('when address is not a ledger address', async () => { + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + // Set state to have empty keyrings, simulating a non-Ledger address + state.metamask.keyrings = []; + + renderHookWithConfirmContextProvider(useLedgerConnection, state); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).not.toHaveBeenCalled(); + expect(spyOnSetLedgerTransportStatus).not.toHaveBeenCalled(); + }); + + it('when from address is not defined in currentConfirmation', async () => { + const tempState = generateUnapprovedConfirmationOnLedgerState( + undefined as unknown as Hex, + ); + + const spyOnSetLedgerWebHidConnectedStatus = jest.spyOn( + appActions, + 'setLedgerWebHidConnectedStatus', + ); + const spyOnSetLedgerTransportStatus = jest.spyOn( + appActions, + 'setLedgerTransportStatus', + ); + + renderHookWithConfirmContextProvider(useLedgerConnection, tempState); + + await flushPromises(); + + expect(spyOnSetLedgerWebHidConnectedStatus).not.toHaveBeenCalled(); + expect(spyOnSetLedgerTransportStatus).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 5fbad8445cd6..33a011c2966a 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -48,6 +48,7 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; +// This function is no longer used in codebase, to be deleted. export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); const dispatch = useDispatch(); @@ -61,8 +62,7 @@ export default function GasDisplay({ gasError }) { const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = - useSelector(getPreferences); + const { showFiatInTestnets } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); const nativeCurrency = useSelector(getNativeCurrency); const { chainId } = providerConfig; @@ -132,7 +132,6 @@ export default function GasDisplay({ gasError }) { type={PRIMARY} key="total-detail-value" value={hexTransactionTotal} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); @@ -144,10 +143,9 @@ export default function GasDisplay({ gasError }) { draftTransaction.amount.value, hexMaximumTransactionFee, )} - hideLabel={!useNativeCurrencyAsPrimaryCurrency} /> ); - } else if (useNativeCurrencyAsPrimaryCurrency) { + } else { detailTotal = primaryTotalTextOverrideMaxAmount; maxAmount = primaryTotalTextOverrideMaxAmount; } @@ -177,7 +175,7 @@ export default function GasDisplay({ gasError }) { type={SECONDARY} key="total-detail-text" value={hexTransactionTotal} - hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} + hideLabel /> ) diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 1fa7ff36085e..41ffd2832169 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -19,13 +19,14 @@ export const REDESIGN_APPROVAL_TYPES = [ export const REDESIGN_USER_TRANSACTION_TYPES = [ TransactionType.contractInteraction, TransactionType.deployContract, + TransactionType.tokenMethodApprove, + TransactionType.tokenMethodIncreaseAllowance, + TransactionType.tokenMethodSetApprovalForAll, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [ ...REDESIGN_USER_TRANSACTION_TYPES, - TransactionType.tokenMethodApprove, - TransactionType.tokenMethodIncreaseAllowance, - TransactionType.tokenMethodSetApprovalForAll, + TransactionType.tokenMethodTransfer, ]; const SIGNATURE_APPROVAL_TYPES = [ diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index 03b4cd5d7cf9..5a85a3eb5d3c 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -20,6 +20,7 @@ min-width: 0; display: flex; flex-direction: column; + padding-top: 8px; } &__connect-status-text { diff --git a/ui/pages/index.js b/ui/pages/index.js index 208436c9127a..0b1cdcef78cd 100644 --- a/ui/pages/index.js +++ b/ui/pages/index.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { HashRouter } from 'react-router-dom'; +import { CompatRouter } from 'react-router-dom-v5-compat'; import * as Sentry from '@sentry/browser'; import { I18nProvider, LegacyI18nProvider } from '../contexts/i18n'; import { @@ -43,19 +44,21 @@ class Index extends PureComponent { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx index cdefb3986d1f..d7a474ad3b24 100644 --- a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx +++ b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.tsx @@ -17,9 +17,7 @@ jest.mock('../../../store/institutional/institution-background', () => ({ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { @@ -50,9 +48,7 @@ describe('Confirm Add Custodian Token', () => { it('tries to connect to custodian with empty token', async () => { const customMockedStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, institutionalFeatures: { connectRequests: [ { diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx index 5719fb38015f..5044d6085812 100644 --- a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.tsx @@ -9,9 +9,7 @@ describe('Confirm Add Custodian Token', () => { const mockStore = { metamask: { - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, }, history: { push: '/', diff --git a/ui/pages/institutional/custody/custody.test.tsx b/ui/pages/institutional/custody/custody.test.tsx index 383e615492da..577e599397ba 100644 --- a/ui/pages/institutional/custody/custody.test.tsx +++ b/ui/pages/institutional/custody/custody.test.tsx @@ -99,9 +99,7 @@ describe('CustodyPage', function () { }, ], }, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, + preferences: {}, appState: { isLoading: false, }, diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.js index fab463e5b685..d91e3e54746d 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.js @@ -1,23 +1,34 @@ import React, { useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; - -import Box from '../../../components/ui/box'; -import { Text } from '../../../components/component-library'; -import Button from '../../../components/ui/button'; import { - FontWeight, - TextAlign, - AlignItems, + Button, + ButtonSize, + ButtonVariant, +} from '../../../components/component-library/button'; +import { TextVariant, + Display, + AlignItems, + JustifyContent, + FlexDirection, } from '../../../helpers/constants/design-system'; +import { + Box, + Text, + IconName, + ButtonLink, + ButtonLinkSize, + IconSize, +} from '../../../components/component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { ONBOARDING_PIN_EXTENSION_ROUTE, ONBOARDING_PRIVACY_SETTINGS_ROUTE, } from '../../../helpers/constants/routes'; -import { isBeta } from '../../../helpers/utils/build-types'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import { getFirstTimeFlowType } from '../../../selectors'; +import { getSeedPhraseBackedUp } from '../../../ducks/metamask/metamask'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -31,84 +42,160 @@ export default function CreationSuccessful() { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const seedPhraseBackedUp = useSelector(getSeedPhraseBackedUp); + const learnMoreLink = + 'https://support.metamask.io/hc/en-us/articles/360015489591-Basic-Safety-and-Security-Tips-for-MetaMask'; + const learnHowToKeepWordsSafe = + 'https://community.metamask.io/t/what-is-a-secret-recovery-phrase-and-how-to-keep-your-crypto-wallet-secure/3440'; const { createSession } = useCreateSession(); const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); return ( -
- - + + + + + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp + ? '🔓' + : '🎉'} + + - {t('walletCreationSuccessTitle')} + {firstTimeFlowType === FirstTimeFlowType.import && + t('yourWalletIsReady')} + + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp && + t('reminderSet')} + + {firstTimeFlowType === FirstTimeFlowType.create && + seedPhraseBackedUp && + t('congratulations')} - - {t('walletCreationSuccessDetail')} + + {firstTimeFlowType === FirstTimeFlowType.import && + t('rememberSRPIfYouLooseAccess', [ + + {t('learnHow')} + , + ])} + + {firstTimeFlowType === FirstTimeFlowType.create && + seedPhraseBackedUp && + t('walletProtectedAndReadyToUse', [ + + {t('securityPrivacyPath')} + , + ])} + {firstTimeFlowType === FirstTimeFlowType.create && + !seedPhraseBackedUp && + t('ifYouGetLockedOut', [ + {t('securityPrivacyPath')}, + ])} - + {t('keepReminderOfSRP', [ + + {t('learnMoreUpperCaseWithDot')} + , + ])} + + )} + + - {t('remember')} - -
    -
  • - - {isBeta() - ? t('betaWalletCreationSuccessReminder1') - : t('walletCreationSuccessReminder1')} - -
  • -
  • - - {isBeta() - ? t('betaWalletCreationSuccessReminder2') - : t('walletCreationSuccessReminder2')} - -
  • -
  • - - {t('walletCreationSuccessReminder3', [ - - {t('walletCreationSuccessReminder3BoldSection')} - , - ])} - -
  • -
  • - -
  • -
- + + {t('settingsOptimisedForEaseOfUseAndSecurity')} + + + + -
+ ); } diff --git a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js index 7d6c55f84642..5349a9f23f9e 100644 --- a/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js +++ b/ui/pages/onboarding-flow/creation-successful/creation-successful.test.js @@ -8,6 +8,8 @@ import { } from '../../../helpers/constants/routes'; import { setBackgroundConnection } from '../../../store/background-connection'; import { renderWithProvider } from '../../../../test/jest'; +import initializedMockState from '../../../../test/data/mock-state.json'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import CreationSuccessful from './creation-successful'; const mockHistoryPush = jest.fn(); @@ -25,7 +27,12 @@ jest.mock('react-router-dom', () => ({ describe('Creation Successful Onboarding View', () => { const mockStore = { - metamask: {}, + metamask: { + providerConfig: { + type: 'test', + }, + firstTimeFlowType: FirstTimeFlowType.import, + }, }; const store = configureMockStore([thunk])(mockStore); setBackgroundConnection({ completeOnboarding: completeOnboardingStub }); @@ -34,19 +41,94 @@ describe('Creation Successful Onboarding View', () => { jest.resetAllMocks(); }); - it('should redirect to privacy-settings view when "Advanced configuration" button is clicked', () => { + it('should remind the user to not loose the SRP and keep it safe (Import case)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.import, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + , + customMockStore, + ); + + expect(getByText('Your wallet is ready')).toBeInTheDocument(); + expect( + getByText( + /Remember, if you lose your Secret Recovery Phrase, you lose access to your wallet/u, + ), + ).toBeInTheDocument(); + }); + + it('should show the Congratulations! message to the user (New wallet & backed up SRP)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.create, + seedPhraseBackedUp: true, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + , + customMockStore, + ); + + expect(getByText('Congratulations!')).toBeInTheDocument(); + expect( + getByText(/Your wallet is protected and ready to use/u), + ).toBeInTheDocument(); + }); + + it('should show the Reminder set! message to the user (New wallet & did not backed up SRP)', () => { + const importFirstTimeFlowState = { + ...initializedMockState, + metamask: { + ...initializedMockState.metamask, + firstTimeFlowType: FirstTimeFlowType.create, + seedPhraseBackedUp: false, + }, + }; + const customMockStore = configureMockStore([thunk])( + importFirstTimeFlowState, + ); + + const { getByText } = renderWithProvider( + , + customMockStore, + ); + + expect(getByText('Reminder set!')).toBeInTheDocument(); + expect( + getByText( + /If you get locked out of the app or get a new device, you will lose your funds./u, + ), + ).toBeInTheDocument(); + }); + + it('should redirect to privacy-settings view when "Manage default settings" button is clicked', () => { const { getByText } = renderWithProvider(, store); - const privacySettingsButton = getByText('Advanced configuration'); + const privacySettingsButton = getByText('Manage default settings'); fireEvent.click(privacySettingsButton); expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PRIVACY_SETTINGS_ROUTE, ); }); - it('should route to pin extension route when "Got it" button is clicked', async () => { + it('should route to pin extension route when "Done" button is clicked', async () => { const { getByText } = renderWithProvider(, store); - const gotItButton = getByText('Got it'); - fireEvent.click(gotItButton); + const doneButton = getByText('Done'); + fireEvent.click(doneButton); await waitFor(() => { expect(mockHistoryPush).toHaveBeenCalledWith( ONBOARDING_PIN_EXTENSION_ROUTE, diff --git a/ui/pages/onboarding-flow/creation-successful/index.scss b/ui/pages/onboarding-flow/creation-successful/index.scss index ca05b3b1323e..bbb558627caf 100644 --- a/ui/pages/onboarding-flow/creation-successful/index.scss +++ b/ui/pages/onboarding-flow/creation-successful/index.scss @@ -1,46 +1,9 @@ @use "design-system"; .creation-successful { - @include design-system.screen-sm-min { - display: flex; - flex-direction: column; - align-items: center; - } - img { align-self: center; } max-width: 600px; - - ul { - list-style-type: disc; - max-width: 500px; - } - - li { - margin-left: 25px; - - a { - justify-content: flex-start; - padding: 0; - } - } - - &__bold { - font-weight: bold; - } - - &__actions { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; - - button { - margin-top: 14px; - max-width: 280px; - padding: 16px 0; - } - } } diff --git a/ui/pages/onboarding-flow/pin-extension/pin-extension.js b/ui/pages/onboarding-flow/pin-extension/pin-extension.js index 216bb1416cdf..c9ad1806d49d 100644 --- a/ui/pages/onboarding-flow/pin-extension/pin-extension.js +++ b/ui/pages/onboarding-flow/pin-extension/pin-extension.js @@ -1,13 +1,18 @@ import React, { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) useState, + useContext, ///: END:ONLY_INCLUDE_IF } from 'react'; import { useHistory } from 'react-router-dom'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Carousel } from 'react-responsive-carousel'; -import { setCompletedOnboarding } from '../../../store/actions'; +import { + setCompletedOnboarding, + performSignIn, + toggleExternalServices, +} from '../../../store/actions'; ///: END:ONLY_INCLUDE_IF import { useI18nContext } from '../../../hooks/useI18nContext'; import Button from '../../../components/ui/button'; @@ -30,6 +35,18 @@ import OnboardingPinMmiBillboard from '../../institutional/pin-mmi-billboard/pin ///: END:ONLY_INCLUDE_IF import { Text } from '../../../components/component-library'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + getFirstTimeFlowType, + getExternalServicesOnboardingToggleState, +} from '../../../selectors'; +import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; +import { selectParticipateInMetaMetrics } from '../../../selectors/metamask-notifications/authentication'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; import OnboardingPinBillboard from './pin-billboard'; ///: END:ONLY_INCLUDE_IF @@ -39,14 +56,37 @@ export default function OnboardingPinExtension() { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const [selectedIndex, setSelectedIndex] = useState(0); const dispatch = useDispatch(); - ///: END:ONLY_INCLUDE_IF + const trackEvent = useContext(MetaMetricsContext); + const firstTimeFlowType = useSelector(getFirstTimeFlowType); + + const externalServicesOnboardingToggleState = useSelector( + getExternalServicesOnboardingToggleState, + ); + const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); + const participateInMetaMetrics = useSelector(selectParticipateInMetaMetrics); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const handleClick = async () => { if (selectedIndex === 0) { setSelectedIndex(1); } else { + dispatch(toggleExternalServices(externalServicesOnboardingToggleState)); await dispatch(setCompletedOnboarding()); + + if (externalServicesOnboardingToggleState) { + if (!isProfileSyncingEnabled || participateInMetaMetrics) { + await dispatch(performSignIn()); + } + } + + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.OnboardingWalletSetupComplete, + properties: { + wallet_setup_type: + firstTimeFlowType === FirstTimeFlowType.import ? 'import' : 'new', + new_wallet: firstTimeFlowType === FirstTimeFlowType.create, + }, + }); history.push(DEFAULT_ROUTE); } }; diff --git a/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js b/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js index 8dc0529c86ae..00c7c38cf1d0 100644 --- a/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js +++ b/ui/pages/onboarding-flow/pin-extension/pin-extension.test.js @@ -11,6 +11,8 @@ const completeOnboardingStub = jest .fn() .mockImplementation(() => Promise.resolve()); +const toggleExternalServicesStub = jest.fn(); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: jest.fn(() => []), @@ -18,10 +20,20 @@ jest.mock('react-router-dom', () => ({ describe('Creation Successful Onboarding View', () => { const mockStore = { - metamask: {}, + metamask: { + providerConfig: { + type: 'test', + }, + }, + appState: { + externalServicesOnboardingToggleState: true, + }, }; const store = configureMockStore([thunk])(mockStore); - setBackgroundConnection({ completeOnboarding: completeOnboardingStub }); + setBackgroundConnection({ + completeOnboarding: completeOnboardingStub, + toggleExternalServices: toggleExternalServicesStub, + }); const pushMock = jest.fn(); beforeAll(() => { diff --git a/ui/pages/onboarding-flow/privacy-settings/index.scss b/ui/pages/onboarding-flow/privacy-settings/index.scss index 53ce477fe7af..6e3f793cc5a2 100644 --- a/ui/pages/onboarding-flow/privacy-settings/index.scss +++ b/ui/pages/onboarding-flow/privacy-settings/index.scss @@ -5,22 +5,16 @@ flex-direction: column; justify-content: center; align-items: center; + overflow-x: hidden; @include design-system.screen-sm-max { margin-bottom: 24px; } - @include design-system.screen-sm-min { - margin-bottom: 40px; - } - &__header { - display: flex; - justify-content: center; - flex-direction: column; - text-align: center; - max-width: 500px; - margin: 24px; + a { + color: var(--color-primary-default); + } } &__settings { @@ -29,11 +23,6 @@ max-width: 620px; margin-bottom: 20px; - @include design-system.screen-sm-min { - margin-inline-start: 48px; - margin-inline-end: 48px; - } - a { color: var(--color-primary-default); @@ -65,6 +54,36 @@ } } + .container { + display: flex; + width: 100%; + transition: transform 0.5s ease; + } + + .hidden { + display: none; + } + + .categories-item { + cursor: pointer; + } + + .list-view, + .detail-view { + flex: 0 0 100%; + width: 100%; + } + + /* slide in show the detail view */ + .container.show-detail { + transform: translateX(-100%); + } + + /* slide back to show the list view */ + .container.show-list { + transform: translateX(0%); + } + &__customizable-network:hover { cursor: pointer; } diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index 53cfe99efeb0..ca3bd0af2ff4 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -5,6 +5,7 @@ import { ButtonVariant } from '@metamask/snaps-sdk'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { addUrlProtocolPrefix } from '../../../../app/scripts/lib/util'; + import { useSetIsProfileSyncingEnabled, useEnableProfileSyncing, @@ -19,12 +20,12 @@ import { PRIVACY_POLICY_LINK, TRANSACTION_SIMULATIONS_LEARN_MORE_LINK, } from '../../../../shared/lib/ui-utils'; +import Button from '../../../components/ui/button'; + import { Box, Text, TextField, - ButtonPrimary, - ButtonPrimarySize, IconName, ButtonLink, AvatarNetwork, @@ -34,15 +35,17 @@ import { } from '../../../components/component-library'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - AlignItems, Display, - FlexDirection, - JustifyContent, TextAlign, TextColor, TextVariant, + IconColor, + AlignItems, + JustifyContent, + FlexDirection, + BlockSize, } from '../../../helpers/constants/design-system'; -import { ONBOARDING_PIN_EXTENSION_ROUTE } from '../../../helpers/constants/routes'; +import { ONBOARDING_COMPLETION_ROUTE } from '../../../helpers/constants/routes'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getPetnamesEnabled, @@ -50,9 +53,7 @@ import { getNetworkConfigurationsByChainId, } from '../../../selectors'; import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; -import { selectParticipateInMetaMetrics } from '../../../selectors/metamask-notifications/authentication'; import { - setCompletedOnboarding, setIpfsGateway, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, @@ -63,10 +64,8 @@ import { showModal, toggleNetworkMenu, setIncomingTransactionsPreferences, - toggleExternalServices, setUseTransactionSimulations, setPetnamesEnabled, - performSignIn, setEditedNetwork, } from '../../../store/actions'; import { @@ -116,6 +115,10 @@ export default function PrivacySettings() { const dispatch = useDispatch(); const history = useHistory(); + const [showDetail, setShowDetail] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [hiddenClass, setHiddenClass] = useState(true); + const defaultState = useSelector((state) => state.metamask); const { incomingTransactionsPreferences, @@ -128,7 +131,6 @@ export default function PrivacySettings() { useTransactionSimulations, } = defaultState; const petnamesEnabled = useSelector(getPetnamesEnabled); - const participateInMetaMetrics = useSelector(selectParticipateInMetaMetrics); const [usePhishingDetection, setUsePhishingDetection] = useState(null); const [turnOn4ByteResolution, setTurnOn4ByteResolution] = @@ -168,7 +170,6 @@ export default function PrivacySettings() { ); const handleSubmit = () => { - dispatch(toggleExternalServices(externalServicesOnboardingToggleState)); dispatch(setUsePhishDetect(phishingToggleState)); dispatch(setUse4ByteResolution(turnOn4ByteResolution)); dispatch(setUseTokenDetection(turnOnTokenDetection)); @@ -176,20 +177,12 @@ export default function PrivacySettings() { setUseMultiAccountBalanceChecker(isMultiAccountBalanceCheckerEnabled), ); dispatch(setUseCurrencyRateCheck(turnOnCurrencyRateCheck)); - dispatch(setCompletedOnboarding()); dispatch(setUseAddressBarEnsResolution(addressBarResolution)); setUseTransactionSimulations(isTransactionSimulationsEnabled); dispatch(setPetnamesEnabled(turnOnPetnames)); // Profile Syncing Setup - if (externalServicesOnboardingToggleState) { - if ( - profileSyncingProps.isProfileSyncingEnabled || - participateInMetaMetrics - ) { - dispatch(performSignIn()); - } - } else { + if (!externalServicesOnboardingToggleState) { profileSyncingProps.setIsProfileSyncingEnabled(false); } @@ -211,7 +204,7 @@ export default function PrivacySettings() { }, }); - history.push(ONBOARDING_PIN_EXTENSION_ROUTE); + history.push(ONBOARDING_COMPLETION_ROUTE); }; const handleUseProfileSync = async () => { @@ -242,352 +235,513 @@ export default function PrivacySettings() { } }; + const handleItemSelected = (item) => { + setSelectedItem(item); + setShowDetail(true); + + setTimeout(() => { + setHiddenClass(false); + }, 500); + }; + + const handleBack = () => { + setShowDetail(false); + setTimeout(() => { + setHiddenClass(true); + }, 500); + }; + + const items = [ + { id: 1, title: t('general'), subtitle: t('generalDescription') }, + { id: 2, title: t('assets'), subtitle: t('assetsDescription') }, + { id: 3, title: t('security'), subtitle: t('securityDescription') }, + ]; + return ( <>
-
- - {t('advancedConfiguration')} - - - {t('setAdvancedPrivacySettingsDetails')} - -
- { - if (toggledValue === false) { - dispatch(openBasicFunctionalityModal()); - } else { - dispatch(onboardingToggleBasicFunctionalityOn()); - trackEvent({ - category: MetaMetricsEventCategory.Onboarding, - event: MetaMetricsEventName.SettingsUpdated, - properties: { - settings_group: 'onboarding_advanced_configuration', - settings_type: 'basic_functionality', - old_value: false, - new_value: true, - was_profile_syncing_on: false, - }, - }); - } - }} - title={t('basicConfigurationLabel')} - description={t('basicConfigurationDescription', [ - - {t('privacyMsg')} - , - ])} - /> - - - dispatch(setIncomingTransactionsPreferences(chainId, value)) - } - incomingTransactionsPreferences={incomingTransactionsPreferences} - /> - - + + - {t('profileSyncPrivacyLink')} - , - ])} - /> - {profileSyncingProps.profileSyncingError && ( - - - {t('notificationsSettingsBoxError')} +
+ +
+ +
diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js index 80561e9376ae..ec8b88fc52e9 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js @@ -97,8 +97,8 @@ describe('Privacy Settings Onboarding View', () => { disableProfileSyncing: disableProfileSyncingStub, }); - it('should update preferences', () => { - const { container, getByText } = renderWithProvider( + it('should update the default settings from each category', () => { + const { container, queryByTestId } = renderWithProvider( , store, ); @@ -114,61 +114,76 @@ describe('Privacy Settings Onboarding View', () => { expect(setUseTransactionSimulationsStub).toHaveBeenCalledTimes(0); expect(setPreferenceStub).toHaveBeenCalledTimes(0); - const toggles = container.querySelectorAll('input[type=checkbox]'); - const submitButton = getByText('Done'); - // TODO: refactor this toggle array, not very readable - // toggle to false + // Default Settings - General category + const itemCategoryGeneral = queryByTestId('category-item-General'); + expect(itemCategoryGeneral).toBeInTheDocument(); + fireEvent.click(itemCategoryGeneral); + + let toggles = container.querySelectorAll('input[type=checkbox]'); + const backButton = queryByTestId('privacy-settings-back-button'); fireEvent.click(toggles[0]); // toggleExternalServicesStub - fireEvent.click(toggles[1]); // setIncomingTransactionsPreferencesStub - fireEvent.click(toggles[2]); // setIncomingTransactionsPreferencesStub (2) - fireEvent.click(toggles[3]); // setIncomingTransactionsPreferencesStub (3) - fireEvent.click(toggles[4]); // setIncomingTransactionsPreferencesStub (4) - fireEvent.click(toggles[5]); // setUsePhishDetectStub - fireEvent.click(toggles[6]); - fireEvent.click(toggles[7]); // setUse4ByteResolutionStub - fireEvent.click(toggles[8]); // setUseTokenDetectionStub - fireEvent.click(toggles[9]); // setUseMultiAccountBalanceCheckerStub - fireEvent.click(toggles[10]); // setUseTransactionSimulationsStub - fireEvent.click(toggles[11]); // setUseAddressBarEnsResolutionStub - fireEvent.click(toggles[12]); // setUseCurrencyRateCheckStub - fireEvent.click(toggles[13]); // setPreferenceStub - - expect(mockOpenBasicFunctionalityModal).toHaveBeenCalledTimes(1); - - fireEvent.click(submitButton); - - expect(toggleExternalServicesStub).toHaveBeenCalledTimes(1); - expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledTimes(4); - expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); - expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); + + // Default Settings - Assets category + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + + toggles = container.querySelectorAll('input[type=checkbox]'); + + fireEvent.click(toggles[0]); // setUseTokenDetectionStub + fireEvent.click(toggles[1]); // setUseTransactionSimulationsStub + + fireEvent.click(toggles[2]); // setIncomingTransactionsPreferencesStub + fireEvent.click(toggles[3]); // setIncomingTransactionsPreferencesStub (2) + fireEvent.click(toggles[4]); // setIncomingTransactionsPreferencesStub (3) + fireEvent.click(toggles[5]); // setIncomingTransactionsPreferencesStub (4) + + fireEvent.click(toggles[6]); // setUseCurrencyRateCheckStub + fireEvent.click(toggles[7]); // setUseAddressBarEnsResolutionStub + fireEvent.click(toggles[8]); // setUseMultiAccountBalanceCheckerStub + + // Default Settings - Security category + const itemCategorySecurity = queryByTestId('category-item-Security'); + fireEvent.click(itemCategorySecurity); + + toggles = container.querySelectorAll('input[type=checkbox]'); + + fireEvent.click(toggles[0]); // setUsePhishDetectStub + fireEvent.click(toggles[1]); // setUse4ByteResolutionStub + fireEvent.click(toggles[2]); // setPreferenceStub + + fireEvent.click(backButton); + expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(1); - expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1); - expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(1); - expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(1); + expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(true); expect(setUseTransactionSimulationsStub).toHaveBeenCalledTimes(1); - expect(setPreferenceStub).toHaveBeenCalledTimes(1); + expect(setUseTransactionSimulationsStub.mock.calls[0][0]).toStrictEqual( + false, + ); + expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledTimes(4); expect(setIncomingTransactionsPreferencesStub).toHaveBeenCalledWith( CHAIN_IDS.MAINNET, false, expect.anything(), ); - // toggleExternalServices is true still because modal is "open" but not confirmed yet - expect(toggleExternalServicesStub.mock.calls[0][0]).toStrictEqual(true); - expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); - expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); - expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(true); - expect(setUseMultiAccountBalanceCheckerStub.mock.calls[0][0]).toStrictEqual( - false, - ); + + expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(1); expect(setUseCurrencyRateCheckStub.mock.calls[0][0]).toStrictEqual(false); + expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(1); expect(setUseAddressBarEnsResolutionStub.mock.calls[0][0]).toStrictEqual( false, ); - expect(setUseTransactionSimulationsStub.mock.calls[0][0]).toStrictEqual( + expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1); + expect(setUseMultiAccountBalanceCheckerStub.mock.calls[0][0]).toStrictEqual( false, ); + + expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); + expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); + expect(setUse4ByteResolutionStub).toHaveBeenCalledTimes(1); + expect(setUse4ByteResolutionStub.mock.calls[0][0]).toStrictEqual(false); + expect(setPreferenceStub).toHaveBeenCalledTimes(1); expect(setPreferenceStub.mock.calls[0][0]).toStrictEqual( 'petnamesEnabled', false, @@ -182,6 +197,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { @@ -194,8 +212,8 @@ describe('Privacy Settings Onboarding View', () => { const validIpfsUrl = queryByText('IPFS gateway URL is valid'); expect(validIpfsUrl).toBeInTheDocument(); - const submitButton = queryByText('Done'); - fireEvent.click(submitButton); + const backButton = queryByTestId('privacy-settings-back-button'); + fireEvent.click(backButton); expect(setIpfsGatewayStub).toHaveBeenCalled(); }); @@ -206,6 +224,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { @@ -226,6 +247,9 @@ describe('Privacy Settings Onboarding View', () => { store, ); + const itemCategoryAssets = queryByTestId('category-item-Assets'); + fireEvent.click(itemCategoryAssets); + const ipfsInput = queryByTestId('ipfs-input'); const ipfsEvent = { target: { diff --git a/ui/pages/onboarding-flow/privacy-settings/setting.js b/ui/pages/onboarding-flow/privacy-settings/setting.js index 31ee059d1126..5811707603c0 100644 --- a/ui/pages/onboarding-flow/privacy-settings/setting.js +++ b/ui/pages/onboarding-flow/privacy-settings/setting.js @@ -7,6 +7,7 @@ import { TextVariant, AlignItems, Display, + TextColor, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -25,7 +26,7 @@ export const Setting = ({
{title} - + {description}
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/connect-page/__snapshots__/connect-page.test.tsx.snap b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap new file mode 100644 index 000000000000..6353df3e96cc --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/__snapshots__/connect-page.test.tsx.snap @@ -0,0 +1,251 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConnectPage should render correctly 1`] = ` +
+
+
+
+
+

+

+ Connect with MetaMask +

+

+ This site wants to + : +

+

+
+
+
+
+
+
+ +
+
+

+ See your accounts and suggest transactions +

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

+ Use your enabled networks +

+
+ + Requesting for + +
Alerts"" + data-tooltipped="" + style="display: inline;" + > +
+
+
+
+ G +
+
+
+
+ Custom Mainnet RPC logo +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+`; diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx new file mode 100644 index 000000000000..d7c50c6aa501 --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; +import { ConnectPage, ConnectPageRequest } from './connect-page'; + +const render = ( + props: { + request: ConnectPageRequest; + permissionsRequestId: string; + rejectPermissionsRequest: (id: string) => void; + approveConnection: (request: ConnectPageRequest) => void; + activeTabOrigin: string; + } = { + request: { + id: '1', + origin: 'https://test.dapp', + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }, + state = {}, +) => { + const store = configureStore({ + ...mockState, + metamask: { + ...mockState.metamask, + ...state, + permissionHistory: { + 'https://test.dapp': { + eth_accounts: { + accounts: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1709225290848, + }, + }, + }, + }, + }, + activeTab: { + origin: 'https://test.dapp', + }, + }); + return renderWithProvider(, store); +}; +describe('ConnectPage', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should render title correctly', () => { + const { getByText } = render(); + expect(getByText('Connect with MetaMask')).toBeDefined(); + }); + + it('should render account connectionListItem', () => { + const { getByText } = render(); + expect( + getByText('See your accounts and suggest transactions'), + ).toBeDefined(); + }); + + it('should render network connectionListItem', () => { + const { getByText } = render(); + expect(getByText('Use your enabled networks')).toBeDefined(); + }); + + it('should render confirm and cancel button', () => { + const { getByText } = render(); + const confirmButton = getByText('Connect'); + const cancelButton = getByText('Cancel'); + expect(confirmButton).toBeDefined(); + expect(cancelButton).toBeDefined(); + }); +}); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx new file mode 100644 index 000000000000..a30047fbd38a --- /dev/null +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -0,0 +1,159 @@ +import React, { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + getInternalAccounts, + getNetworkConfigurationsByChainId, + getSelectedInternalAccount, + getUpdatedAndSortedAccounts, +} from '../../../selectors'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Text, +} from '../../../components/component-library'; +import { + Content, + Footer, + Header, + Page, +} from '../../../components/multichain/pages/page'; +import { SiteCell } from '../../../components/multichain/pages/review-permissions-page'; +import { + BackgroundColor, + BlockSize, + Display, + FlexDirection, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { MergedInternalAccount } from '../../../selectors/selectors.types'; +import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu'; +import { TEST_CHAINS } from '../../../../shared/constants/network'; +import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer'; + +export type ConnectPageRequest = { + id: string; + origin: string; +}; + +type ConnectPageProps = { + request: ConnectPageRequest; + permissionsRequestId: string; + rejectPermissionsRequest: (id: string) => void; + approveConnection: (request: ConnectPageRequest) => void; + activeTabOrigin: string; +}; + +export const ConnectPage: React.FC = ({ + request, + permissionsRequestId, + rejectPermissionsRequest, + approveConnection, + activeTabOrigin, +}) => { + const t = useI18nContext(); + + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + const [nonTestNetworks, testNetworks] = useMemo( + () => + Object.entries(networkConfigurations).reduce( + ([nonTestNetworksList, testNetworksList], [chainId, network]) => { + const isTest = (TEST_CHAINS as string[]).includes(chainId); + (isTest ? testNetworksList : nonTestNetworksList).push(network); + return [nonTestNetworksList, testNetworksList]; + }, + [[] as NetworkConfiguration[], [] as NetworkConfiguration[]], + ), + [networkConfigurations], + ); + const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId); + const [selectedChainIds, setSelectedChainIds] = useState( + defaultSelectedChainIds, + ); + + const accounts = useSelector(getUpdatedAndSortedAccounts); + const internalAccounts = useSelector(getInternalAccounts); + const mergedAccounts: MergedInternalAccount[] = useMemo(() => { + return mergeAccounts(accounts, internalAccounts).filter( + (account: InternalAccount) => isEvmAccountType(account.type), + ); + }, [accounts, internalAccounts]); + + const currentAccount = useSelector(getSelectedInternalAccount); + const defaultAccountsAddresses = [currentAccount?.address]; + const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( + defaultAccountsAddresses, + ); + + const onConfirm = () => { + const _request = { + ...request, + approvedAccounts: selectedAccountAddresses, + approvedChainIds: selectedChainIds, + }; + approveConnection(_request); + }; + + return ( + +
+ {t('connectWithMetaMask')} + {t('connectionDescription')}: +
+ + + +
+ + + + + + + +
+
+ ); +}; diff --git a/ui/pages/permissions-connect/index.scss b/ui/pages/permissions-connect/index.scss index 513809505d50..954ec7a1121c 100644 --- a/ui/pages/permissions-connect/index.scss +++ b/ui/pages/permissions-connect/index.scss @@ -44,4 +44,8 @@ justify-self: flex-end; font-weight: bold; } + + .connect-page { + background-color: var(--color-background-alternative); // main-container adds the width but overrides the boxProps. So, we need extra class to apply css + } } diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index e5adf45a43fe..417a82777b36 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -19,12 +19,14 @@ 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'; import SnapInstall from './snaps/snap-install'; import SnapUpdate from './snaps/snap-update'; import SnapResult from './snaps/snap-result'; +import { ConnectPage } from './connect-page/connect-page'; const APPROVE_TIMEOUT = MILLISECOND * 1200; @@ -155,7 +157,6 @@ export default class PermissionConnect extends Component { ) { history.replace(confirmPermissionPath); } - if (history.location.pathname === connectPath && !isRequestingAccounts) { switch (requestType) { case 'wallet_installSnap': @@ -292,9 +293,14 @@ export default class PermissionConnect extends Component { ); } + approveConnection = (...args) => { + const { approvePermissionsRequest } = this.props; + approvePermissionsRequest(...args); + this.redirect(true); + }; + render() { const { - approvePermissionsRequest, accounts, showNewAccountModal, newAccountNumber, @@ -314,6 +320,7 @@ export default class PermissionConnect extends Component { approvePendingApproval, rejectPendingApproval, setSnapsInstallPrivacyWarningShownStatus, + approvePermissionsRequest, } = this.props; const { selectedAccountAddresses, @@ -322,6 +329,8 @@ export default class PermissionConnect extends Component { snapsInstallPrivacyWarningShown, } = this.state; + const isRequestingSnap = isSnapId(permissionsRequest?.metadata?.origin); + return (
{!hideTopBar && this.renderTopBar(permissionsRequestId)} @@ -332,27 +341,41 @@ export default class PermissionConnect extends Component { ( - this.selectAccounts(addresses)} - selectNewAccountViaModal={(handleAccountClick) => { - showNewAccountModal({ - onCreateNewAccount: (address) => - handleAccountClick(address), - newAccountNumber, - }); - }} - addressLastConnectedMap={addressLastConnectedMap} - cancelPermissionsRequest={(requestId) => - this.cancelPermissionsRequest(requestId) - } - permissionsRequestId={permissionsRequestId} - selectedAccountAddresses={selectedAccountAddresses} - targetSubjectMetadata={targetSubjectMetadata} - /> - )} + render={() => + isRequestingSnap ? ( + + this.selectAccounts(addresses) + } + selectNewAccountViaModal={(handleAccountClick) => { + showNewAccountModal({ + onCreateNewAccount: (address) => + handleAccountClick(address), + newAccountNumber, + }); + }} + addressLastConnectedMap={addressLastConnectedMap} + cancelPermissionsRequest={(requestId) => + this.cancelPermissionsRequest(requestId) + } + permissionsRequestId={permissionsRequestId} + selectedAccountAddresses={selectedAccountAddresses} + targetSubjectMetadata={targetSubjectMetadata} + /> + ) : ( + + this.cancelPermissionsRequest(requestId) + } + activeTabOrigin={this.state.origin} + request={permissionsRequest} + permissionsRequestId={permissionsRequestId} + approveConnection={this.approveConnection} + /> + ) + } /> ( { - approvePermissionsRequest(...args); - this.redirect(true); - }} + approveConnection={this.approveConnection} rejectConnection={(requestId) => this.cancelPermissionsRequest(requestId) } 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 5c91d49c5266..25c41ca37c82 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -11,6 +11,7 @@ import Home from '../home'; import { PermissionsPage, Connections, + ReviewPermissions, } from '../../components/multichain/pages'; import Settings from '../settings'; import Authenticated from '../../helpers/higher-order-components/authenticated'; @@ -37,6 +38,7 @@ import { ToastContainer, Toast, } from '../../components/multichain'; +import { SurveyToast } from '../../components/ui/survey-toast'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; @@ -77,6 +79,7 @@ import { TOKEN_DETAILS, CONNECTIONS, PERMISSIONS, + REVIEW_PERMISSIONS, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) INSTITUTIONAL_FEATURES_DONE_ROUTE, CUSTODY_ACCOUNT_DONE_ROUTE, @@ -189,6 +192,8 @@ export default class Routes extends Component { accountDetailsAddress: PropTypes.string, isImportNftsModalOpen: PropTypes.bool.isRequired, hideImportNftsModal: PropTypes.func.isRequired, + isPermittedNetworkToastOpen: PropTypes.bool.isRequired, + hidePermittedNetworkToast: PropTypes.func.isRequired, isIpfsModalOpen: PropTypes.bool.isRequired, isBasicConfigurationModalOpen: PropTypes.bool.isRequired, hideIpfsModal: PropTypes.func.isRequired, @@ -199,6 +204,7 @@ export default class Routes extends Component { addPermittedAccount: PropTypes.func.isRequired, switchedNetworkDetails: PropTypes.object, useNftDetection: PropTypes.bool, + currentNetwork: PropTypes.object, showNftEnablementToast: PropTypes.bool, setHideNftEnablementToast: PropTypes.func.isRequired, clearSwitchedNetworkDetails: PropTypes.func.isRequired, @@ -439,6 +445,11 @@ export default class Routes extends Component { component={Connections} /> + ); @@ -554,6 +565,17 @@ export default class Routes extends Component { return true; } + const isReviewPermissionsPgae = Boolean( + matchPath(location.pathname, { + path: REVIEW_PERMISSIONS, + exact: false, + }), + ); + + if (isReviewPermissionsPgae) { + return true; + } + if (windowType === ENVIRONMENT_TYPE_POPUP && this.onConfirmPage()) { return true; } @@ -635,14 +657,16 @@ export default class Routes extends Component { useNftDetection, showNftEnablementToast, setHideNftEnablementToast, + isPermittedNetworkToastOpen, + currentNetwork, } = this.props; const showAutoNetworkSwitchToast = this.getShowAutoNetworkSwitchTest(); const isPrivacyToastRecent = this.getIsPrivacyToastRecent(); const isPrivacyToastNotShown = !newPrivacyPolicyToastShownDate; const isEvmAccount = isEvmAccountType(account?.type); - const autoHideToastDelay = 5 * SECOND; + const safeEncodedHost = encodeURIComponent(activeTabOrigin); const onAutoHideToast = () => { setHideNftEnablementToast(false); @@ -653,6 +677,7 @@ export default class Routes extends Component { return ( + {showConnectAccountToast && !this.state.hideConnectAccountToast && isEvmAccount ? ( @@ -735,7 +760,7 @@ export default class Routes extends Component { } @@ -761,6 +786,32 @@ export default class Routes extends Component { onAutoHideToast={onAutoHideToast} /> ) : null} + + {isPermittedNetworkToastOpen ? ( + + } + text={this.context.t('permittedChainToastUpdate', [ + getURLHost(activeTabOrigin), + currentNetwork?.nickname, + ])} + actionText={this.context.t('editPermissions')} + onActionClick={() => { + this.props.hidePermittedNetworkToast(); + this.props.history.push( + `${REVIEW_PERMISSIONS}/${safeEncodedHost}`, + ); + }} + onClose={() => this.props.hidePermittedNetworkToast()} + /> + ) : null} ); } @@ -929,6 +980,7 @@ export default class Routes extends Component { {isImportNftsModalOpen ? ( hideImportNftsModal()} /> ) : null} + {isIpfsModalOpen ? ( hideIpfsModal()} /> ) : null} diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index ec8c4e96c864..6151fedc687b 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -115,6 +115,13 @@ describe('Routes Component', () => { 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/routes/routes.container.js b/ui/pages/routes/routes.container.js index 856aa8b53ade..419daf561778 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -28,6 +28,7 @@ import { getUseRequestQueue, getUseNftDetection, getNftDetectionEnablementToast, + getCurrentNetwork, } from '../../selectors'; import { getSmartTransactionsOptInStatus } from '../../../shared/modules/selectors'; import { @@ -52,6 +53,7 @@ import { hideKeyringRemovalResultModal, ///: END:ONLY_INCLUDE_IF setEditedNetwork, + hidePermittedNetworkToast, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -77,6 +79,7 @@ function mapStateToProps(state) { const account = getSelectedAccount(state); const activeTabOrigin = activeTab?.origin; const connectedAccounts = getPermittedAccountsForCurrentTab(state); + const currentNetwork = getCurrentNetwork(state); const showConnectAccountToast = Boolean( allowShowAccountSetting && account && @@ -129,10 +132,12 @@ function mapStateToProps(state) { accountDetailsAddress: state.appState.accountDetailsAddress, isImportNftsModalOpen: state.appState.importNftsModal.open, isIpfsModalOpen: state.appState.showIpfsModalOpen, + isPermittedNetworkToastOpen: state.appState.showPermittedNetworkToastOpen, switchedNetworkDetails, useNftDetection, showNftEnablementToast, networkToAutomaticallySwitchTo, + currentNetwork, totalUnapprovedConfirmationCount: getNumberOfAllUnapprovedTransactionsAndMessages(state), neverShowSwitchedNetworkMessage: getNeverShowSwitchedNetworkMessage(state), @@ -160,6 +165,7 @@ function mapDispatchToProps(dispatch) { toggleNetworkMenu: () => dispatch(toggleNetworkMenu()), hideImportNftsModal: () => dispatch(hideImportNftsModal()), hideIpfsModal: () => dispatch(hideIpfsModal()), + hidePermittedNetworkToast: () => dispatch(hidePermittedNetworkToast()), hideImportTokensModal: () => dispatch(hideImportTokensModal()), hideDeprecatedNetworkModal: () => dispatch(hideDeprecatedNetworkModal()), addPermittedAccount: (activeTabOrigin, address) => diff --git a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap index fcc11ec8336b..6318abd37570 100644 --- a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap +++ b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap @@ -501,7 +501,7 @@ exports[`AdvancedTab Component should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
{ - const t = useI18nContext(); - const settingsRefs = useRef(); - - useEffect(() => { - handleSettingsRefs(t, t('alerts'), settingsRefs); - }, [settingsRefs, t]); - - const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]); - - return ( - <> -
- {title} -
- - - - setAlertEnabledness(alertId, !isEnabled)} - value={isEnabled} - /> -
-
- - ); -}; - -AlertSettingsEntry.propTypes = { - alertId: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, -}; - -const AlertsTab = () => { - const t = useI18nContext(); - - const alertConfig = { - [AlertTypes.unconnectedAccount]: { - title: t('alertSettingsUnconnectedAccount'), - description: t('alertSettingsUnconnectedAccountDescription'), - }, - [AlertTypes.web3ShimUsage]: { - title: t('alertSettingsWeb3ShimUsage'), - description: t('alertSettingsWeb3ShimUsageDescription'), - }, - }; - - return ( -
- {Object.entries(alertConfig).map( - ([alertId, { title, description }], _) => ( - - ), - )} -
- ); -}; - -export default AlertsTab; diff --git a/ui/pages/settings/alerts-tab/alerts-tab.scss b/ui/pages/settings/alerts-tab/alerts-tab.scss deleted file mode 100644 index 0e8ee8f83983..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.scss +++ /dev/null @@ -1,38 +0,0 @@ -@use "design-system"; - -.alerts-tab { - &__body { - @include design-system.H6; - - display: grid; - grid-template-columns: 8fr 30px max-content; - grid-template-rows: 1fr 1fr; - align-items: center; - display: block; - } - - &__description-container { - display: flex; - } - - &__description-container > * { - padding: 0 8px; - } - - &__description { - display: flex; - align-items: center; - - &__icon { - color: var(--color-icon-alternative); - } - } - - &__item { - border-bottom: 1px solid var(--color-border-muted); - padding: 16px 32px; - display: flex; - justify-content: space-between; - align-items: center; - } -} diff --git a/ui/pages/settings/alerts-tab/alerts-tab.stories.js b/ui/pages/settings/alerts-tab/alerts-tab.stories.js deleted file mode 100644 index 65b9dfd12d11..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.stories.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import AlertsTab from './alerts-tab'; - -export default { - title: 'Components/UI/Pages/AlertsTab ', - - component: AlertsTab, -}; - -export const DefaultAlertsTab = () => { - return ; -}; - -DefaultAlertsTab.storyName = 'Default'; diff --git a/ui/pages/settings/alerts-tab/alerts-tab.test.js b/ui/pages/settings/alerts-tab/alerts-tab.test.js deleted file mode 100644 index 8750c4a9a3ec..000000000000 --- a/ui/pages/settings/alerts-tab/alerts-tab.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/jest'; -import { AlertTypes } from '../../../../shared/constants/alerts'; -import AlertsTab from '.'; - -const mockSetAlertEnabledness = jest.fn(); - -jest.mock('../../../store/actions', () => ({ - setAlertEnabledness: (...args) => mockSetAlertEnabledness(...args), -})); - -describe('Alerts Tab', () => { - const store = configureMockStore([])({ - metamask: { - alertEnabledness: { - unconnectedAccount: false, - web3ShimUsage: false, - }, - }, - }); - - it('calls setAlertEnabledness with the correct params method when the toggles are clicked', () => { - renderWithProvider(, store); - - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(0); - fireEvent.click(screen.getAllByRole('checkbox')[0]); - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(1); - expect(mockSetAlertEnabledness.mock.calls[0][0]).toBe( - AlertTypes.unconnectedAccount, - ); - expect(mockSetAlertEnabledness.mock.calls[0][1]).toBe(true); - - fireEvent.click(screen.getAllByRole('checkbox')[1]); - expect(mockSetAlertEnabledness.mock.calls).toHaveLength(2); - expect(mockSetAlertEnabledness.mock.calls[1][0]).toBe( - AlertTypes.web3ShimUsage, - ); - expect(mockSetAlertEnabledness.mock.calls[1][1]).toBe(true); - }); -}); diff --git a/ui/pages/settings/alerts-tab/index.js b/ui/pages/settings/alerts-tab/index.js deleted file mode 100644 index f6aa526da73e..000000000000 --- a/ui/pages/settings/alerts-tab/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './alerts-tab'; 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/settings/experimental-tab/experimental-tab.component.tsx b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx index c59cd6ead481..f1d1f610a7a4 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.component.tsx +++ b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx @@ -394,7 +394,6 @@ export default class ExperimentalTab extends PureComponent ///: BEGIN:ONLY_INCLUDE_IF(build-flask) // We're only setting the code fences here since // we should remove it for the feature release - /* Section: Bitcoin Accounts */ this.renderBitcoinSupport() ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index 48e12e8adebc..f57e1c310998 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -1,7 +1,6 @@ @use "design-system"; @import 'info-tab/index'; -@import 'alerts-tab/alerts-tab'; @import 'developer-options-tab/index'; @import 'networks-tab/index'; @import 'settings-tab/index'; @@ -263,11 +262,11 @@ } &__body { - padding: 24px; + padding: 0 16px 16px 16px; } &__content-row { - padding: 10px 0 20px; + padding: 16px 0 0; @include design-system.screen-sm-max { flex-wrap: wrap; @@ -296,6 +295,12 @@ margin-top: 10px; } + &__title { + font-size: 14px; + font-weight: 500; + line-height: 22px; + } + &__identicon { display: flex; flex-direction: row; @@ -326,11 +331,7 @@ &__description { @include design-system.H6; - margin-top: 8px; - margin-bottom: 12px; - color: var(--color-text-default); - font-size: 14px; - font-weight: 400; + line-height: 22px; } } diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index a57775ed145b..dcec71767fe6 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -551,7 +551,7 @@ exports[`Security Tab should match snapshot 1`] = ` class="mm-box mm-incoming-transaction-toggle" >

Show incoming transactions

@@ -1020,7 +1020,7 @@ exports[`Security Tab should match snapshot 1`] = ` class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth" >
+
+
+ + Delete MetaMetrics data + +
+ + + This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our + + Privacy policy + + . + + +
+
+
+
+ +

+ Since you've never opted into MetaMetrics, there's no data to delete here. +

+
+ +
+
diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx new file mode 100644 index 000000000000..27132fb82f5c --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../../store/store'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; + +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DeleteMetaMetricsDataButton from './delete-metametrics-data-button'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + +describe('DeleteMetaMetricsDataButton', () => { + const useSelectorMock = useSelector as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + const mockDispatch = jest.fn(); + + beforeEach(() => { + useDispatchMock.mockReturnValue(mockDispatch); + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return undefined; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return ''; + } + + return undefined; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const store = configureStore({}); + const { getByTestId, getAllByText, container } = renderWithProvider( + , + store, + ); + expect(getByTestId('delete-metametrics-data-button')).toBeInTheDocument(); + expect(getAllByText('Delete MetaMetrics data')).toHaveLength(2); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when metrics is opted in and metametrics id is available ', async () => { + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when page mounts after a deletion task is performed and more data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + + // if user does not opt in to participate in metrics or for profile sync, metametricsId will not be created. + it('should disable the data deletion button when there is metametrics id not available', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return null; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + expect( + container.querySelector('.settings-page__content-item-col')?.textContent, + ).toMatchInlineSnapshot( + `"Since you've never opted into MetaMetrics, there's no data to delete here.Delete MetaMetrics data"`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + , + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + it('should open the modal on the button click', () => { + const store = configureStore({}); + const { getByRole } = renderWithProvider( + , + store, + ); + const deleteButton = getByRole('button', { + name: 'Delete MetaMetrics data', + }); + fireEvent.click(deleteButton); + expect(mockDispatch).toHaveBeenCalledWith(openDeleteMetaMetricsDataModal()); + }); +}); diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx new file mode 100644 index 000000000000..34b61697ed95 --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CONSENSYS_PRIVACY_LINK } from '../../../../../shared/lib/ui-utils'; +import ClearMetametricsData from '../../../../components/app/clear-metametrics-data'; +import { + Box, + ButtonPrimary, + Icon, + IconName, + IconSize, + PolymorphicComponentPropWithRef, + PolymorphicRef, + Text, +} from '../../../../components/component-library'; +import { + Display, + FlexDirection, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getShowDataDeletionErrorModal, + getShowDeleteMetaMetricsDataModal, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DataDeletionErrorModal from '../../../../components/app/data-deletion-error-modal'; +import { formatDate } from '../../../../helpers/utils/util'; +import { DeleteRegulationStatus } from '../../../../../shared/constants/metametrics'; + +type DeleteMetaMetricsDataButtonProps = + PolymorphicComponentPropWithRef; + +type DeleteMetaMetricsDataButtonComponent = < + C extends React.ElementType = 'div', +>( + props: DeleteMetaMetricsDataButtonProps, +) => React.ReactElement | null; + +const DeleteMetaMetricsDataButton: DeleteMetaMetricsDataButtonComponent = + React.forwardRef( + ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { ...props }: DeleteMetaMetricsDataButtonProps, + ref: PolymorphicRef, + ) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const metaMetricsId = useSelector(getMetaMetricsId); + const metaMetricsDataDeletionStatus: DeleteRegulationStatus = useSelector( + getMetaMetricsDataDeletionStatus, + ); + const metaMetricsDataDeletionTimestamp = useSelector( + getMetaMetricsDataDeletionTimestamp, + ); + const formatedDate = formatDate( + metaMetricsDataDeletionTimestamp, + 'd/MM/y', + ); + + const showDeleteMetaMetricsDataModal = useSelector( + getShowDeleteMetaMetricsDataModal, + ); + const showDataDeletionErrorModal = useSelector( + getShowDataDeletionErrorModal, + ); + const latestMetricsEventTimestamp = useSelector( + getLatestMetricsEventTimestamp, + ); + + let dataDeletionButtonDisabled = Boolean(!metaMetricsId); + if (!dataDeletionButtonDisabled && metaMetricsDataDeletionStatus) { + dataDeletionButtonDisabled = + [ + DeleteRegulationStatus.Initialized, + DeleteRegulationStatus.Running, + DeleteRegulationStatus.Finished, + ].includes(metaMetricsDataDeletionStatus) && + metaMetricsDataDeletionTimestamp > latestMetricsEventTimestamp; + } + const privacyPolicyLink = ( + + {t('privacyMsg')} + + ); + return ( + <> + +
+ {t('deleteMetaMetricsData')} +
+ {dataDeletionButtonDisabled && Boolean(metaMetricsId) + ? t('deleteMetaMetricsDataRequestedDescription', [ + formatedDate, + privacyPolicyLink, + ]) + : t('deleteMetaMetricsDataDescription', [privacyPolicyLink])} +
+
+
+ {Boolean(!metaMetricsId) && ( + + + + {t('metaMetricsIdNotAvailableError')} + + + )} + { + dispatch(openDeleteMetaMetricsDataModal()); + }} + disabled={dataDeletionButtonDisabled} + > + {t('deleteMetaMetricsData')} + +
+
+ {showDeleteMetaMetricsDataModal && } + {showDataDeletionErrorModal && } + + ); + }, + ); + +export default DeleteMetaMetricsDataButton; diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts new file mode 100644 index 000000000000..945f4d349ede --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts @@ -0,0 +1 @@ +export { default } from './delete-metametrics-data-button'; diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index f6da9fe2367f..1fae729d3f31 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -52,8 +52,10 @@ import { } from '../../../helpers/utils/settings-search'; import IncomingTransactionToggle from '../../../components/app/incoming-trasaction-toggle/incoming-transaction-toggle'; -import ProfileSyncToggle from './profile-sync-toggle'; +import { updateDataDeletionTaskStatus } from '../../../store/actions'; import MetametricsToggle from './metametrics-toggle'; +import ProfileSyncToggle from './profile-sync-toggle'; +import DeleteMetametricsDataButton from './delete-metametrics-data-button'; export default class SecurityTab extends PureComponent { static contextTypes = { @@ -102,6 +104,7 @@ export default class SecurityTab extends PureComponent { useExternalServices: PropTypes.bool, toggleExternalServices: PropTypes.func.isRequired, setSecurityAlertsEnabled: PropTypes.func, + metaMetricsDataDeletionId: PropTypes.string, }; state = { @@ -138,9 +141,12 @@ export default class SecurityTab extends PureComponent { } } - componentDidMount() { + async componentDidMount() { const { t } = this.context; handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs); + if (this.props.metaMetricsDataDeletionId) { + await updateDataDeletionTaskStatus(); + } } toggleSetting(value, eventName, eventAction, toggleMethod) { @@ -961,7 +967,7 @@ export default class SecurityTab extends PureComponent { return ( {this.renderDataCollectionForMarketing()} +
); diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 747e3738fe3f..224072ef2b10 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -20,10 +20,12 @@ import { setUseExternalNameSources, setUseTransactionSimulations, setSecurityAlertsEnabled, + updateDataDeletionTaskStatus, } from '../../../store/actions'; import { getIsSecurityAlertsEnabled, getNetworkConfigurationsByChainId, + getMetaMetricsDataDeletionId, getPetnamesEnabled, } from '../../../selectors'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; @@ -78,6 +80,7 @@ const mapStateToProps = (state) => { petnamesEnabled, securityAlertsEnabled: getIsSecurityAlertsEnabled(state), useTransactionSimulations: metamask.useTransactionSimulations, + metaMetricsDataDeletionId: getMetaMetricsDataDeletionId(state), }; }; @@ -116,6 +119,9 @@ const mapDispatchToProps = (dispatch) => { setUseTransactionSimulations: (value) => { return dispatch(setUseTransactionSimulations(value)); }, + updateDataDeletionTaskStatus: () => { + return updateDataDeletionTaskStatus(); + }, setSecurityAlertsEnabled: (value) => setSecurityAlertsEnabled(value), }; }; diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 905fec684fd5..5e31cfb68c57 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -13,6 +13,8 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { getIsSecurityAlertsEnabled } from '../../../selectors'; import SecurityTab from './security-tab.container'; +const mockOpenDeleteMetaMetricsDataModal = jest.fn(); + const mockSetSecurityAlertsEnabled = jest .fn() .mockImplementation(() => () => undefined); @@ -36,6 +38,14 @@ jest.mock('../../../store/actions', () => ({ setSecurityAlertsEnabled: (val) => mockSetSecurityAlertsEnabled(val), })); +jest.mock('../../../ducks/app/app.ts', () => { + return { + openDeleteMetaMetricsDataModal: () => { + return mockOpenDeleteMetaMetricsDataModal; + }, + }; +}); + describe('Security Tab', () => { mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch @@ -214,7 +224,23 @@ describe('Security Tab', () => { await user.click(screen.getByText(tEn('addCustomNetwork'))); expect(global.platform.openExtensionInBrowser).toHaveBeenCalled(); }); + it('clicks "Delete MetaMetrics Data"', async () => { + mockState.metamask.participateInMetaMetrics = true; + mockState.metamask.metaMetricsId = 'fake-metametrics-id'; + const localMockStore = configureMockStore([thunk])(mockState); + renderWithProvider(, localMockStore); + + expect( + screen.queryByTestId(`delete-metametrics-data-button`), + ).toBeInTheDocument(); + + fireEvent.click( + screen.getByRole('button', { name: 'Delete MetaMetrics data' }), + ); + + expect(mockOpenDeleteMetaMetricsDataModal).toHaveBeenCalled(); + }); describe('Blockaid', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/ui/pages/settings/settings-search/settings-search.js b/ui/pages/settings/settings-search/settings-search.js index 8703d4a96a2c..b105e09da698 100644 --- a/ui/pages/settings/settings-search/settings-search.js +++ b/ui/pages/settings/settings-search/settings-search.js @@ -4,9 +4,12 @@ import Fuse from 'fuse.js'; import InputAdornment from '@material-ui/core/InputAdornment'; import TextField from '../../../components/ui/text-field'; import { I18nContext } from '../../../contexts/i18n'; -import SearchIcon from '../../../components/ui/icon/search-icon'; import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import { Icon, IconName } from '../../../components/component-library'; +import { + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; import { IconColor } from '../../../helpers/constants/design-system'; export default function SettingsSearch({ @@ -17,9 +20,8 @@ export default function SettingsSearch({ const t = useContext(I18nContext); const [searchQuery, setSearchQuery] = useState(''); - const [searchIconColor, setSearchIconColor] = useState( - 'var(--color-icon-muted)', - ); + + const [searchIconColor, setSearchIconColor] = useState(IconColor.iconMuted); const settingsRoutesListArray = Object.values(settingsRoutesList); const settingsSearchFuse = new Fuse(settingsRoutesListArray, { @@ -37,9 +39,9 @@ export default function SettingsSearch({ const sanitizedSearchQuery = _searchQuery.trimStart(); setSearchQuery(sanitizedSearchQuery); if (sanitizedSearchQuery === '') { - setSearchIconColor('var(--color-icon-muted)'); + setSearchIconColor(IconColor.iconMuted); } else { - setSearchIconColor('var(--color-icon-default)'); + setSearchIconColor(IconColor.iconDefault); } const fuseSearchResult = settingsSearchFuse.search(sanitizedSearchQuery); @@ -58,7 +60,11 @@ export default function SettingsSearch({ const renderStartAdornment = () => { return ( - + ); }; @@ -73,7 +79,11 @@ export default function SettingsSearch({ onClick={() => handleSearch('')} style={{ cursor: 'pointer' }} > - + )} @@ -93,6 +103,7 @@ export default function SettingsSearch({ autoComplete="off" startAdornment={renderStartAdornment()} endAdornment={renderEndAdornment()} + theme="bordered" /> ); } diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js index b998cde80515..191bbbc78685 100644 --- a/ui/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/pages/settings/settings-tab/settings-tab.component.js @@ -62,12 +62,10 @@ export default class SettingsTab extends PureComponent { currentLocale: PropTypes.string, useBlockie: PropTypes.bool, currentCurrency: PropTypes.string, - nativeCurrency: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, - setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, + showNativeTokenAsMainBalance: PropTypes.bool, + setShowNativeTokenAsMainBalancePreference: PropTypes.func, hideZeroBalanceTokens: PropTypes.bool, setHideZeroBalanceTokens: PropTypes.func, - lastFetchedConversionDate: PropTypes.number, selectedAddress: PropTypes.string, tokenList: PropTypes.object, theme: PropTypes.string, @@ -94,8 +92,7 @@ export default class SettingsTab extends PureComponent { renderCurrentConversion() { const { t } = this.context; - const { currentCurrency, setCurrentCurrency, lastFetchedConversionDate } = - this.props; + const { currentCurrency, setCurrentCurrency } = this.props; return (
- {t('currencyConversion')} - - {lastFetchedConversionDate - ? t('updatedWithDate', [ - new Date(lastFetchedConversionDate * 1000).toString(), - ]) - : t('noConversionDateAvailable')} - + + {t('currencyConversion')} +
@@ -131,6 +127,7 @@ export default class SettingsTab extends PureComponent { }, }); }} + className="settings-page__content-item__dropdown" />
@@ -141,10 +138,6 @@ export default class SettingsTab extends PureComponent { renderCurrentLocale() { const { t } = this.context; const { updateCurrentLocale, currentLocale } = this.props; - const currentLocaleMeta = locales.find( - (locale) => locale.code === currentLocale, - ); - const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''; return (
- + {t('currentLanguage')} - - - {currentLocaleName} - +
@@ -191,15 +185,20 @@ export default class SettingsTab extends PureComponent { id="toggle-zero-balance" >
- {t('hideZeroBalanceTokens')} + + {t('hideZeroBalanceTokens')} +
setHideZeroBalanceTokens(!value)} - offLabel={t('off')} - onLabel={t('on')} + data-testid="toggle-zero-balance-button" />
@@ -229,14 +228,19 @@ export default class SettingsTab extends PureComponent {
{t('accountIdenticon')} - + {t('jazzAndBlockies')} - +
-
-
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "deadline_missed" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "pending" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "reverted" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "unknown" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the component with initial props 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
-
-
- -
-
-`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap new file mode 100644 index 000000000000..f3ff42c89116 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmartTransactionStatusPage renders the "failed" STX status: smart-transaction-status-failed 1`] = ` +
+
+
+
+
+

+ Your transaction failed +

+
+

+ Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. +

+
+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "pending" STX status: smart-transaction-status-pending 1`] = ` +
+
+
+
+
+

+ Your transaction was submitted +

+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "success" STX status: smart-transaction-status-success 1`] = ` +
+
+
+
+
+

+ Your transaction is complete +

+
+ +
+
+
+ +
+
+`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss index 5e74ba9a8b3d..2227673029d8 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss +++ b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss @@ -1,10 +1,3 @@ - -@keyframes shift { - to { - background-position: 100% 0; - } -} - .smart-transaction-status-page { text-align: center; @@ -20,24 +13,6 @@ } } - &__loading-bar-container { - @media screen and (min-width: 768px) { - max-width: 260px; - } - - width: 100%; - height: 3px; - background: var(--color-background-alternative); - display: flex; - margin-top: 16px; - } - - &__loading-bar { - height: 3px; - background: var(--color-primary-default); - transition: width 0.5s linear; - } - &__footer { grid-area: footer; } @@ -45,35 +20,4 @@ &__countdown { width: 25px; } - - // Slightly overwrite the default SimulationDetails layout to look better on the Smart Transaction status page. - .simulation-details-layout { - margin-left: 0; - margin-right: 0; - width: 100%; - text-align: left; - } - - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - - &--top { - width: 1634px; - height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; - } - - &--bottom { - width: 1600px; - height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; - } - } } diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx new file mode 100644 index 000000000000..fa4166af1461 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +// Declare a variable to store the onComplete callback +let mockOnComplete: () => void; + +// Modify the existing jest.mock to capture the onComplete callback +jest.mock('../../../components/component-library/lottie-animation', () => ({ + LottieAnimation: ({ + path, + loop, + autoplay, + onComplete, + }: { + path: string; + loop: boolean; + autoplay: boolean; + onComplete: () => void; + }) => { + // Store the onComplete callback for later use in tests + mockOnComplete = onComplete; + return ( +
+ ); + }, +})); + +describe('SmartTransactionsStatusAnimation', () => { + it('renders correctly for PENDING status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for SUCCESS status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('confirmed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for REVERTED status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for UNKNOWN status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for other statuses', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('processing'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); + + it('transitions from submittingIntro to submittingLoop when onComplete is called', () => { + render( + , + ); + const lottieAnimation = screen.getByTestId('mock-lottie-animation'); + + // Initially, should render 'submitting-intro' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + + // Trigger the onComplete callback to simulate animation completion + expect(lottieAnimation.getAttribute('data-on-complete')).toBeDefined(); + act(() => { + mockOnComplete(); + }); + + // After onComplete is called, it should transition to 'submitting-loop' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-loop'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); +}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx new file mode 100644 index 000000000000..3dc739aefa1f --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx @@ -0,0 +1,80 @@ +import React, { useState, useCallback } from 'react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { Box } from '../../../components/component-library'; +import { Display } from '../../../helpers/constants/design-system'; +import { LottieAnimation } from '../../../components/component-library/lottie-animation'; + +const ANIMATIONS_FOLDER = 'images/animations/smart-transaction-status'; + +type AnimationInfo = { + path: string; + loop: boolean; +}; + +const Animations: Record = { + Failed: { + path: `${ANIMATIONS_FOLDER}/failed.lottie.json`, + loop: false, + }, + Confirmed: { + path: `${ANIMATIONS_FOLDER}/confirmed.lottie.json`, + loop: false, + }, + SubmittingIntro: { + path: `${ANIMATIONS_FOLDER}/submitting-intro.lottie.json`, + loop: false, + }, + SubmittingLoop: { + path: `${ANIMATIONS_FOLDER}/submitting-loop.lottie.json`, + loop: true, + }, + Processing: { + path: `${ANIMATIONS_FOLDER}/processing.lottie.json`, + loop: true, + }, +}; + +export const SmartTransactionStatusAnimation = ({ + status, +}: { + status: SmartTransactionStatuses; +}) => { + const [isIntro, setIsIntro] = useState(true); + + let animation: AnimationInfo; + + if (status === SmartTransactionStatuses.PENDING) { + animation = isIntro + ? Animations.SubmittingIntro + : Animations.SubmittingLoop; + } else { + switch (status) { + case SmartTransactionStatuses.SUCCESS: + animation = Animations.Confirmed; + break; + case SmartTransactionStatuses.REVERTED: + case SmartTransactionStatuses.UNKNOWN: + animation = Animations.Failed; + break; + default: + animation = Animations.Processing; + } + } + + const handleAnimationComplete = useCallback(() => { + if (status === SmartTransactionStatuses.PENDING && isIntro) { + setIsIntro(false); + } + }, [status, isIntro]); + + return ( + + + + ); +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx new file mode 100644 index 000000000000..12d356ce4cc4 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import SmartTransactionStatusPage from './smart-transaction-status-page'; +import { Meta, StoryObj } from '@storybook/react'; +import { SimulationData } from '@metamask/transaction-controller'; +import { mockNetworkState } from '../../../../test/stub/networks'; + +// Mock data +const CHAIN_ID_MOCK = '0x1'; + +const simulationData: SimulationData = { + nativeBalanceChange: { + previousBalance: '0x0', + newBalance: '0x0', + difference: '0x12345678912345678', + isDecrease: true, + }, + tokenBalanceChanges: [], +}; + +const TX_MOCK = { + id: 'txId', + simulationData, + chainId: CHAIN_ID_MOCK, +}; + +const storeMock = configureStore({ + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: false, + }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + transactions: [TX_MOCK], + currentNetworkTxList: [TX_MOCK], + }, +}); + +const meta: Meta = { + title: 'Pages/SmartTransactions/SmartTransactionStatusPage', + component: SmartTransactionStatusPage, + decorators: [(story) => {story()}], +}; + +export default meta; +type Story = StoryObj; + +export const Pending: Story = { + args: { + requestState: { + smartTransaction: { + status: 'pending', + creationTime: Date.now(), + uuid: 'uuid', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Success: Story = { + args: { + requestState: { + smartTransaction: { + status: 'success', + creationTime: Date.now() - 60000, // 1 minute ago + uuid: 'uuid-success', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-success', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Failed: Story = { + args: { + requestState: { + smartTransaction: { + status: 'unknown', + creationTime: Date.now() - 180000, // 3 minutes ago + uuid: 'uuid-failed', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-failed', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx index 2eb29bfa4e4e..4492ed4e4844 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { SmartTransactionStatuses, @@ -8,9 +8,7 @@ import { import { Box, Text, - Icon, IconName, - IconSize, Button, ButtonVariant, ButtonSecondary, @@ -26,22 +24,18 @@ import { TextColor, FontWeight, IconColor, - TextAlign, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getFullTxData } from '../../../selectors'; -import { getFeatureFlagsByChainId } from '../../../../shared/modules/selectors'; import { BaseUrl } from '../../../../shared/constants/urls'; -import { - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE, - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE, -} from '../../../../shared/constants/smartTransactions'; import { hideLoadingIndication } from '../../../store/actions'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { SimulationDetails } from '../../confirmations/components/simulation-details'; import { NOTIFICATION_WIDTH } from '../../../../shared/constants/notifications'; -type RequestState = { +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +export type RequestState = { smartTransaction?: SmartTransaction; isDapp: boolean; txId?: string; @@ -49,8 +43,8 @@ type RequestState = { export type SmartTransactionStatusPageProps = { requestState: RequestState; - onCloseExtension: () => void; - onViewActivity: () => void; + onCloseExtension?: () => void; + onViewActivity?: () => void; }; export const showRemainingTimeInMinAndSec = ( @@ -66,30 +60,18 @@ export const showRemainingTimeInMinAndSec = ( const getDisplayValues = ({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }: { t: ReturnType; - countdown: JSX.Element | undefined; isSmartTransactionPending: boolean; - isSmartTransactionTakingTooLong: boolean; isSmartTransactionSuccess: boolean; isSmartTransactionCancelled: boolean; }) => { - if (isSmartTransactionPending && isSmartTransactionTakingTooLong) { - return { - title: t('smartTransactionTakingTooLong'), - description: t('smartTransactionTakingTooLongDescription', [countdown]), - iconName: IconName.Clock, - iconColor: IconColor.primaryDefault, - }; - } else if (isSmartTransactionPending) { + if (isSmartTransactionPending) { return { title: t('smartTransactionPending'), - description: t('stxEstimatedCompletion', [countdown]), iconName: IconName.Clock, iconColor: IconColor.primaryDefault, }; @@ -102,7 +84,7 @@ const getDisplayValues = ({ } else if (isSmartTransactionCancelled) { return { title: t('smartTransactionCancelled'), - description: t('smartTransactionCancelledDescription', [countdown]), + description: t('smartTransactionCancelledDescription'), iconName: IconName.Danger, iconColor: IconColor.errorDefault, }; @@ -116,98 +98,6 @@ const getDisplayValues = ({ }; }; -const useRemainingTime = ({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, -}: { - isSmartTransactionPending: boolean; - smartTransaction?: SmartTransaction; - stxMaxDeadline: number; - stxEstimatedDeadline: number; -}) => { - const [timeLeftForPendingStxInSec, setTimeLeftForPendingStxInSec] = - useState(0); - const [isSmartTransactionTakingTooLong, setIsSmartTransactionTakingTooLong] = - useState(false); - const stxDeadline = isSmartTransactionTakingTooLong - ? stxMaxDeadline - : stxEstimatedDeadline; - - useEffect(() => { - if (!isSmartTransactionPending) { - return; - } - - const calculateRemainingTime = () => { - const secondsAfterStxSubmission = smartTransaction?.creationTime - ? Math.round((Date.now() - smartTransaction.creationTime) / 1000) - : 0; - - if (secondsAfterStxSubmission > stxDeadline) { - setTimeLeftForPendingStxInSec(0); - if (!isSmartTransactionTakingTooLong) { - setIsSmartTransactionTakingTooLong(true); - } - return; - } - - setTimeLeftForPendingStxInSec(stxDeadline - secondsAfterStxSubmission); - }; - - const intervalId = setInterval(calculateRemainingTime, 1000); - calculateRemainingTime(); - - // eslint-disable-next-line consistent-return - return () => clearInterval(intervalId); - }, [ - isSmartTransactionPending, - isSmartTransactionTakingTooLong, - smartTransaction?.creationTime, - stxDeadline, - ]); - - return { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - }; -}; - -const Deadline = ({ - isSmartTransactionPending, - stxDeadline, - timeLeftForPendingStxInSec, -}: { - isSmartTransactionPending: boolean; - stxDeadline: number; - timeLeftForPendingStxInSec: number; -}) => { - if (!isSmartTransactionPending) { - return null; - } - return ( - -
-
-
- - ); -}; - const Description = ({ description }: { description: string | undefined }) => { if (!description) { return null; @@ -388,29 +278,10 @@ const Title = ({ title }: { title: string }) => { ); }; -const SmartTransactionsStatusIcon = ({ - iconName, - iconColor, -}: { - iconName: IconName; - iconColor: IconColor; -}) => { - return ( - - - - ); -}; - export const SmartTransactionStatusPage = ({ requestState, - onCloseExtension, - onViewActivity, + onCloseExtension = () => null, + onViewActivity = () => null, }: SmartTransactionStatusPageProps) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -423,50 +294,15 @@ export const SmartTransactionStatusPage = ({ const isSmartTransactionCancelled = Boolean( smartTransaction?.status?.startsWith(SmartTransactionStatuses.CANCELLED), ); - const featureFlags: { - smartTransactions?: { - expectedDeadline?: number; - maxDeadline?: number; - }; - } | null = useSelector(getFeatureFlagsByChainId); - const stxEstimatedDeadline = - featureFlags?.smartTransactions?.expectedDeadline || - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE; - const stxMaxDeadline = - featureFlags?.smartTransactions?.maxDeadline || - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE; - const { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - } = useRemainingTime({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, - }); + const chainId: string = useSelector(getCurrentChainId); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: This same selector is used in the awaiting-swap component. const fullTxData = useSelector((state) => getFullTxData(state, txId)) || {}; - const countdown = isSmartTransactionPending ? ( - - {showRemainingTimeInMinAndSec(timeLeftForPendingStxInSec)} - - ) : undefined; - - const { title, description, iconName, iconColor } = getDisplayValues({ + const { title, description } = getDisplayValues({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }); @@ -515,20 +351,10 @@ export const SmartTransactionStatusPage = ({ paddingRight={6} width={BlockSize.Full} > - - - <Deadline - isSmartTransactionPending={isSmartTransactionPending} - stxDeadline={stxDeadline} - timeLeftForPendingStxInSec={timeLeftForPendingStxInSec} - /> <Description description={description} /> <PortfolioSmartTransactionStatusUrl portfolioSmartTransactionStatusUrl={ @@ -539,15 +365,13 @@ export const SmartTransactionStatusPage = ({ /> </Box> {canShowSimulationDetails && ( - <SimulationDetails - simulationData={fullTxData.simulationData} - transactionId={fullTxData.id} - /> + <Box width={BlockSize.Full}> + <SimulationDetails + simulationData={fullTxData.simulationData} + transactionId={fullTxData.id} + /> + </Box> )} - <Box - marginTop={3} - className="smart-transaction-status-page__background-animation smart-transaction-status-page__background-animation--bottom" - /> </Box> <SmartTransactionsStatusPageFooter isDapp={isDapp} diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js deleted file mode 100644 index d014c56373a4..000000000000 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../test/jest'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { SmartTransactionStatusPage } from '.'; - -const middleware = [thunk]; - -describe('SmartTransactionStatusPage', () => { - const requestState = { - smartTransaction: { - status: SmartTransactionStatuses.PENDING, - creationTime: Date.now(), - }, - }; - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Submitting your transaction')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "Sorry for the wait" pending status', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const newRequestState = { - ...requestState, - smartTransaction: { - ...requestState.smartTransaction, - creationTime: 1519211809934, - }, - }; - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={newRequestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('Sorry for the wait')).toBeInTheDocument(); - expect(queryByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction is complete')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "reverted" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.REVERTED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect( - getByText( - 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', - ), - ).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - requestState.smartTransaction = latestSmartTransaction; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect( - getByText( - `Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees.`, - ), - ).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "deadline_missed" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = - SmartTransactionStatuses.CANCELLED_DEADLINE_MISSED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "unknown" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.UNKNOWN; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "pending" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.PENDING; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(queryByText('View activity')).not.toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx new file mode 100644 index 000000000000..afd9b2872ce1 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { + SmartTransaction, + SmartTransactionStatuses, +} from '@metamask/smart-transactions-controller/dist/types'; + +import { fireEvent } from '@testing-library/react'; +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../test/jest'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { + SmartTransactionStatusPage, + RequestState, +} from './smart-transaction-status-page'; + +// Mock the SmartTransactionStatusAnimation component and capture props +jest.mock('./smart-transaction-status-animation', () => ({ + SmartTransactionStatusAnimation: ({ + status, + }: { + status: SmartTransactionStatuses; + }) => <div data-testid="mock-animation" data-status={status} />, +})); + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); + +const defaultRequestState: RequestState = { + smartTransaction: { + status: SmartTransactionStatuses.PENDING, + creationTime: Date.now(), + uuid: 'uuid', + chainId: CHAIN_IDS.MAINNET, + }, + isDapp: false, + txId: 'txId', +}; + +describe('SmartTransactionStatusPage', () => { + const statusTestCases = [ + { + status: SmartTransactionStatuses.PENDING, + isDapp: false, + expectedTexts: ['Your transaction was submitted', 'View activity'], + snapshotName: 'pending', + }, + { + status: SmartTransactionStatuses.SUCCESS, + isDapp: false, + expectedTexts: [ + 'Your transaction is complete', + 'View transaction', + 'View activity', + ], + snapshotName: 'success', + }, + { + status: SmartTransactionStatuses.REVERTED, + isDapp: false, + expectedTexts: [ + 'Your transaction failed', + 'View transaction', + 'View activity', + 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', + ], + snapshotName: 'failed', + }, + ]; + + statusTestCases.forEach(({ status, isDapp, expectedTexts, snapshotName }) => { + it(`renders the "${snapshotName}" STX status${ + isDapp ? ' for a dapp transaction' : '' + }`, () => { + const state = createSwapsMockStore(); + const latestSmartTransaction = + state.metamask.smartTransactionsState.smartTransactions[ + CHAIN_IDS.MAINNET + ][1]; + latestSmartTransaction.status = status; + const requestState: RequestState = { + smartTransaction: latestSmartTransaction as SmartTransaction, + isDapp, + txId: 'txId', + }; + + const { getByText, getByTestId, container } = renderWithProvider( + <SmartTransactionStatusPage requestState={requestState} />, + mockStore(state), + ); + + expectedTexts.forEach((text) => { + expect(getByText(text)).toBeInTheDocument(); + }); + + expect(getByTestId('mock-animation')).toBeInTheDocument(); + expect(getByTestId('mock-animation')).toHaveAttribute( + 'data-status', + status, + ); + expect(container).toMatchSnapshot( + `smart-transaction-status-${snapshotName}`, + ); + }); + }); + + describe('Action Buttons', () => { + it('calls onCloseExtension when Close extension button is clicked', () => { + const onCloseExtension = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: true }} + onCloseExtension={onCloseExtension} + />, + store, + ); + + const closeButton = getByText('Close extension'); + fireEvent.click(closeButton); + expect(onCloseExtension).toHaveBeenCalled(); + }); + + it('calls onViewActivity when View activity button is clicked', () => { + const onViewActivity = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: false }} + onViewActivity={onViewActivity} + />, + store, + ); + + const viewActivityButton = getByText('View activity'); + fireEvent.click(viewActivityButton); + expect(onViewActivity).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap index a971dd30faba..29fefa61b305 100644 --- a/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap +++ b/ui/pages/snap-account-redirect/__snapshots__/create-snap-redirect.test.tsx.snap @@ -15,6 +15,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i > <div class="mm-box mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center" + style="overflow: hidden;" > <div class="mm-box mm-text mm-avatar-base mm-avatar-base--size-sm mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-alternative mm-box--background-color-background-alternative mm-box--rounded-full" @@ -24,6 +25,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i </div> <p class="mm-box mm-text mm-text--body-md-medium mm-text--ellipsis mm-box--margin-left-2 mm-box--color-text-default" + title="@metamask/snap-simple-keyring" > @metamask/snap-simple-keyring </p> @@ -107,7 +109,7 @@ exports[`<SnapAccountRedirect /> renders the url and message when provided and i class="mm-box mm-text mm-text--body-md mm-box--padding-2 mm-box--color-primary-default" data-testid="snap-account-redirect-url-display-box" > - https://metamask.github.io/snap-simple-keyring/1.1.2/ + https://metamask.github.io/snap-simple-keyring/1.1.6/ </p> <button aria-label="" diff --git a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap index 01a3a89ad4c5..d58907ed2684 100644 --- a/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap +++ b/ui/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap @@ -5,7 +5,7 @@ exports[`DropdownInputPair renders the component with initial props 1`] = ` class="MuiFormControl-root MuiTextField-root dropdown-input-pair__input MuiFormControl-marginDense MuiFormControl-fullWidth" > <div - class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-marginDense MuiInput-marginDense" + class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-marginDense MuiInput-marginDense" > <input aria-invalid="false" diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 98bb6933d0c3..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( @@ -782,10 +784,17 @@ export default function PrepareSwapPage({ ); } + const isNonDefaultToken = !isSwapsDefaultTokenSymbol( + fromTokenSymbol, + chainId, + ); + const hasPositiveFromTokenBalance = rawFromTokenBalance > 0; + const isTokenEligibleForMaxBalance = + isSmartTransaction || (!isSmartTransaction && isNonDefaultToken); const showMaxBalanceLink = fromTokenSymbol && - !isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && - rawFromTokenBalance > 0; + isTokenEligibleForMaxBalance && + hasPositiveFromTokenBalance; return ( <div className="prepare-swap-page"> diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 496ae5ee6d9e..9921161c4da4 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -19,23 +19,19 @@ import SelectQuotePopover from '../select-quote-popover'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; import { usePrevious } from '../../../hooks/usePrevious'; -import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getQuotes, - getSelectedQuote, getApproveTxParams, getFetchParams, setBalanceError, getQuotesLastFetched, getBalanceError, getCustomSwapsGas, // Gas limit. - getCustomMaxFeePerGas, - getCustomMaxPriorityFeePerGas, - getSwapsUserFeeLevel, getDestinationTokenInfo, getUsedSwapsGasPrice, getTopQuote, + getUsedQuote, signAndSendTransactions, getBackgroundSwapRouteState, swapsQuoteSelected, @@ -79,11 +75,10 @@ import { PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import { - addHexes, - decGWEIToHexWEI, decimalToHex, decWEIToDecETH, sumHexes, + hexToDecimal, } from '../../../../shared/modules/conversion.utils'; import { getCustomTxParamsData } from '../../confirmations/confirm-approve/confirm-approve.util'; import { @@ -91,6 +86,7 @@ import { getRenderableNetworkFeesForQuote, getFeeForSmartTransaction, formatSwapsValueForDisplay, + getSwap1559GasFeeEstimates, } from '../swaps.util'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { @@ -113,6 +109,7 @@ import { Size, FlexDirection, Severity, + FontStyle, } from '../../../helpers/constants/design-system'; import { BannerAlert, @@ -143,11 +140,43 @@ import { import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; import useRamps from '../../../hooks/ramps/useRamps/useRamps'; +import { getTokenFiatAmount } from '../../../helpers/utils/token-util'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { useAsyncResult } from '../../../hooks/useAsyncResult'; +import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; let intervalId; +const ViewAllQuotesLink = React.memo(function ViewAllQuotesLink({ + trackAllAvailableQuotesOpened, + setSelectQuotePopoverShown, + t, +}) { + const handleClick = useCallback(() => { + trackAllAvailableQuotesOpened(); + setSelectQuotePopoverShown(true); + }, [trackAllAvailableQuotesOpened, setSelectQuotePopoverShown]); + + return ( + <ButtonLink + key="view-all-quotes" + data-testid="review-quote-view-all-quotes" + onClick={handleClick} + size={Size.inherit} + > + {t('viewAllQuotes')} + </ButtonLink> + ); +}); + +ViewAllQuotesLink.propTypes = { + trackAllAvailableQuotesOpened: PropTypes.func.isRequired, + setSelectQuotePopoverShown: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, +}; + export default function ReviewQuote({ setReceiveToAmount }) { const history = useHistory(); const dispatch = useDispatch(); @@ -190,9 +219,6 @@ export default function ReviewQuote({ setReceiveToAmount }) { // Select necessary data const gasPrice = useSelector(getUsedSwapsGasPrice); const customMaxGas = useSelector(getCustomSwapsGas); - const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas); - const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas); - const swapsUserFeeLevel = useSelector(getSwapsUserFeeLevel); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates); const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); @@ -205,10 +231,9 @@ export default function ReviewQuote({ setReceiveToAmount }) { ); const balanceError = useSelector(getBalanceError); const fetchParams = useSelector(getFetchParams, isEqual); - const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const selectedQuote = useSelector(getSelectedQuote, isEqual); + const approveTxParams = useSelector(getApproveTxParams, isEqual); const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const tradeValue = usedQuote?.trade?.value ?? '0x0'; const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const chainId = useSelector(getCurrentChainId); @@ -228,7 +253,32 @@ export default function ReviewQuote({ setReceiveToAmount }) { ); const smartTransactionFees = useSelector(getSmartTransactionFees, isEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); + const { estimatedBaseFee = '0' } = useGasFeeEstimates(); + + const gasFeeEstimates = useAsyncResult(async () => { + if (!networkAndAccountSupports1559) { + return undefined; + } + + return await getSwap1559GasFeeEstimates( + usedQuote.trade, + approveTxParams, + estimatedBaseFee, + chainId, + ); + }, [ + usedQuote.trade, + approveTxParams, + estimatedBaseFee, + chainId, + networkAndAccountSupports1559, + ]); + + const gasFeeEstimatesTrade = gasFeeEstimates.value?.tradeGasFeeEstimates; + const gasFeeEstimatesApprove = gasFeeEstimates.value?.approveGasFeeEstimates; + const unsignedTransaction = usedQuote.trade; + const { isGasIncludedTrade } = usedQuote; const isSmartTransaction = currentSmartTransactionsEnabled && smartTransactionsOptInStatus; @@ -242,15 +292,6 @@ export default function ReviewQuote({ setReceiveToAmount }) { return ''; }); - let gasFeeInputs; - if (networkAndAccountSupports1559) { - // For Swaps we want to get 'high' estimations by default. - // eslint-disable-next-line react-hooks/rules-of-hooks - gasFeeInputs = useGasFeeInputs(GasRecommendations.high, { - userFeeLevel: swapsUserFeeLevel || GasRecommendations.high, - }); - } - const fetchParamsSourceToken = fetchParams?.sourceToken; const additionalTrackingParams = { @@ -275,27 +316,11 @@ export default function ReviewQuote({ setReceiveToAmount }) { customMaxGas, ); - let maxFeePerGas; - let maxPriorityFeePerGas; - let baseAndPriorityFeePerGas; - - // EIP-1559 gas fees. - if (networkAndAccountSupports1559) { - const { - maxFeePerGas: suggestedMaxFeePerGas, - maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas, - gasFeeEstimates: { estimatedBaseFee = '0' } = {}, - } = gasFeeInputs; - maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas); - maxPriorityFeePerGas = - customMaxPriorityFeePerGas || - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas); - baseAndPriorityFeePerGas = addHexes( - decGWEIToHexWEI(estimatedBaseFee), - maxPriorityFeePerGas, - ); - } - let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); + let gasTotalInWeiHex = calcGasTotal( + maxGasLimit, + gasFeeEstimatesTrade?.maxFeePerGas || gasPrice, + ); + if (multiLayerL1FeeTotal !== null) { gasTotalInWeiHex = sumHexes( gasTotalInWeiHex || '0x0', @@ -332,12 +357,19 @@ export default function ReviewQuote({ setReceiveToAmount }) { calcTokenAmount(approveValue, selectedFromToken.decimals).toFixed(9); const approveGas = approveTxParams?.gas; + const gasPriceTrade = networkAndAccountSupports1559 + ? gasFeeEstimatesTrade?.baseAndPriorityFeePerGas + : gasPrice; + + const gasPriceApprove = networkAndAccountSupports1559 + ? gasFeeEstimatesApprove?.baseAndPriorityFeePerGas + : gasPrice; + const renderablePopoverData = useMemo(() => { return quotesToRenderableData({ quotes, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -352,9 +384,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { }); }, [ quotes, - gasPrice, - baseAndPriorityFeePerGas, - networkAndAccountSupports1559, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -385,9 +416,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { getRenderableNetworkFeesForQuote({ tradeGas: usedGasLimit, approveGas, - gasPrice: networkAndAccountSupports1559 - ? baseAndPriorityFeePerGas - : gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -404,7 +434,8 @@ export default function ReviewQuote({ setReceiveToAmount }) { const renderableMaxFees = getRenderableNetworkFeesForQuote({ tradeGas: maxGasLimit, approveGas, - gasPrice: maxFeePerGas || gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -850,7 +881,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { tokenBalanceUnavailable || disableSubmissionDueToPriceWarning || (networkAndAccountSupports1559 && - baseAndPriorityFeePerGas === undefined) || + gasFeeEstimatesTrade?.baseAndPriorityFeePerGas === undefined) || (!networkAndAccountSupports1559 && (gasPrice === null || gasPrice === undefined)) || (currentSmartTransactionsEnabled && @@ -880,7 +911,9 @@ export default function ReviewQuote({ setReceiveToAmount }) { ]); useEffect(() => { - if (isSmartTransaction && !insufficientTokens) { + // If it's a smart transaction, has sufficient tokens, and gas is not included in the trade, + // set up gas fee polling. + if (isSmartTransaction && !insufficientTokens && !isGasIncludedTrade) { const unsignedTx = { from: unsignedTransaction.from, to: unsignedTransaction.to, @@ -923,6 +956,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { chainId, swapsNetworkConfig.stxGetTransactionsRefreshTime, insufficientTokens, + isGasIncludedTrade, ]); useEffect(() => { @@ -1045,6 +1079,40 @@ export default function ReviewQuote({ setReceiveToAmount }) { } }; + const gasTokenFiatAmount = useMemo(() => { + if (!isGasIncludedTrade) { + return undefined; + } + const tradeTxTokenFee = + smartTransactionFees?.tradeTxFees?.fees?.[0]?.tokenFees?.[0]; + if (!tradeTxTokenFee) { + return undefined; + } + const { token: { address, decimals, symbol } = {}, balanceNeededToken } = + tradeTxTokenFee; + const checksumAddress = toChecksumHexAddress(address); + const contractExchangeRate = memoizedTokenConversionRates[checksumAddress]; + const gasTokenAmountDec = calcTokenAmount( + hexToDecimal(balanceNeededToken), + decimals, + ).toString(10); + return getTokenFiatAmount( + contractExchangeRate, + conversionRate, + currentCurrency, + gasTokenAmountDec, + symbol, + true, + true, + ); + }, [ + isGasIncludedTrade, + smartTransactionFees, + memoizedTokenConversionRates, + conversionRate, + currentCurrency, + ]); + return ( <div className="review-quote"> <div className="review-quote__content"> @@ -1122,9 +1190,9 @@ export default function ReviewQuote({ setReceiveToAmount }) { <Text variant={TextVariant.bodyMd} marginRight={1} - color={TextColor.textAlternative} + color={TextColor.textDefault} > - {t('quoteRate')} + {t('quoteRate')}* </Text> <ExchangeRateDisplay primaryTokenValue={calcTokenValue( @@ -1141,78 +1209,156 @@ export default function ReviewQuote({ setReceiveToAmount }) { showIconForSwappingTokens={false} /> </Box> - <Box - display={DISPLAY.FLEX} - justifyContent={JustifyContent.spaceBetween} - alignItems={AlignItems.stretch} - > + {isGasIncludedTrade && ( <Box display={DISPLAY.FLEX} - alignItems={AlignItems.center} - width={FRACTIONS.SIX_TWELFTHS} + justifyContent={JustifyContent.spaceBetween} + alignItems={AlignItems.stretch} > - <Text - variant={TextVariant.bodyMd} - as="h6" - color={TextColor.textAlternative} - marginRight={1} + <Box + display={DISPLAY.FLEX} + alignItems={AlignItems.center} + width={FRACTIONS.SIX_TWELFTHS} > - {t('transactionDetailGasHeading')} - </Text> - <InfoTooltip - position="left" - contentText={ - <p className="fee-card__info-tooltip-paragraph"> - {t('swapGasFeesExplanation', [ + <Text + variant={TextVariant.bodyMd} + as="h6" + color={TextColor.textDefault} + marginRight={1} + > + {t('gasFee')} + </Text> + <InfoTooltip + position="left" + contentText={ + <> + <p className="fee-card__info-tooltip-paragraph"> + {t('swapGasIncludedTooltipExplanation')} + </p> <ButtonLink - key="learn-more-gas-link" + key="learn-more-about-gas-included-link" size={ButtonLinkSize.Inherit} - href={ZENDESK_URLS.GAS_FEES} + href={ZENDESK_URLS.SWAPS_GAS_FEES} target="_blank" rel="noopener noreferrer" externalLink onClick={() => { trackEvent({ - event: 'Clicked "Gas Fees: Learn More" Link', + event: + 'Clicked "GasIncluded tooltip: Learn More" Link', category: MetaMetricsEventCategory.Swaps, }); }} > - {t('swapGasFeesExplanationLinkText')} - </ButtonLink>, - ])} - </p> - } - /> + {t('swapGasIncludedTooltipExplanationLinkText')} + </ButtonLink> + </> + } + /> + </Box> + <Box + display={DISPLAY.FLEX} + justifyContent={JustifyContent.flexEnd} + alignItems={AlignItems.flexEnd} + width={FRACTIONS.SIX_TWELFTHS} + > + <Text + variant={TextVariant.bodyMd} + as="h6" + color={TextColor.textDefault} + data-testid="review-quote-gas-fee-in-fiat" + textAlign={TEXT_ALIGN.RIGHT} + style={{ textDecoration: 'line-through' }} + marginRight={1} + > + {gasTokenFiatAmount} + </Text> + <Text + variant={TextVariant.bodySm} + as="h6" + color={TextColor.textDefault} + textAlign={TEXT_ALIGN.RIGHT} + fontStyle={FontStyle.Italic} + > + {t('included')} + </Text> + </Box> </Box> + )} + {!isGasIncludedTrade && ( <Box display={DISPLAY.FLEX} - alignItems={AlignItems.flexEnd} - width={FRACTIONS.SIX_TWELFTHS} + justifyContent={JustifyContent.spaceBetween} + alignItems={AlignItems.stretch} > - <Text - variant={TextVariant.bodyMd} - as="h6" - color={TextColor.textAlternative} - width={FRACTIONS.EIGHT_TWELFTHS} - textAlign={TEXT_ALIGN.RIGHT} - paddingRight={1} + <Box + display={DISPLAY.FLEX} + alignItems={AlignItems.center} + width={FRACTIONS.SIX_TWELFTHS} > - {feeInEth} - </Text> - <Text - variant={TextVariant.bodyMdBold} - as="h6" - color={TextColor.textAlternative} - data-testid="review-quote-gas-fee-in-fiat" - width={FRACTIONS.FOUR_TWELFTHS} - textAlign={TEXT_ALIGN.RIGHT} + <Text + variant={TextVariant.bodyMd} + as="h6" + color={TextColor.textDefault} + marginRight={1} + > + {t('transactionDetailGasHeading')} + </Text> + <InfoTooltip + position="left" + contentText={ + <p className="fee-card__info-tooltip-paragraph"> + {t('swapGasFeesExplanation', [ + <ButtonLink + key="learn-more-gas-link" + size={ButtonLinkSize.Inherit} + href={ZENDESK_URLS.GAS_FEES} + target="_blank" + rel="noopener noreferrer" + externalLink + onClick={() => { + trackEvent({ + event: 'Clicked "Gas Fees: Learn More" Link', + category: MetaMetricsEventCategory.Swaps, + }); + }} + > + {t('swapGasFeesExplanationLinkText')} + </ButtonLink>, + ])} + </p> + } + /> + </Box> + <Box + display={DISPLAY.FLEX} + alignItems={AlignItems.flexEnd} + width={FRACTIONS.SIX_TWELFTHS} > - {` ${feeInFiat}`} - </Text> + <Text + variant={TextVariant.bodyMd} + as="h6" + color={TextColor.textDefault} + width={FRACTIONS.EIGHT_TWELFTHS} + textAlign={TEXT_ALIGN.RIGHT} + paddingRight={1} + > + {feeInEth} + </Text> + <Text + variant={TextVariant.bodyMdBold} + as="h6" + color={TextColor.textDefault} + data-testid="review-quote-gas-fee-in-fiat" + width={FRACTIONS.FOUR_TWELFTHS} + textAlign={TEXT_ALIGN.RIGHT} + > + {` ${feeInFiat}`} + </Text> + </Box> </Box> - </Box> - {(maxFeeInFiat || maxFeeInEth) && ( + )} + {!isGasIncludedTrade && (maxFeeInFiat || maxFeeInEth) && ( <Box display={DISPLAY.FLEX}> <Box display={DISPLAY.FLEX} width={FRACTIONS.SIX_TWELFTHS}></Box> <Box @@ -1222,7 +1368,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { > <Text variant={TextVariant.bodySm} - color={TextColor.textAlternative} + color={TextColor.textDefault} width={FRACTIONS.EIGHT_TWELFTHS} paddingRight={1} textAlign={TEXT_ALIGN.RIGHT} @@ -1231,7 +1377,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { </Text> <Text variant={TextVariant.bodySm} - color={TextColor.textAlternative} + color={TextColor.textDefault} width={FRACTIONS.FOUR_TWELFTHS} textAlign={TEXT_ALIGN.RIGHT} > @@ -1248,7 +1394,7 @@ export default function ReviewQuote({ setReceiveToAmount }) { <Text variant={TextVariant.bodyMd} as="h6" - color={TextColor.textAlternative} + color={TextColor.textDefault} marginRight={1} > {t('swapEnableTokenForSwapping', [tokenApprovalTextComponent])} @@ -1264,32 +1410,55 @@ export default function ReviewQuote({ setReceiveToAmount }) { </Text> </Box> )} - <Box - display={DISPLAY.FLEX} - marginTop={3} - justifyContent={JustifyContent.center} - alignItems={AlignItems.center} - > - <Text variant={TextVariant.bodySm} color={TextColor.textDefault}> - {t('swapIncludesMetaMaskFeeViewAllQuotes', [ - metaMaskFee, - <ButtonLink - key="view-all-quotes" - data-testid="review-quote-view-all-quotes" - onClick={ - /* istanbul ignore next */ - () => { - trackAllAvailableQuotesOpened(); - setSelectQuotePopoverShown(true); + {isGasIncludedTrade && ( + <Box + display={DISPLAY.FLEX} + marginTop={3} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + flexDirection={FlexDirection.Column} + > + <Text + variant={TextVariant.bodySm} + color={TextColor.textAlternative} + > + * {t('swapIncludesGasAndMetaMaskFee', [metaMaskFee])} + </Text> + <Text variant={TextVariant.bodySm} color={TextColor.textDefault}> + <ViewAllQuotesLink + trackAllAvailableQuotesOpened={trackAllAvailableQuotesOpened} + setSelectQuotePopoverShown={setSelectQuotePopoverShown} + t={t} + /> + </Text> + </Box> + )} + {!isGasIncludedTrade && ( + <Box + display={DISPLAY.FLEX} + marginTop={3} + justifyContent={JustifyContent.center} + alignItems={AlignItems.center} + > + <Text + variant={TextVariant.bodySm} + color={TextColor.textAlternative} + > + * + {t('swapIncludesMetaMaskFeeViewAllQuotes', [ + metaMaskFee, + <ViewAllQuotesLink + key="view-all-quotes" + trackAllAvailableQuotesOpened={ + trackAllAvailableQuotesOpened } - } - size={Size.inherit} - > - {t('viewAllQuotes')} - </ButtonLink>, - ])} - </Text> - </Box> + setSelectQuotePopoverShown={setSelectQuotePopoverShown} + t={t} + />, + ])} + </Text> + </Box> + )} </Box> </div> <SwapsFooter diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.test.js b/ui/pages/swaps/prepare-swap-page/review-quote.test.js index 0734bb7be394..cacd52ca47ed 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.test.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.test.js @@ -3,12 +3,13 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { NetworkType } from '@metamask/controller-utils'; -import { setBackgroundConnection } from '../../../store/background-connection'; +import { act } from '@testing-library/react'; import { renderWithProvider, createSwapsMockStore, - MOCKS, } from '../../../../test/jest'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { getSwap1559GasFeeEstimates } from '../swaps.util'; import ReviewQuote from './review-quote'; jest.mock( @@ -16,17 +17,10 @@ jest.mock( () => () => '<InfoTooltipIcon />', ); -jest.mock('../../confirmations/hooks/useGasFeeInputs', () => { - return { - useGasFeeInputs: () => { - return { - maxFeePerGas: 16, - maxPriorityFeePerGas: 3, - gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(), - }; - }, - }; -}); +jest.mock('../swaps.util', () => ({ + ...jest.requireActual('../swaps.util'), + getSwap1559GasFeeEstimates: jest.fn(), +})); const middleware = [thunk]; const createProps = (customProps = {}) => { @@ -36,22 +30,17 @@ const createProps = (customProps = {}) => { }; }; -setBackgroundConnection({ - resetPostFetchState: jest.fn(), - safeRefetchQuotes: jest.fn(), - setSwapsErrorKey: jest.fn(), - updateTransaction: jest.fn(), - getGasFeeTimeEstimate: jest.fn(), - setSwapsQuotesPollingLimitEnabled: jest.fn(), -}); - describe('ReviewQuote', () => { + const getSwap1559GasFeeEstimatesMock = jest.mocked( + getSwap1559GasFeeEstimates, + ); + it('renders the component with initial props', () => { const store = configureMockStore(middleware)(createSwapsMockStore()); const props = createProps(); const { getByText } = renderWithProvider(<ReviewQuote {...props} />, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -73,7 +62,7 @@ describe('ReviewQuote', () => { const props = createProps(); const { getByText } = renderWithProvider(<ReviewQuote {...props} />, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -96,7 +85,7 @@ describe('ReviewQuote', () => { const props = createProps(); const { getByText } = renderWithProvider(<ReviewQuote {...props} />, store); expect(getByText('New quotes in')).toBeInTheDocument(); - expect(getByText('Quote rate')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); expect(getByText('Includes a 1% MetaMask fee –')).toBeInTheDocument(); expect(getByText('view all quotes')).toBeInTheDocument(); expect(getByText('Estimated gas fee')).toBeInTheDocument(); @@ -106,4 +95,120 @@ describe('ReviewQuote', () => { expect(getByText('Edit limit')).toBeInTheDocument(); expect(getByText('Swap')).toBeInTheDocument(); }); + + it('renders the component with gas included quotes', () => { + const state = createSwapsMockStore(); + state.metamask.swapsState.quotes.TEST_AGG_2.isGasIncludedTrade = true; + state.metamask.marketData[CHAIN_IDS.MAINNET][ + '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI token contract address. + ] = { + price: 2, + contractPercentChange1d: 0.004, + priceChange1d: 0.00004, + }; + state.metamask.currencyRates.ETH = { + conversionDate: 1708532473.416, + conversionRate: 2918.02, + usdConversionRate: 2918.02, + }; + const store = configureMockStore(middleware)(state); + const props = createProps(); + const { getByText } = renderWithProvider(<ReviewQuote {...props} />, store); + expect(getByText('New quotes in')).toBeInTheDocument(); + expect(getByText('Quote rate*')).toBeInTheDocument(); + expect( + getByText('* Includes gas and a 1% MetaMask fee'), + ).toBeInTheDocument(); + expect(getByText('view all quotes')).toBeInTheDocument(); + expect(getByText('Gas fee')).toBeInTheDocument(); + // $6.82 gas fee is calculated based on params set in the the beginning of the test. + expect(getByText('$6.82')).toBeInTheDocument(); + expect(getByText('Swap')).toBeInTheDocument(); + }); + + describe('uses gas fee estimates from transaction controller if 1559 and smart disabled', () => { + let smartDisabled1559State; + + beforeEach(() => { + smartDisabled1559State = createSwapsMockStore(); + smartDisabled1559State.metamask.selectedNetworkClientId = + NetworkType.mainnet; + smartDisabled1559State.metamask.networksMetadata = { + [NetworkType.mainnet]: { + EIPS: { 1559: true }, + status: 'available', + }, + }; + smartDisabled1559State.metamask.preferences.smartTransactionsOptInStatus = false; + }); + + it('with only trade transaction', async () => { + getSwap1559GasFeeEstimatesMock.mockResolvedValueOnce({ + estimatedBaseFee: '0x1', + tradeGasFeeEstimates: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + baseAndPriorityFeePerGas: '0x123456789123', + }, + approveGasFeeEstimates: undefined, + }); + + const store = configureMockStore(middleware)(smartDisabled1559State); + const props = createProps(); + const { getByText } = renderWithProvider( + <ReviewQuote {...props} />, + store, + ); + + await act(() => { + // Intentionally empty + }); + + expect(getByText('Estimated gas fee')).toBeInTheDocument(); + expect(getByText('3.94315 ETH')).toBeInTheDocument(); + expect(getByText('Max fee:')).toBeInTheDocument(); + expect(getByText('$7.37')).toBeInTheDocument(); + }); + + it('with trade and approve transactions', async () => { + smartDisabled1559State.metamask.swapsState.quotes.TEST_AGG_2.approvalNeeded = + { + data: '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00', + to: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + amount: '0', + from: '0x2369267687A84ac7B494daE2f1542C40E37f4455', + gas: '123456', + }; + + getSwap1559GasFeeEstimatesMock.mockResolvedValueOnce({ + estimatedBaseFee: '0x1', + tradeGasFeeEstimates: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x3', + baseAndPriorityFeePerGas: '0x123456789123', + }, + approveGasFeeEstimates: { + maxFeePerGas: '0x4', + maxPriorityFeePerGas: '0x5', + baseAndPriorityFeePerGas: '0x9876543210', + }, + }); + + const store = configureMockStore(middleware)(smartDisabled1559State); + const props = createProps(); + const { getByText } = renderWithProvider( + <ReviewQuote {...props} />, + store, + ); + + await act(() => { + // Intentionally empty + }); + + expect(getByText('Estimated gas fee')).toBeInTheDocument(); + expect(getByText('4.72438 ETH')).toBeInTheDocument(); + expect(getByText('Max fee:')).toBeInTheDocument(); + expect(getByText('$8.15')).toBeInTheDocument(); + }); + }); }); diff --git a/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap b/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap index 651c33786acc..f5e215c9fa5e 100644 --- a/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap +++ b/ui/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap @@ -5,7 +5,7 @@ exports[`SearchableItemList renders the component with initial props 1`] = ` class="MuiFormControl-root MuiTextField-root searchable-item-list__search MuiFormControl-fullWidth" > <div - class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth Mui-focused Mui-focused TextField-inputFocused-11 MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart" + class="MuiInputBase-root MuiInput-root TextField-inputRoot-12 MuiInputBase-fullWidth MuiInput-fullWidth Mui-focused Mui-focused TextField-inputFocused-11 MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart" > <div class="MuiInputAdornment-root MuiInputAdornment-positionStart" diff --git a/ui/pages/swaps/smart-transaction-status/index.scss b/ui/pages/swaps/smart-transaction-status/index.scss index 4229acca71c0..d19add085c65 100644 --- a/ui/pages/swaps/smart-transaction-status/index.scss +++ b/ui/pages/swaps/smart-transaction-status/index.scss @@ -36,26 +36,13 @@ width: 100%; } - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - + &__spacer-box { &--top { - width: 1634px; height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; } &--bottom { - width: 1600px; height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; } } diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index 157190687f31..b103ead2097c 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -8,11 +8,10 @@ import { getFetchParams, prepareToLeaveSwaps, getCurrentSmartTransactions, - getSelectedQuote, - getTopQuote, getCurrentSmartTransactionsEnabled, getSwapsNetworkConfig, cancelSwapsSmartTransaction, + getUsedQuote, } from '../../../ducks/swaps/swaps'; import { isHardwareWallet, @@ -74,9 +73,7 @@ export default function SmartTransactionStatusPage() { const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const needsTwoConfirmations = true; - const selectedQuote = useSelector(getSelectedQuote, isEqual); - const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const currentSmartTransactions = useSelector( getCurrentSmartTransactions, isEqual, @@ -368,7 +365,7 @@ export default function SmartTransactionStatusPage() { </Box> <Box marginTop={3} - className="smart-transaction-status__background-animation smart-transaction-status__background-animation--top" + className="smart-transaction-status__spacer-box--top" /> {icon && ( <Box marginTop={3} marginBottom={2}> @@ -443,7 +440,7 @@ export default function SmartTransactionStatusPage() { )} <Box marginTop={3} - className="smart-transaction-status__background-animation smart-transaction-status__background-animation--bottom" + className="smart-transaction-status__spacer-box--bottom" /> {subDescription && ( <Text diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js index d7ecace642ae..d081e8d58ee1 100644 --- a/ui/pages/swaps/swaps.util.test.js +++ b/ui/pages/swaps/swaps.util.test.js @@ -18,6 +18,7 @@ import { LINEA, BASE, } from '../../../shared/constants/swaps'; +import { estimateGasFee } from '../../store/actions'; import { TOKENS, EXPECTED_TOKENS_RESULT, @@ -35,6 +36,8 @@ import { showRemainingTimeInMinAndSec, getFeeForSmartTransaction, formatSwapsValueForDisplay, + fetchTopAssetsList, + getSwap1559GasFeeEstimates, } from './swaps.util'; jest.mock('../../../shared/lib/storage-helpers', () => ({ @@ -42,7 +45,24 @@ jest.mock('../../../shared/lib/storage-helpers', () => ({ setStorageItem: jest.fn(), })); +jest.mock('../../store/actions', () => ({ + estimateGasFee: jest.fn(), +})); + +const ESTIMATED_BASE_FEE_GWEI_MOCK = '1'; +const TRADE_TX_PARAMS_MOCK = { data: '0x123' }; +const APPROVE_TX_PARAMS_MOCK = { data: '0x456' }; +const CHAIN_ID_MOCK = '0x1'; +const MAX_FEE_PER_GAS_MOCK = '0x1'; +const MAX_PRIORITY_FEE_PER_GAS_MOCK = '0x2'; + describe('Swaps Util', () => { + const estimateGasFeeMock = jest.mocked(estimateGasFee); + + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { nock.cleanAll(); }); @@ -85,6 +105,25 @@ describe('Swaps Util', () => { }); }); + describe('fetchTopAssetsList', () => { + beforeEach(() => { + nock('https://swap.api.cx.metamask.io') + .persist() + .get('/networks/1/topAssets') + .reply(200, TOP_ASSETS); + }); + + it('should fetch top assets', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + + it('should fetch top assets on prod', async () => { + const result = await fetchTopAssetsList(CHAIN_IDS.MAINNET); + expect(result).toStrictEqual(TOP_ASSETS); + }); + }); + describe('fetchTopAssets', () => { beforeEach(() => { nock('https://swap.api.cx.metamask.io') @@ -525,4 +564,108 @@ describe('Swaps Util', () => { ).toBeNull(); }); }); + + describe('getSwap1559GasFeeEstimates', () => { + it('returns estimated base fee in WEI as hex', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + + const { estimatedBaseFee } = await getSwap1559GasFeeEstimates( + {}, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(estimatedBaseFee).toBe('3b9aca00'); + }); + + it('returns trade gas fee estimates', async () => { + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { tradeGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(tradeGasFeeEstimates).toStrictEqual({ + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + baseAndPriorityFeePerGas: '3b9aca02', + }); + + expect(estimateGasFeeMock).toHaveBeenCalledTimes(1); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: TRADE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + }); + + it('returns approve gas fee estimates if approve params', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { approveGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + APPROVE_TX_PARAMS_MOCK, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(approveGasFeeEstimates).toStrictEqual({ + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + baseAndPriorityFeePerGas: '3b9aca02', + }); + + expect(estimateGasFeeMock).toHaveBeenCalledTimes(2); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: TRADE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + expect(estimateGasFeeMock).toHaveBeenCalledWith({ + transactionParams: APPROVE_TX_PARAMS_MOCK, + chainId: CHAIN_ID_MOCK, + networkClientId: undefined, + }); + }); + + it('returns no approve gas fee estimates if no approve params', async () => { + estimateGasFeeMock.mockResolvedValueOnce({}); + estimateGasFeeMock.mockResolvedValueOnce({ + estimates: { + high: { + maxFeePerGas: MAX_FEE_PER_GAS_MOCK, + maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS_MOCK, + }, + }, + }); + + const { approveGasFeeEstimates } = await getSwap1559GasFeeEstimates( + TRADE_TX_PARAMS_MOCK, + undefined, + ESTIMATED_BASE_FEE_GWEI_MOCK, + CHAIN_ID_MOCK, + ); + + expect(approveGasFeeEstimates).toBeUndefined(); + }); + }); }); diff --git a/ui/pages/swaps/swaps.util.ts b/ui/pages/swaps/swaps.util.ts index ce06d4ef37f8..9cbf0b67a867 100644 --- a/ui/pages/swaps/swaps.util.ts +++ b/ui/pages/swaps/swaps.util.ts @@ -1,6 +1,10 @@ import { BigNumber } from 'bignumber.js'; -import { Json } from '@metamask/utils'; +import { Hex, Json } from '@metamask/utils'; import { IndividualTxFees } from '@metamask/smart-transactions-controller/dist/types'; +import { + FeeMarketGasFeeEstimates, + TransactionParams, +} from '@metamask/transaction-controller'; import { ALLOWED_CONTRACT_ADDRESSES, ARBITRUM, @@ -39,11 +43,14 @@ import { validateData, } from '../../../shared/lib/swaps-utils'; import { + addHexes, + decGWEIToHexWEI, decimalToHex, getValueFromWeiHex, sumHexes, } from '../../../shared/modules/conversion.utils'; import { EtherDenomination } from '../../../shared/constants/common'; +import { estimateGasFee } from '../../store/actions'; const CACHE_REFRESH_FIVE_MINUTES = 300000; const USD_CURRENCY_CODE = 'usd'; @@ -56,7 +63,7 @@ type Validator = { validator: (a: string) => boolean; }; -const TOKEN_VALIDATORS: Validator[] = [ +export const TOKEN_VALIDATORS: Validator[] = [ { property: 'address', type: 'string', @@ -199,9 +206,9 @@ export async function fetchAggregatorMetadata(chainId: any): Promise<object> { return filteredAggregators; } -// TODO: Replace `any` with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function fetchTopAssets(chainId: any): Promise<object> { +export async function fetchTopAssetsList( + chainId: string, +): Promise<{ address: string }[]> { const topAssetsUrl = getBaseApi('topAssets', chainId); const response = (await fetchWithCache({ @@ -210,14 +217,19 @@ export async function fetchTopAssets(chainId: any): Promise<object> { fetchOptions: { method: 'GET', headers: clientIdHeader }, cacheOptions: { cacheRefreshTime: CACHE_REFRESH_FIVE_MINUTES }, })) || []; + const topAssetsList = response.filter((asset: { address: string }) => + validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl), + ); + return topAssetsList; +} + +export async function fetchTopAssets( + chainId: string, +): Promise<Record<string, { index: string }>> { + const response = await fetchTopAssetsList(chainId); const topAssetsMap = response.reduce( - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_topAssetsMap: any, asset: { address: string }, index: number) => { - if (validateData(TOP_ASSET_VALIDATORS, asset, topAssetsUrl)) { - return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; - } - return _topAssetsMap; + (_topAssetsMap, asset: { address: string }, index: number) => { + return { ..._topAssetsMap, [asset.address]: { index: String(index) } }; }, {}, ); @@ -350,7 +362,8 @@ export const getFeeForSmartTransaction = ({ export function getRenderableNetworkFeesForQuote({ tradeGas, approveGas, - gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, USDConversionRate, @@ -363,7 +376,8 @@ export function getRenderableNetworkFeesForQuote({ }: { tradeGas: string; approveGas: string; - gasPrice: string; + gasPriceTrade: string; + gasPriceApprove: string; currentCurrency: string; conversionRate: number; USDConversionRate?: number; @@ -381,16 +395,17 @@ export function getRenderableNetworkFeesForQuote({ feeInEth: string; nonGasFee: string; } { - const totalGasLimitForCalculation = new BigNumber(tradeGas || '0x0', 16) - .plus(approveGas || '0x0', 16) - .toString(16); - let gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, gasPrice); - if (multiLayerL1FeeTotal !== null) { - gasTotalInWeiHex = sumHexes( - gasTotalInWeiHex || '0x0', - multiLayerL1FeeTotal || '0x0', - ); - } + const tradeGasFeeTotalHex = calcGasTotal(tradeGas, gasPriceTrade); + + const approveGasFeeTotalHex = approveGas + ? calcGasTotal(approveGas, gasPriceApprove) + : '0x0'; + + const gasTotalInWeiHex = sumHexes( + tradeGasFeeTotalHex, + approveGasFeeTotalHex, + multiLayerL1FeeTotal || '0x0', + ); const nonGasFee = new BigNumber(tradeValue, 16) .minus( @@ -442,7 +457,8 @@ export function getRenderableNetworkFeesForQuote({ export function quotesToRenderableData({ quotes, - gasPrice, + gasPriceTrade, + gasPriceApprove, conversionRate, currentCurrency, approveGas, @@ -453,7 +469,8 @@ export function quotesToRenderableData({ multiLayerL1ApprovalFeeTotal, }: { quotes: object; - gasPrice: string; + gasPriceTrade: string; + gasPriceApprove: string; conversionRate: number; currentCurrency: string; approveGas: string; @@ -512,7 +529,8 @@ export function quotesToRenderableData({ getRenderableNetworkFeesForQuote({ tradeGas: gasEstimateWithRefund || decimalToHex(averageGas || 800000), approveGas, - gasPrice, + gasPriceTrade, + gasPriceApprove, currentCurrency, conversionRate, tradeValue: trade.value, @@ -775,3 +793,60 @@ export const parseSmartTransactionsError = (errorMessage: string): string => { const errorJson = errorMessage.slice(12); return JSON.parse(errorJson.trim()); }; + +export const getSwap1559GasFeeEstimates = async ( + tradeTxParams: TransactionParams, + approveTxParams: TransactionParams | undefined, + estimatedBaseFeeGwei: string, + chainId: Hex, +) => { + const estimatedBaseFee = decGWEIToHexWEI(estimatedBaseFeeGwei) as Hex; + + const tradeGasFeeEstimates = await getTransaction1559GasFeeEstimates( + tradeTxParams, + estimatedBaseFee, + chainId, + ); + + const approveGasFeeEstimates = approveTxParams + ? await getTransaction1559GasFeeEstimates( + approveTxParams, + estimatedBaseFee, + chainId, + ) + : undefined; + + return { + tradeGasFeeEstimates, + approveGasFeeEstimates, + estimatedBaseFee, + }; +}; + +async function getTransaction1559GasFeeEstimates( + transactionParams: TransactionParams, + estimatedBaseFee: Hex, + chainId: Hex, +) { + const transactionGasFeeResponse = await estimateGasFee({ + transactionParams, + chainId, + }); + + const transactionGasFeeEstimates = transactionGasFeeResponse?.estimates as + | FeeMarketGasFeeEstimates + | undefined; + + const { maxFeePerGas } = transactionGasFeeEstimates?.high ?? {}; + const { maxPriorityFeePerGas } = transactionGasFeeEstimates?.high ?? {}; + + const baseAndPriorityFeePerGas = maxPriorityFeePerGas + ? (addHexes(estimatedBaseFee, maxPriorityFeePerGas) as Hex) + : undefined; + + return { + baseAndPriorityFeePerGas, + maxFeePerGas, + maxPriorityFeePerGas, + }; +} diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 8dc17ac3c765..be02ba840eb5 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -23,7 +23,6 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import FeeCard from '../fee-card'; import { getQuotes, - getSelectedQuote, getApproveTxParams, getFetchParams, setBalanceError, @@ -36,6 +35,7 @@ import { getDestinationTokenInfo, getUsedSwapsGasPrice, getTopQuote, + getUsedQuote, signAndSendTransactions, getBackgroundSwapRouteState, swapsQuoteSelected, @@ -181,9 +181,8 @@ export default function ViewQuote() { const balanceError = useSelector(getBalanceError); const fetchParams = useSelector(getFetchParams, isEqual); const approveTxParams = useSelector(getApproveTxParams, shallowEqual); - const selectedQuote = useSelector(getSelectedQuote, isEqual); const topQuote = useSelector(getTopQuote, isEqual); - const usedQuote = selectedQuote || topQuote; + const usedQuote = useSelector(getUsedQuote, isEqual); const tradeValue = usedQuote?.trade?.value ?? '0x0'; const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index c623e378c003..1b0a9dd603dd 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -8,6 +8,9 @@ export const getDataCollectionForMarketing = (state) => export const getParticipateInMetaMetrics = (state) => Boolean(state.metamask.participateInMetaMetrics); +export const getLatestMetricsEventTimestamp = (state) => + state.metamask.latestNonAnonymousEventTimestamp; + export const selectFragmentBySuccessEvent = createSelector( selectFragments, (_, fragmentOptions) => fragmentOptions, diff --git a/ui/selectors/metametrics.test.js b/ui/selectors/metametrics.test.js index 13185a47700b..454def7d92a4 100644 --- a/ui/selectors/metametrics.test.js +++ b/ui/selectors/metametrics.test.js @@ -2,6 +2,7 @@ const { selectFragmentBySuccessEvent, selectFragmentById, selectMatchingFragment, + getLatestMetricsEventTimestamp, } = require('.'); describe('selectFragmentBySuccessEvent', () => { @@ -68,4 +69,15 @@ describe('selectMatchingFragment', () => { }); expect(selected).toHaveProperty('id', 'randomid'); }); + describe('getLatestMetricsEventTimestamp', () => { + it('should find matching fragment in state by id', () => { + const state = { + metamask: { + latestNonAnonymousEventTimestamp: 12345, + }, + }; + const timestamp = getLatestMetricsEventTimestamp(state); + expect(timestamp).toBe(12345); + }); + }); }); 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/permissions.js b/ui/selectors/permissions.js index 65f2acf37c4b..fb32d41c9b17 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -2,6 +2,8 @@ import { ApprovalType } from '@metamask/controller-utils'; import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/snaps-rpc-methods'; import { isEvmAccountType } from '@metamask/keyring-api'; import { CaveatTypes } from '../../shared/constants/permissions'; +// eslint-disable-next-line import/no-restricted-paths +import { PermissionNames } from '../../app/scripts/controllers/permissions'; import { getApprovalRequestsByType } from './approvals'; import { createDeepEqualSelector } from './util'; import { @@ -60,6 +62,12 @@ export function getPermittedAccounts(state, origin) { ); } +export function getPermittedChains(state, origin) { + return getChainsFromPermission( + getChainsPermissionFromSubject(subjectSelector(state, origin)), + ); +} + /** * Selects the permitted accounts from the eth_accounts permission for the * origin of the current tab. @@ -75,6 +83,14 @@ export function getPermittedAccountsForSelectedTab(state, activeTab) { return getPermittedAccounts(state, activeTab); } +export function getPermittedChainsForCurrentTab(state) { + return getPermittedAccounts(state, getOriginOfCurrentTab(state)); +} + +export function getPermittedChainsForSelectedTab(state, activeTab) { + return getPermittedChains(state, activeTab); +} + /** * Returns a map of permitted accounts by origin for all origins. * @@ -92,6 +108,17 @@ export function getPermittedAccountsByOrigin(state) { }, {}); } +export function getPermittedChainsByOrigin(state) { + const subjects = getPermissionSubjects(state); + return Object.keys(subjects).reduce((acc, subjectKey) => { + const chains = getChainsFromSubject(subjects[subjectKey]); + if (chains.length > 0) { + acc[subjectKey] = chains; + } + return acc; + }, {}); +} + export function getSubjectMetadata(state) { return state.metamask.subjectMetadata; } @@ -256,6 +283,14 @@ function getAccountsPermissionFromSubject(subject = {}) { return subject.permissions?.eth_accounts || {}; } +function getChainsFromSubject(subject) { + return getChainsFromPermission(getChainsPermissionFromSubject(subject)); +} + +function getChainsPermissionFromSubject(subject = {}) { + return subject.permissions?.[PermissionNames.permittedChains] || {}; +} + function getAccountsFromPermission(accountsPermission) { const accountsCaveat = getAccountsCaveatFromPermission(accountsPermission); return accountsCaveat && Array.isArray(accountsCaveat.value) @@ -263,6 +298,22 @@ function getAccountsFromPermission(accountsPermission) { : []; } +function getChainsFromPermission(chainsPermission) { + const chainsCaveat = getChainsCaveatFromPermission(chainsPermission); + return chainsCaveat && Array.isArray(chainsCaveat.value) + ? chainsCaveat.value + : []; +} + +function getChainsCaveatFromPermission(chainsPermission = {}) { + return ( + Array.isArray(chainsPermission.caveats) && + chainsPermission.caveats.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + ) + ); +} + function getAccountsCaveatFromPermission(accountsPermission = {}) { return ( Array.isArray(accountsPermission.caveats) && diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 42ed4fecbdf4..2059c3a4678d 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -108,6 +108,7 @@ import { MultichainNativeAssets } from '../../shared/constants/multichain/assets // eslint-disable-next-line import/no-restricted-paths import { BridgeFeatureFlagsKey } from '../../app/scripts/controllers/bridge/types'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -537,6 +538,24 @@ export const getSelectedAccount = createDeepEqualSelector( }, ); +export const getWatchedToken = (transactionMeta) => + createSelector( + [getSelectedAccount, getAllTokens], + (selectedAccount, detectedTokens) => { + const { chainId } = transactionMeta; + + const selectedToken = detectedTokens?.[chainId]?.[ + selectedAccount.address + ]?.find( + (token) => + toChecksumHexAddress(token.address) === + toChecksumHexAddress(transactionMeta.txParams.to), + ); + + return selectedToken; + }, + ); + export function getTargetAccount(state, targetAddress) { const accounts = getMetaMaskAccounts(state); return accounts[targetAddress]; @@ -1325,6 +1344,10 @@ export function getShowWhatsNewPopup(state) { return state.appState.showWhatsNewPopup; } +export function getShowPermittedNetworkToastOpen(state) { + return state.appState.showPermittedNetworkToastOpen; +} + /** * Returns a memoized selector that gets the internal accounts from the Redux store. * @@ -1492,6 +1515,11 @@ export const getConnectedSitesList = createDeepEqualSelector( }, ); +export function getShouldShowAggregatedBalancePopover(state) { + const { shouldShowAggregatedBalancePopover } = getPreferences(state); + return shouldShowAggregatedBalancePopover; +} + export const getConnectedSnapsList = createDeepEqualSelector( getSnapsList, (snapsData) => { @@ -1978,6 +2006,10 @@ export function getShowPrivacyPolicyToast(state) { ); } +export function getLastViewedUserSurvey(state) { + return state.metamask.lastViewedUserSurvey; +} + export function getShowOutdatedBrowserWarning(state) { const { outdatedBrowserWarningLastShown } = state.metamask; if (!outdatedBrowserWarningLastShown) { @@ -2566,6 +2598,26 @@ export function getNameSources(state) { return state.metamask.nameSources || {}; } +export function getShowDeleteMetaMetricsDataModal(state) { + return state.appState.showDeleteMetaMetricsDataModal; +} + +export function getShowDataDeletionErrorModal(state) { + return state.appState.showDataDeletionErrorModal; +} + +export function getMetaMetricsDataDeletionId(state) { + return state.metamask.metaMetricsDataDeletionId; +} + +export function getMetaMetricsDataDeletionTimestamp(state) { + return state.metamask.metaMetricsDataDeletionTimestamp; +} + +export function getMetaMetricsDataDeletionStatus(state) { + return state.metamask.metaMetricsDataDeletionStatus; +} + /** * To get all installed snaps with proper metadata * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 342e7d7187c8..24b2a2afe125 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -11,6 +11,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; +import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -2018,4 +2019,65 @@ describe('#getConnectedSitesList', () => { }, }); }); + describe('#getShowDeleteMetaMetricsDataModal', () => { + it('returns state of showDeleteMetaMetricsDataModal', () => { + expect( + selectors.getShowDeleteMetaMetricsDataModal({ + appState: { + showDeleteMetaMetricsDataModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getShowDataDeletionErrorModal', () => { + it('returns state of showDataDeletionErrorModal', () => { + expect( + selectors.getShowDataDeletionErrorModal({ + appState: { + showDataDeletionErrorModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getMetaMetricsDataDeletionId', () => { + it('returns metaMetricsDataDeletionId', () => { + expect( + selectors.getMetaMetricsDataDeletionId({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123'); + }); + }); + describe('#getMetaMetricsDataDeletionTimestamp', () => { + it('returns metaMetricsDataDeletionTimestamp', () => { + expect( + selectors.getMetaMetricsDataDeletionTimestamp({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123345'); + }); + }); + describe('#getMetaMetricsDataDeletionStatus', () => { + it('returns metaMetricsDataDeletionStatus', () => { + expect( + selectors.getMetaMetricsDataDeletionStatus({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('INITIALIZED'); + }); + }); }); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index a54a5a220be8..6f8080e516ae 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -14,6 +14,10 @@ export const NETWORK_DROPDOWN_CLOSE = 'UI_NETWORK_DROPDOWN_CLOSE'; export const IMPORT_NFTS_MODAL_OPEN = 'UI_IMPORT_NFTS_MODAL_OPEN'; export const IMPORT_NFTS_MODAL_CLOSE = 'UI_IMPORT_NFTS_MODAL_CLOSE'; export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN'; +export const SHOW_PERMITTED_NETWORK_TOAST_OPEN = + 'UI_PERMITTED_NETWORK_TOAST_OPEN'; +export const SHOW_PERMITTED_NETWORK_TOAST_CLOSE = + 'UI_PERMITTED_NETWORK_TOAST_CLOSE'; export const SHOW_IPFS_MODAL_CLOSE = 'UI_IPFS_MODAL_CLOSE'; export const IMPORT_TOKENS_POPOVER_OPEN = 'UI_IMPORT_TOKENS_POPOVER_OPEN'; export const IMPORT_TOKENS_POPOVER_CLOSE = 'UI_IMPORT_TOKENS_POPOVER_CLOSE'; @@ -78,6 +82,10 @@ export const SHOW_NFT_DETECTION_ENABLEMENT_TOAST = export const TOGGLE_ACCOUNT_MENU = 'TOGGLE_ACCOUNT_MENU'; export const TOGGLE_NETWORK_MENU = 'TOGGLE_NETWORK_MENU'; +export const SET_SELECTED_ACCOUNTS_FOR_DAPP_CONNECTIONS = + 'SET_SELECTED_ACCOUNTS_FOR_DAPP_CONNECTIONS'; +export const SET_SELECTED_NETWORKS_FOR_DAPP_CONNECTIONS = + 'SET_SELECTED_NETWORKS_FOR_DAPP_CONNECTIONS'; // deprecated network modal export const DEPRECATED_NETWORK_POPOVER_OPEN = @@ -91,6 +99,14 @@ export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS'; export const SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING'; +export const DELETE_METAMETRICS_DATA_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_MODAL_OPEN'; +export const DELETE_METAMETRICS_DATA_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_MODAL_CLOSE'; +export const DATA_DELETION_ERROR_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_OPEN'; +export const DATA_DELETION_ERROR_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_CLOSE'; // locale export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE'; @@ -158,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 68c887c82a82..8d72ce63e32d 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -17,6 +17,10 @@ import { MetaMetricsNetworkEventSource } from '../../shared/constants/metametric import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../../test/stub/networks'; import { CHAIN_IDS } from '../../shared/constants/network'; +import { + CaveatTypes, + EndowmentTypes, +} from '../../shared/constants/permissions'; import * as actions from './actions'; import * as actionConstants from './actionConstants'; import { setBackgroundConnection } from './background-connection'; @@ -77,6 +81,10 @@ describe('Actions', () => { background.abortTransactionSigning = sinon.stub(); background.toggleExternalServices = sinon.stub(); background.getStatePatches = sinon.stub().callsFake((cb) => cb(null, [])); + background.removePermittedChain = sinon.stub(); + background.requestAccountsAndChainPermissionsWithId = sinon.stub(); + background.grantPermissions = sinon.stub(); + background.grantPermissionsIncremental = sinon.stub(); }); describe('#tryUnlockMetamask', () => { @@ -2530,4 +2538,151 @@ 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(); + }); + + it('calls removePermittedChain in the background', async () => { + const store = mockStore(); + + background.removePermittedChain.callsFake((_, __, cb) => cb()); + setBackgroundConnection(background); + + await store.dispatch(actions.removePermittedChain('test.com', '0x1')); + + expect( + background.removePermittedChain.calledWith( + 'test.com', + '0x1', + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('requestAccountsAndChainPermissionsWithId', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls requestAccountsAndChainPermissionsWithId in the background', async () => { + const store = mockStore(); + + background.requestAccountsAndChainPermissionsWithId.callsFake((_, cb) => + cb(), + ); + setBackgroundConnection(background); + + await store.dispatch( + actions.requestAccountsAndChainPermissionsWithId('test.com'), + ); + + expect( + background.requestAccountsAndChainPermissionsWithId.calledWith( + 'test.com', + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('grantPermittedChain', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls grantPermissionsIncremental in the background', async () => { + const store = mockStore(); + + background.grantPermissionsIncremental.callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await actions.grantPermittedChain('test.com', '0x1'); + expect( + background.grantPermissionsIncremental.calledWith( + { + subject: { origin: 'test.com' }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + sinon.match.func, + ), + ).toBe(true); + expect(store.getActions()).toStrictEqual([]); + }); + }); + + describe('grantPermittedChains', () => { + afterEach(() => { + sinon.restore(); + }); + + it('calls grantPermissions in the background', async () => { + const store = mockStore(); + + background.grantPermissions.callsFake((_, cb) => cb()); + setBackgroundConnection(background); + + await actions.grantPermittedChains('test.com', ['0x1', '0x2']); + expect( + background.grantPermissions.calledWith( + { + subject: { origin: 'test.com' }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1', '0x2'], + }, + ], + }, + }, + }, + sinon.match.func, + ), + ).toBe(true); + + expect(store.getActions()).toStrictEqual([]); + }); + }); }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index db23a2e5e7a2..91453590791c 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -28,6 +28,7 @@ import { UpdateProposedNamesResult, } from '@metamask/name-controller'; import { + GasFeeEstimates, TransactionMeta, TransactionParams, TransactionType, @@ -119,6 +120,11 @@ 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, +} from '../../shared/constants/permissions'; import * as actionConstants from './actionConstants'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { updateCustodyState } from './institutional/institution-actions'; @@ -1748,8 +1754,8 @@ export function setSelectedAccount( export function addPermittedAccount( origin: string, - address: [], -): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { + address: string, +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise<void>((resolve, reject) => { callBackgroundMethod( @@ -1767,14 +1773,14 @@ export function addPermittedAccount( await forceUpdateMetamaskState(dispatch); }; } -export function addMorePermittedAccounts( +export function addPermittedAccounts( origin: string, address: string[], -): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise<void>((resolve, reject) => { callBackgroundMethod( - 'addMorePermittedAccounts', + 'addPermittedAccounts', [origin, address], (error) => { if (error) { @@ -1792,7 +1798,7 @@ export function addMorePermittedAccounts( export function removePermittedAccount( origin: string, address: string, -): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> { +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch: MetaMaskReduxDispatch) => { await new Promise<void>((resolve, reject) => { callBackgroundMethod( @@ -1811,6 +1817,67 @@ export function removePermittedAccount( }; } +export function addPermittedChain( + origin: string, + chainId: string, +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise<void>((resolve, reject) => { + callBackgroundMethod('addPermittedChain', [origin, chainId], (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + await forceUpdateMetamaskState(dispatch); + }; +} +export function addPermittedChains( + origin: string, + chainIds: string[], +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise<void>((resolve, reject) => { + callBackgroundMethod( + 'addPermittedChains', + [origin, chainIds], + (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }, + ); + }); + await forceUpdateMetamaskState(dispatch); + }; +} + +export function removePermittedChain( + origin: string, + chainId: string, +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + await new Promise<void>((resolve, reject) => { + callBackgroundMethod( + 'removePermittedChain', + [origin, chainId], + (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }, + ); + }); + await forceUpdateMetamaskState(dispatch); + }; +} + export function showAccountsPage() { return { type: actionConstants.SHOW_ACCOUNTS_PAGE, @@ -2552,6 +2619,18 @@ export function hideImportNftsModal(): Action { }; } +export function hidePermittedNetworkToast(): Action { + return { + type: actionConstants.SHOW_PERMITTED_NETWORK_TOAST_CLOSE, + }; +} + +export function showPermittedNetworkToast(): Action { + return { + type: actionConstants.SHOW_PERMITTED_NETWORK_TOAST_OPEN, + }; +} + // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setConfirmationExchangeRates(value: Record<string, any>) { @@ -2923,6 +3002,7 @@ export function setFeatureFlag( export function setPreference( preference: string, value: boolean | string | object, + showLoading: boolan = true, ): ThunkAction< Promise<TemporaryPreferenceFlagDef>, MetaMaskReduxState, @@ -2930,13 +3010,13 @@ export function setPreference( AnyAction > { return (dispatch: MetaMaskReduxDispatch) => { - dispatch(showLoadingIndication()); + showLoading && dispatch(showLoadingIndication()); return new Promise<TemporaryPreferenceFlagDef>((resolve, reject) => { callBackgroundMethod<TemporaryPreferenceFlagDef>( 'setPreference', [preference, value], (err, updatedPreferences) => { - dispatch(hideLoadingIndication()); + showLoading && dispatch(hideLoadingIndication()); if (err) { dispatch(displayWarning(err)); reject(err); @@ -2958,10 +3038,8 @@ export function setDefaultHomeActiveTabName( }; } -export function setUseNativeCurrencyAsPrimaryCurrencyPreference( - value: boolean, -) { - return setPreference('useNativeCurrencyAsPrimaryCurrency', value); +export function setShowNativeTokenAsMainBalancePreference(value: boolean) { + return setPreference('showNativeTokenAsMainBalance', value); } export function setHideZeroBalanceTokens(value: boolean) { @@ -2972,6 +3050,14 @@ export function setShowFiatConversionOnTestnetsPreference(value: boolean) { return setPreference('showFiatInTestnets', value); } +/** + * Sets shouldShowAggregatedBalancePopover to false once the user toggles + * the setting to show native token as main balance. + */ +export function setAggregatedBalancePopoverShown() { + return setPreference('shouldShowAggregatedBalancePopover', false); +} + export function setShowTestNetworks(value: boolean) { return setPreference('showTestNetworks', value); } @@ -3000,6 +3086,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<void, MetaMaskReduxState, unknown, AnyAction> { @@ -3143,7 +3233,7 @@ export function toggleNetworkMenu(payload?: { }; } -export function setAccountDetailsAddress(address: string) { +export function setAccountDetailsAddress(address: string[]) { return { type: actionConstants.SET_ACCOUNT_DETAILS_ADDRESS, payload: address, @@ -3587,6 +3677,7 @@ export function fetchAndSetQuotes( fromAddress: string; balanceError: string; sourceDecimals: number; + enableGasIncludedQuotes: boolean; }, fetchParamsMetaData: { sourceTokenInfo: Token; @@ -3800,6 +3891,19 @@ export function requestAccountsPermissionWithId( }; } +export function requestAccountsAndChainPermissionsWithId( + origin: string, +): ThunkAction<Promise<void>, MetaMaskReduxState, unknown, AnyAction> { + return async (dispatch: MetaMaskReduxDispatch) => { + const id = await submitRequestToBackground( + 'requestAccountsAndChainPermissionsWithId', + [origin], + ); + await forceUpdateMetamaskState(dispatch); + return id; + }; +} + /** * Approves the permissions request. * @@ -4160,6 +4264,12 @@ export function setNewPrivacyPolicyToastClickedOrClosed() { }; } +export function setLastViewedUserSurvey(id: number) { + return async () => { + await submitRequestToBackground('setLastViewedUserSurvey', [id]); + }; +} + export function setOnboardingDate() { return async () => { await submitRequestToBackground('setOnboardingDate'); @@ -4379,6 +4489,14 @@ export function estimateGas(params: TransactionParams): Promise<Hex> { return submitRequestToBackground('estimateGas', [params]); } +export function estimateGasFee(request: { + transactionParams: TransactionParams; + chainId?: Hex; + networkClientId?: NetworkClientId; +}): Promise<{ estimates: GasFeeEstimates }> { + return submitRequestToBackground('estimateGasFee', [request]); +} + export async function updateTokenType( tokenAddress: string, ): Promise<Token | undefined> { @@ -4667,18 +4785,15 @@ export function signAndSendSmartTransaction({ unsignedTransaction, smartTransactionFees.fees, ); - const signedCanceledTransactions = await createSignedTransactions( - unsignedTransaction, - smartTransactionFees.cancelFees, - true, - ); try { const response = await submitRequestToBackground<{ uuid: string }>( 'submitSignedTransactions', [ { signedTransactions, - signedCanceledTransactions, + // The "signedCanceledTransactions" parameter is still expected by the STX controller but is no longer used. + // So we are passing an empty array. The parameter may be deprecated in a future update. + signedCanceledTransactions: [], txParams: unsignedTransaction, }, ], @@ -5362,6 +5477,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. * @@ -5557,6 +5700,48 @@ export async function getNextAvailableAccountName( ); } +export async function grantPermittedChain( + selectedTabOrigin: string, + chainId?: string, +): Promise<string> { + return await submitRequestToBackground<void>('grantPermissionsIncremental', [ + { + subject: { origin: selectedTabOrigin }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: [chainId], + }, + ], + }, + }, + }, + ]); +} + +export async function grantPermittedChains( + selectedTabOrigin: string, + chainIds: string[], +): Promise<string> { + return await submitRequestToBackground<void>('grantPermissions', [ + { + subject: { origin: selectedTabOrigin }, + approvedPermissions: { + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: chainIds, + }, + ], + }, + }, + }, + ]); +} + export async function decodeTransactionData({ transactionData, contractAddress, diff --git a/yarn.lock b/yarn.lock index 08b76a2b2048..f2992051fdf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,7 +2429,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^4.0.0, @ethereumjs/tx@npm:^4.0.2, @ethereumjs/tx@npm:^4.1.1, @ethereumjs/tx@npm:^4.2.0": +"@ethereumjs/tx@npm:^4.0.2, @ethereumjs/tx@npm:^4.1.1, @ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" dependencies: @@ -4762,18 +4762,18 @@ __metadata: languageName: node linkType: hard -"@metamask/account-watcher@npm:^4.1.0": - version: 4.1.0 - resolution: "@metamask/account-watcher@npm:4.1.0" +"@metamask/account-watcher@npm:^4.1.1": + version: 4.1.1 + resolution: "@metamask/account-watcher@npm:4.1.1" dependencies: "@ethereumjs/tx": "npm:^5.1.0" "@ethereumjs/util": "npm:^9.0.1" - "@metamask/keyring-api": "npm:^4.0.1" + "@metamask/keyring-api": "npm:^8.1.3" "@metamask/snaps-sdk": "npm:^6.2.1" "@metamask/utils": "npm:^8.3.0" ethers: "npm:^5.7.2" uuid: "npm:^9.0.0" - checksum: 10/51c150cc1a703c6726f7c11eb6b4906636a5c33cf25c2b60c7d120e67483fae37ac79ba46a5156518cb9666c2c64fea00f1d6ec23faa266b28a814c4fcefa561 + checksum: 10/a1b53cdcd3a5844c1edd2e91bf6d2e5a1f3914f795c928f9611c56bc4133c8338e4ae491cb2fda7273e59830a1d613ce17997a0639bb82ec5c71c2f0b260d88e languageName: node linkType: hard @@ -4800,14 +4800,14 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^18.2.1": - version: 18.2.1 - resolution: "@metamask/accounts-controller@npm:18.2.1" +"@metamask/accounts-controller@npm:^18.2.2": + version: 18.2.2 + resolution: "@metamask/accounts-controller@npm:18.2.2" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/base-controller": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.3" - "@metamask/keyring-api": "npm:^8.1.0" + "@metamask/eth-snap-keyring": "npm:^4.3.6" + "@metamask/keyring-api": "npm:^8.1.3" "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/utils": "npm:^9.1.0" @@ -4818,7 +4818,7 @@ __metadata: peerDependencies: "@metamask/keyring-controller": ^17.0.0 "@metamask/snaps-controllers": ^9.7.0 - checksum: 10/cad8d68e5c5d8b349fcf5bfd6bc900cccbb5ad54bdcf2678a4469f7b3118064ca26bedafaafa89bea6ddce6f0cfb22af8eb8b7958bbd6cfce916f19a91a8e770 + checksum: 10/095be37c94a577304425f80600d4ef847c83c702ccf3d6b1591602d1fe292bdd3273131e336d6108bd713bff38812dfc4d7b21d4075669cde24e12f117f2dd81 languageName: node linkType: hard @@ -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 @@ -4943,10 +4981,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.6.0": - version: 0.6.0 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.0" - checksum: 10/baf4d7a43ddb5f210437c722e90abc6a3b4056390cc1d075e1a09acb82e934db338fce36fb897560e7f9ecd8ff3fcbd4795b3076dc7243af7ac93ea5d47b63f5 +"@metamask/bitcoin-wallet-snap@npm:^0.6.1": + version: 0.6.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.1" + checksum: 10/9c595e328cd63efe62cdda4194efe44ab3da4a54a89007f485280924aa9e8ee37042bda0a07751f3ce01c2c3e4740b16cd130f07558aa84cd57b20a8d5f1d3a7 languageName: node linkType: hard @@ -5344,21 +5382,22 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.3": - version: 4.3.3 - resolution: "@metamask/eth-snap-keyring@npm:4.3.3" +"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6": + version: 4.3.6 + resolution: "@metamask/eth-snap-keyring@npm:4.3.6" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/keyring-api": "npm:^8.1.0" - "@metamask/snaps-controllers": "npm:^9.6.0" - "@metamask/snaps-sdk": "npm:^6.4.0" - "@metamask/snaps-utils": "npm:^7.8.0" + "@metamask/snaps-controllers": "npm:^9.7.0" + "@metamask/snaps-sdk": "npm:^6.5.1" + "@metamask/snaps-utils": "npm:^7.8.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - "@types/uuid": "npm:^9.0.1" - uuid: "npm:^9.0.0" - checksum: 10/035c82afef82a4cee7bc63b5c4f152a132b683017ec90a4b614764a4bc7adcca8faccf78c25adcddca2d29eee2fed08706f07d72afb93640956b86e862d4f555 + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + peerDependencies: + "@metamask/keyring-api": ^8.1.3 + checksum: 10/378dce125ba9e38b9ba7d9b7124383b4fd8d2782207dc69e1ae9e262beb83f22044eae5200986d4c353de29e5283c289e56b3acb88c8971a63f9365bdde3d5b4 languageName: node linkType: hard @@ -5379,17 +5418,17 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-trezor-keyring@npm:^3.1.0": - version: 3.1.0 - resolution: "@metamask/eth-trezor-keyring@npm:3.1.0" +"@metamask/eth-trezor-keyring@npm:^3.1.3": + version: 3.1.3 + resolution: "@metamask/eth-trezor-keyring@npm:3.1.3" dependencies: - "@ethereumjs/tx": "npm:^4.0.0" - "@ethereumjs/util": "npm:^8.0.0" - "@metamask/eth-sig-util": "npm:^7.0.1" + "@ethereumjs/tx": "npm:^4.2.0" + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/eth-sig-util": "npm:^7.0.3" "@trezor/connect-plugin-ethereum": "npm:^9.0.3" "@trezor/connect-web": "npm:^9.1.11" hdkey: "npm:^2.1.0" - checksum: 10/2e72ab89f757494f4e4ddf46a6ddd4b6ac7db15d051d6252cd883fff537df01235f56fe9c6d02e8da03cf735a6c67f9bcdf35e50895cab034f88e838b1100b81 + checksum: 10/d32a687bcaab4593e6208a1bb59cbdd2b111eff357fd30e707787454ef571abfb4e6162422504f730f3ab2fe576b555d68114de0406ae5cdad252dab1b635cce languageName: node linkType: hard @@ -5590,18 +5629,6 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^6.0.2": - version: 6.0.2 - resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" - dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^8.3.0" - readable-stream: "npm:^3.6.2" - checksum: 10/eb6fc179959206abeba8b12118757d55cc0028681566008a4005b570d21a9369795452e1bdb672fc9858f46a4e9ed5c996cfff0e85b47cef8bf39a6edfee8f1e - languageName: node - linkType: hard - "@metamask/json-rpc-middleware-stream@npm:^8.0.1, @metamask/json-rpc-middleware-stream@npm:^8.0.2": version: 8.0.2 resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.2" @@ -5627,21 +5654,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^4.0.1": - version: 4.0.2 - resolution: "@metamask/keyring-api@npm:4.0.2" - dependencies: - "@metamask/providers": "npm:^15.0.0" - "@metamask/snaps-sdk": "npm:^3.1.1" - "@metamask/utils": "npm:^8.3.0" - "@types/uuid": "npm:^9.0.1" - superstruct: "npm:^1.0.3" - uuid: "npm:^9.0.0" - checksum: 10/8f6dc3b4913803eba8e22228ac6307ca66247900d70755a6dd457c2037b9fb6d3979da472a08e24ccdd81c28c68db3ad41219d915e5e8442ef640a3c0c46b261 - languageName: node - linkType: hard - -"@metamask/keyring-api@npm:^8.0.0, @metamask/keyring-api@npm:^8.1.0, @metamask/keyring-api@npm:^8.1.3": +"@metamask/keyring-api@npm:^8.0.0, @metamask/keyring-api@npm:^8.1.3": version: 8.1.3 resolution: "@metamask/keyring-api@npm:8.1.3" dependencies: @@ -5657,7 +5670,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.1, @metamask/keyring-controller@npm:^17.2.2": +"@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.2": version: 17.2.2 resolution: "@metamask/keyring-controller@npm:17.2.2" dependencies: @@ -5983,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" @@ -6000,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" @@ -6095,26 +6108,6 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^15.0.0": - version: 15.0.0 - resolution: "@metamask/providers@npm:15.0.0" - dependencies: - "@metamask/json-rpc-engine": "npm:^7.3.2" - "@metamask/json-rpc-middleware-stream": "npm:^6.0.2" - "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^8.3.0" - detect-browser: "npm:^5.2.0" - extension-port-stream: "npm:^3.0.0" - fast-deep-equal: "npm:^3.1.3" - is-stream: "npm:^2.0.0" - readable-stream: "npm:^3.6.2" - webextension-polyfill: "npm:^0.10.0" - checksum: 10/d022fe6d2db577fcd299477f19dd1a0ca88baeae542d8a80330694d004bffc289eecf7008c619408c819de8f43eb9fc989b27e266a5961ffd43cb9c2ec749dd5 - languageName: node - linkType: hard - "@metamask/providers@npm:^17.1.2": version: 17.2.0 resolution: "@metamask/providers@npm:17.2.0" @@ -6213,20 +6206,20 @@ __metadata: languageName: node linkType: hard -"@metamask/signature-controller@npm:^19.0.0": - version: 19.0.0 - resolution: "@metamask/signature-controller@npm:19.0.0" +"@metamask/signature-controller@npm:^19.1.0": + version: 19.1.0 + resolution: "@metamask/signature-controller@npm:19.1.0" dependencies: - "@metamask/base-controller": "npm:^7.0.0" - "@metamask/controller-utils": "npm:^11.1.0" - "@metamask/message-manager": "npm:^10.0.3" + "@metamask/base-controller": "npm:^7.0.1" + "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/message-manager": "npm:^10.1.1" "@metamask/utils": "npm:^9.1.0" lodash: "npm:^4.17.21" peerDependencies: "@metamask/approval-controller": ^7.0.0 "@metamask/keyring-controller": ^17.0.0 "@metamask/logging-controller": ^6.0.0 - checksum: 10/9eec874bddee00a969a0231367c55c2b1768ad029c8125929603544ddc94b1e7c833457e39aa0aa5fed19608cb68633f0a90ca40a5639a8d6e2c84dbf9756feb + checksum: 10/ac01b4ba6708e2e74b92ef1c5d4fb9aeff06ae2bd3b445fe8a10bc8e84641ad3bed6fb245f0303ef9d13b7458d022ef07d5ce211a05b14e1ad5ce44ad49cd4ec languageName: node linkType: hard @@ -6267,7 +6260,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.7.0": +"@metamask/snaps-controllers@npm:^9.7.0": version: 9.7.0 resolution: "@metamask/snaps-controllers@npm:9.7.0" dependencies: @@ -6364,7 +6357,7 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.0": +"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -6507,9 +6500,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^37.0.0": - version: 37.0.0 - resolution: "@metamask/transaction-controller@npm:37.0.0" +"@metamask/transaction-controller@npm:^37.2.0": + version: 37.2.0 + resolution: "@metamask/transaction-controller@npm:37.2.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6536,7 +6529,7 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^20.0.0 "@metamask/network-controller": ^21.0.0 - checksum: 10/b4608260cb86ad1a867926b983a21050a2be899f17af909ad2403b5148eada348b0fbb3f7ecef9ebc7cf8d28c040ce4d6f5009709328cda00fab61e10fa94de6 + checksum: 10/0850797efb2157de41eaec153d31f8f63d194d2290fa41a3d439a28f95a35436f47d56546b0fa64427294280476d11ab4a7ed6161a13ad6f8215a3bc052a41e2 languageName: node linkType: hard @@ -6587,9 +6580,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": - version: 9.2.1 - resolution: "@metamask/utils@npm:9.2.1" +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1, @metamask/utils@npm:^9.3.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -6600,7 +6593,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/2192797afd91af19898e107afeaf63e89b61dc7285e0a75d0cc814b5b288e4cdfc856781b01904034c4d2c1efd9bdab512af24c7e4dfe7b77a03f1f3d9dec7e8 + checksum: 10/ed6648cd973bbf3b4eb0e862903b795a99d27784c820e19f62f0bc0ddf353e98c2858d7e9aaebc0249a586391b344e35b9249d13c08e3ea0c74b23dc1c6b1558 languageName: node linkType: hard @@ -7830,6 +7823,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.19.2": + version: 1.19.2 + resolution: "@remix-run/router@npm:1.19.2" + checksum: 10/31b62b66ea68bd62018189047de7b262700113438f62407df019f81a9856a08a705b2b77454be9293518e2f5f3bbf3f8b858ac19f48cb7d89f8ab56b7b630c19 + languageName: node + linkType: hard + "@scure/base@npm:^1.0.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" @@ -13301,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" @@ -13485,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" @@ -14378,9 +14396,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": - version: 1.0.30001600 - resolution: "caniuse-lite@npm:1.0.30001600" - checksum: 10/4c52f83ed71bc5f6e443bd17923460f1c77915adc2c2aa79ddaedceccc690b5917054b0c41b79e9138cbbd9abcdc0db9e224e79e3e734e581dfec06505f3a2b4 + version: 1.0.30001660 + resolution: "caniuse-lite@npm:1.0.30001660" + checksum: 10/5d83f0b7e2075b7e31f114f739155dc6c21b0afe8cb61180f625a4903b0ccd3d7591a5f81c930f14efddfa57040203ba0890850b8a3738f6c7f17c7dd83b9de8 languageName: node linkType: hard @@ -20797,13 +20815,6 @@ __metadata: languageName: node linkType: hard -"gud@npm:^1.0.0": - version: 1.0.0 - resolution: "gud@npm:1.0.0" - checksum: 10/3e2eb37cf794364077c18f036d6aa259c821c7fd188f2b7935cb00d589d82a41e0ebb1be809e1a93679417f62f1ad0513e745c3cf5329596e489aef8c5e5feae - languageName: node - linkType: hard - "gulp-autoprefixer@npm:^8.0.0": version: 8.0.0 resolution: "gulp-autoprefixer@npm:8.0.0" @@ -21294,12 +21305,12 @@ __metadata: languageName: node linkType: hard -"history@npm:^5.0.0": - version: 5.0.0 - resolution: "history@npm:5.0.0" +"history@npm:^5.3.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" dependencies: "@babel/runtime": "npm:^7.7.6" - checksum: 10/d0b744c2028a163aebcee8df89400d6ed7eadc5ea877b0324040d1127a88d6b39395ea5a5f28a1912c75473953e3782c6fb682d363efb98e87a0cc49de95a2c9 + checksum: 10/52ba685b842ca6438ff11ef459951eb13d413ae715866a8dc5f7c3b1ea0cdeb8db6aabf7254551b85f56abc205e6e2d7e1d5afb36b711b401cdaff4f2cf187e9 languageName: node linkType: hard @@ -25347,6 +25358,13 @@ __metadata: languageName: node linkType: hard +"lottie-web@npm:^5.12.2": + version: 5.12.2 + resolution: "lottie-web@npm:5.12.2" + checksum: 10/cd377d54a675b37ac9359306b84097ea402dff3d74a2f45e6e0dbcff1df94b3a978e92e48fd34765754bdbb94bd2d8d4da31954d95f156e77489596b235cac91 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -26073,16 +26091,16 @@ __metadata: "@metamask-institutional/transaction-update": "npm:^0.2.5" "@metamask-institutional/types": "npm:^1.1.0" "@metamask/abi-utils": "npm:^2.0.2" - "@metamask/account-watcher": "npm:^4.1.0" - "@metamask/accounts-controller": "npm:^18.2.1" + "@metamask/account-watcher": "npm:^4.1.1" + "@metamask/accounts-controller": "npm:^18.2.2" "@metamask/address-book-controller": "npm:^6.0.0" "@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.0" + "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" @@ -26101,9 +26119,9 @@ __metadata: "@metamask/eth-ledger-bridge-keyring": "npm:^3.0.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.3" + "@metamask/eth-snap-keyring": "npm:^4.3.6" "@metamask/eth-token-tracker": "npm:^8.0.0" - "@metamask/eth-trezor-keyring": "npm:^3.1.0" + "@metamask/eth-trezor-keyring": "npm:^3.1.3" "@metamask/etherscan-link": "npm:^3.0.0" "@metamask/ethjs": "npm:^0.6.0" "@metamask/ethjs-contract": "npm:^0.4.1" @@ -26111,8 +26129,8 @@ __metadata: "@metamask/forwarder": "npm:^1.1.0" "@metamask/gas-fee-controller": "npm:^18.0.0" "@metamask/jazzicon": "npm:^2.0.0" - "@metamask/keyring-api": "npm:^8.1.0" - "@metamask/keyring-controller": "npm:^17.2.1" + "@metamask/keyring-api": "npm:^8.1.3" + "@metamask/keyring-controller": "npm:^17.2.2" "@metamask/logging-controller": "npm:^6.0.0" "@metamask/logo": "npm:^3.1.2" "@metamask/message-manager": "npm:^10.1.0" @@ -26139,7 +26157,7 @@ __metadata: "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^18.0.1" - "@metamask/signature-controller": "npm:^19.0.0" + "@metamask/signature-controller": "npm:^19.1.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.7.0" "@metamask/snaps-execution-environments": "npm:^6.7.2" @@ -26148,9 +26166,9 @@ __metadata: "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" - "@metamask/transaction-controller": "npm:^37.0.0" + "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^9.3.0" "@ngraveio/bc-ur": "npm:^1.1.12" "@noble/hashes": "npm:^1.3.3" "@octokit/core": "npm:^3.6.0" @@ -26242,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" @@ -26313,7 +26332,7 @@ __metadata: gulp-watch: "npm:^5.0.1" gulp-zip: "npm:^5.1.0" he: "npm:^1.2.0" - history: "npm:^5.0.0" + history: "npm:^5.3.0" html-bundler-webpack-plugin: "npm:^3.17.3" https-browserify: "npm:^1.0.0" human-standard-token-abi: "npm:^2.0.0" @@ -26340,6 +26359,7 @@ __metadata: lodash: "npm:^4.17.21" loglevel: "npm:^1.8.1" loose-envify: "npm:^1.4.0" + lottie-web: "npm:^5.12.2" luxon: "npm:^3.2.1" mocha: "npm:^10.2.0" mocha-junit-reporter: "npm:^2.2.1" @@ -26376,7 +26396,8 @@ __metadata: react-popper: "npm:^2.2.3" react-redux: "npm:^7.2.9" react-responsive-carousel: "npm:^3.2.21" - react-router-dom: "npm:^5.1.2" + react-router-dom: "npm:^5.3.4" + react-router-dom-v5-compat: "npm:^6.26.2" react-simple-file-input: "npm:^2.0.0" react-syntax-highlighter: "npm:^15.5.0" react-tippy: "npm:^1.2.2" @@ -26954,20 +26975,6 @@ __metadata: languageName: node linkType: hard -"mini-create-react-context@npm:^0.3.0": - version: 0.3.2 - resolution: "mini-create-react-context@npm:0.3.2" - dependencies: - "@babel/runtime": "npm:^7.4.0" - gud: "npm:^1.0.0" - tiny-warning: "npm:^1.0.2" - peerDependencies: - prop-types: ^15.0.0 - react: ^0.14.0 || ^15.0.0 || ^16.0.0 - checksum: 10/507e36241965e2dad99ffe191809b0b9dc5e949df03b68000a91a845e12ea3bda8fd4cd35a1f033f3781a72942c7b0208fc1876f37656c7fc7be7d4472f45589 - languageName: node - linkType: hard - "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -30676,32 +30683,46 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^5.1.2": - version: 5.1.2 - resolution: "react-router-dom@npm:5.1.2" +"react-router-dom-v5-compat@npm:^6.26.2": + version: 6.26.2 + resolution: "react-router-dom-v5-compat@npm:6.26.2" dependencies: - "@babel/runtime": "npm:^7.1.2" + "@remix-run/router": "npm:1.19.2" + history: "npm:^5.3.0" + react-router: "npm:6.26.2" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + react-router-dom: 4 || 5 + checksum: 10/0662c16f8fbed2d89b79d7977c94961c331f576bf1c638ba9782656d72a57ab49f081940effc796913870f34a3ebac01287b1bdcb67750b2b04d35e6b59f8180 + languageName: node + linkType: hard + +"react-router-dom@npm:^5.3.4": + version: 5.3.4 + resolution: "react-router-dom@npm:5.3.4" + dependencies: + "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" loose-envify: "npm:^1.3.1" prop-types: "npm:^15.6.2" - react-router: "npm:5.1.2" + react-router: "npm:5.3.4" tiny-invariant: "npm:^1.0.2" tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/a6225fc454780db6afa5da97ac862abe8514f373a6c81d59a8c4d15c6c42eac0ccce76a468ec0ca216d327e84640561fc12af9759de4e12be09ed7fe1db08bb2 + checksum: 10/5e0696ae2d86f466ff700944758a227e1dcd79b48797d567776506e4e3b4a08b81336155feb86a33be9f38c17c4d3d94212b5c60c8ee9a086022e4fd3961db29 languageName: node linkType: hard -"react-router@npm:5.1.2": - version: 5.1.2 - resolution: "react-router@npm:5.1.2" +"react-router@npm:5.3.4": + version: 5.3.4 + resolution: "react-router@npm:5.3.4" dependencies: - "@babel/runtime": "npm:^7.1.2" + "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" hoist-non-react-statics: "npm:^3.1.0" loose-envify: "npm:^1.3.1" - mini-create-react-context: "npm:^0.3.0" path-to-regexp: "npm:^1.7.0" prop-types: "npm:^15.6.2" react-is: "npm:^16.6.0" @@ -30709,7 +30730,18 @@ __metadata: tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/bba4a23090fa02364e21e03ad7b2ff4136ff262871be197b3031e4a03180e36bc9f03fc91c060ebbca58e00d4b59d3a99281a6ef26b6dea37479a5097b8ca2e2 + checksum: 10/99d54a99af6bc6d7cad2e5ea7eee9485b62a8b8e16a1182b18daa7fad7dafa5e526850eaeebff629848b297ae055a9cb5b4aba8760e81af8b903efc049d48f5c + languageName: node + linkType: hard + +"react-router@npm:6.26.2": + version: 6.26.2 + resolution: "react-router@npm:6.26.2" + dependencies: + "@remix-run/router": "npm:1.19.2" + peerDependencies: + react: ">=16.8" + checksum: 10/496e855b53e61066c1791e354f5d79eab56a128d9722fdc6486c3ecd3b3a0bf9968e927028f429893b157f3cc10fc09e890a055847723ee242663e7995fedc9d languageName: node linkType: hard @@ -32864,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"